Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
([#107](https://github.com/watts-dev/watts/pull/107))
* GCMAT plugin via the `PluginGCMAT` class
([#114](https://github.com/watts-dev/watts/pull/114))
* A-LEAF plugin via the `PluginALEAF` class
([#119](https://github.com/watts-dev/watts/pull/119))

### Changes

Expand Down
1 change: 1 addition & 0 deletions doc/source/reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ API Reference
watts.PluginGeneric
watts.PluginABCE
watts.PluginACCERT
watts.PluginALEAF
watts.PluginGCMat
watts.PluginMCNP
watts.PluginMOOSE
Expand Down
39 changes: 39 additions & 0 deletions doc/source/user/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,45 @@ As with other plugins, :class:`~watts.PluginABCE` is easily used by::
.. note::
`ABCE` is still under active development.

A-LEAF Plugin
+++++++++++++
The :class:`~watts.PluginALEAF` class enables simulations with the Argonne
Low-carbon Electricity Analysis Framework (A-LEAF) code using a text based
input file for fuel price modification. The A-LEAF code using an excel based
input file, and the A-LEAF plugin will only modify the fuel price under the
``Fuel`` section of the input file. Since A-LEAF code is still under
active development, the plugin will be updated as the code is updated.

The A-LEAF fuel price input file can be templated as follows:

.. code-block:: jinja

Scenario FUEL Type Unit 2022 2023 2024 2025 2026 ...

Reference Coal Real 2022 $/MMBtu 1.64 1.61 1.65 1.57 1.52 ...
...
Base Nuclear Real 2020 $/MMBTU {% for year, price in fuel_price.items() %}
{{ price }}{% if not loop.last %} {% endif %}{% endfor %}
Base Biomass Real 2020 $/MMBTU 5.00 5.11 5.22 5.34 5.45 ...
...
Low OGS Real 2020 $/MMBTU 3.30 3.00 2.73 2.58 2.56 ...

Before running the A-LEAF plugin, you need to specify the directory that the
executable and the license key are in (they must be in the same directory). This
can be done by adding the ``A-LEAF_DIR`` variable to the environment or by
explicitly specifying the path in the Python script as::

aleaf_plugin = watts.PluginALEAF(
'aleaf_template',
executable="/path/to/A-LEAF",
extra_inputs=[]
)
Comment thread
JiaZhou-PU marked this conversation as resolved.

As with other plugins, :class:`~watts.PluginALEAF` can be used by calling the
:meth:`~watts.PluginALEAF` instance directly the same way as other plugins.
aleaf_plugin = watts.PluginALEAF('aleaf_template')
aleaf_result = aleaf_plugin(params)


Dakota Plugin
+++++++++++++
Expand Down
19 changes: 19 additions & 0 deletions examples/1App_ALAEF_LCGTEP/Fuel.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Scenario FUEL Type Unit 2022 2023 2024 2025 2026 2027 2028 2029 2030 2031 2032 2033 2034 2035 2036 2037 2038 2039 2040 2041 2042 2043 2044 2045 2046 2047 2048 2049 2050 2051 2052 2053 2054 2055 2056 2057 2058 2059 2060
Reference Coal Real 2021 $/MMBtu 1.64 1.61 1.65 1.57 1.52 1.50 1.49 1.50 1.50 1.50 1.50 1.50 1.51 1.52 1.53 1.54 1.56 1.56 1.58 1.59 1.60 1.60 1.62 1.63 1.64 1.65 1.65 1.66 1.66 1.75 1.85 1.96 2.07 2.19 2.31 2.44 2.58 2.73 2.89
Reference NG Real 2021 $/MMBtu 3.84 3.49 3.17 3.00 2.98 3.08 3.25 3.36 3.46 3.54 3.58 3.65 3.64 3.64 3.65 3.67 3.68 3.69 3.72 3.73 3.70 3.71 3.65 3.61 3.60 3.60 3.62 3.60 3.59 3.80 4.01 4.24 4.48 4.74 5.01 5.29 5.59 5.91 6.25
Reference Petroleum Real 2021 $/MMBtu 12.18 10.50 11.39 11.61 11.89 12.20 12.46 12.58 12.82 13.08 13.27 13.42 13.54 13.67 13.86 14.03 14.20 14.26 14.55 14.70 14.75 15.00 15.28 15.36 15.58 15.65 15.62 15.65 15.59 16.48 17.42 18.41 19.46 20.57 21.74 22.98 24.29 25.68 27.14
Reference Nuclear Real 2021 $/MMBtu 0.72 0.72 0.72 0.72 0.72 0.73 0.73 0.73 0.73 0.73 0.73 0.74 0.74 0.74 0.74 0.74 0.74 0.75 0.75 0.75 0.75 0.76 0.76 0.76 0.76 0.76 0.77 0.77 0.77 0.81 0.86 0.91 0.96 1.02 1.07 1.14 1.20 1.27 1.34
Reference Biomass Real 2020 $/MMBTU 5.00 5.11 5.22 5.34 5.45 5.57 5.70 5.82 5.95 6.08 6.22 6.35 6.49 6.63 6.78 6.93 7.08 7.24 7.40 7.56 7.73 7.90 8.07 8.25 8.43 8.61 8.80 9.00 9.20 9.72 10.27 10.86 11.48 12.13 12.82 13.56 14.33 15.15 16.01
Reference OGS Real 2020 $/MMBTU 3.84 3.49 3.17 3.00 2.98 3.08 3.25 3.36 3.46 3.54 3.58 3.65 3.64 3.64 3.65 3.67 3.68 3.69 3.72 3.73 3.70 3.71 3.65 3.61 3.60 3.60 3.62 3.60 3.59 3.80 4.01 4.24 4.48 4.74 5.01 5.29 5.59 5.91 6.25
Base Coal Real 2020 $/MMBTU 1.57 1.54 1.57 1.50 1.45 1.43 1.43 1.43 1.43 1.44 1.43 1.43 1.44 1.45 1.46 1.47 1.49 1.49 1.51 1.52 1.52 1.53 1.54 1.56 1.56 1.57 1.57 1.58 1.58 1.67 1.77 1.87 1.98 2.09 2.21 2.33 2.47 2.61 2.76
Base NG Real 2020 $/MMBTU 3.67 3.34 3.03 2.87 2.84 2.94 3.10 3.21 3.30 3.38 3.42 3.48 3.48 3.47 3.48 3.50 3.52 3.52 3.55 3.56 3.54 3.54 3.48 3.45 3.44 3.43 3.46 3.44 3.43 3.62 3.83 4.05 4.28 4.52 4.78 5.05 5.34 5.65 5.97
Base Petroleum Real 2020 $/MMBTU 11.63 10.03 10.88 11.09 11.35 11.65 11.90 12.01 12.24 12.49 12.68 12.82 12.93 13.05 13.24 13.40 13.56 13.61 13.90 14.04 14.09 14.32 14.60 14.67 14.88 14.95 14.92 14.94 14.89 15.74 16.63 17.58 18.58 19.64 20.76 21.95 23.20 24.52 25.92
Base Nuclear Real 2020 $/MMBTU {% for year, price in fuel_price.items() %}{{ price }}{% if not loop.last %} {% endif %}{% endfor %}
Base Biomass Real 2020 $/MMBTU 5.00 5.11 5.22 5.34 5.45 5.57 5.70 5.82 5.95 6.08 6.22 6.35 6.49 6.63 6.78 6.93 7.08 7.24 7.40 7.56 7.73 7.90 8.07 8.25 8.43 8.61 8.80 9.00 9.20 9.72 10.27 10.86 11.48 12.13 12.82 13.56 14.33 15.15 16.01
Base OGS Real 2020 $/MMBTU 3.67 3.34 3.03 2.87 2.84 2.94 3.10 3.21 3.30 3.38 3.42 3.48 3.48 3.47 3.48 3.50 3.52 3.52 3.55 3.56 3.54 3.54 3.48 3.45 3.44 3.43 3.46 3.44 3.43 3.62 3.83 4.05 4.28 4.52 4.78 5.05 5.34 5.65 5.97
Low Coal Real 2020 $/MMBTU 1.41 1.38 1.42 1.35 1.31 1.29 1.28 1.29 1.29 1.29 1.29 1.29 1.30 1.30 1.32 1.33 1.34 1.34 1.36 1.37 1.37 1.38 1.39 1.40 1.41 1.42 1.41 1.42 1.43 1.51 1.59 1.68 1.78 1.88 1.99 2.10 2.22 2.35 2.48
Low NG Real 2020 $/MMBTU 3.30 3.00 2.73 2.58 2.56 2.64 2.79 2.89 2.97 3.05 3.07 3.13 3.13 3.13 3.14 3.15 3.17 3.17 3.20 3.20 3.18 3.19 3.13 3.11 3.10 3.09 3.11 3.09 3.09 3.26 3.45 3.64 3.85 4.07 4.30 4.55 4.81 5.08 5.37
Low Petroleum Real 2020 $/MMBTU 10.46 9.03 9.79 9.98 10.22 10.48 10.71 10.81 11.02 11.24 11.41 11.53 11.64 11.75 11.91 12.06 12.21 12.25 12.51 12.63 12.68 12.89 13.14 13.21 13.39 13.45 13.43 13.45 13.40 14.16 14.97 15.82 16.73 17.68 18.69 19.75 20.88 22.07 23.33
Low Nuclear Real 2020 $/MMBTU 0.62 0.62 0.62 0.62 0.62 0.62 0.62 0.63 0.63 0.63 0.63 0.63 0.63 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 0.41 1.03 1.09 1.15
Low Biomass Real 2020 $/MMBTU 4.50 4.60 4.70 4.80 4.91 5.02 5.13 5.24 5.36 5.47 5.59 5.72 5.84 5.97 6.10 6.24 6.37 6.51 6.66 6.80 6.95 7.11 7.26 7.42 7.59 7.75 7.92 8.10 8.28 8.75 9.25 9.77 10.33 10.92 11.54 12.20 12.90 13.63 14.41
Low OGS Real 2020 $/MMBTU 3.30 3.00 2.73 2.58 2.56 2.64 2.79 2.89 2.97 3.05 3.07 3.13 3.13 3.13 3.14 3.15 3.17 3.17 3.20 3.20 3.18 3.19 3.13 3.11 3.10 3.09 3.11 3.09 3.09 3.26 3.45 3.64 3.85 4.07 4.30 4.55 4.81 5.08 5.37
22 changes: 22 additions & 0 deletions examples/1App_ALAEF_LCGTEP/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# 1App_ALEAF_LCGTEP

## Purpose

This example provides a demonstration for using WATTS to run and analyze simulations with A-LEAF (Argonne Low-carbon Electricity Analysis Framework) under various supply chain disruption scenarios. The example explores different fuel price settings to understand the impact on US nuclear capacity expansion result.


## Code(s)

- A-LEAF
- Julia (A-LEAF dependency)
Comment thread
JiaZhou-PU marked this conversation as resolved.
Outdated

## Keywords

- Energy Market
- Capacity Expansion
- Economic Modeling

## File descriptions

- [__watts_exec.py__](watts_exec.py): WATTS workflow for this example. This is the file to execute to run the problem described above.
- [__A-LEAF_fuel_template__](Fuel.txt): Fuel price for this example
67 changes: 67 additions & 0 deletions examples/1App_ALAEF_LCGTEP/watts_exec.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# SPDX-FileCopyrightText: 2022-2025 UChicago Argonne, LLC
# SPDX-License-Identifier: MIT

"""
This example demonstrates how to use WATTS to run an A-LEAF calculation.
"""

import watts
from pathlib import Path
import pandas as pd

params = watts.Parameters()


fuel_price = {
2022: 0.62, 2023: 0.62, 2024: 0.62, 2025: 0.62, 2026: 0.62, 2027: 0.62,
2028: 0.63, 2029: 0.63, 2030: 0.63, 2031: 0.76, 2032: 0.76, 2033: 0.76,
2034: 0.76, 2035: 0.76, 2036: 0.77, 2037: 0.77, 2038: 0.77, 2039: 0.77,
2040: 0.77, 2041: 0.78, 2042: 0.78, 2043: 0.78, 2044: 0.78, 2045: 0.78,
2046: 0.79, 2047: 0.79, 2048: 0.79, 2049: 0.79, 2050: 0.79, 2051: 0.80,
2052: 0.80, 2053: 0.80, 2054: 0.80, 2055: 0.80, 2056: 0.81, 2057: 0.81,
2058: 0.81, 2059: 0.81, 2060: 0.81
}


# Set nuclear prices from reference and calculated growth
starting_year = 2031
starting_price = 0.76

# Compute prices dynamically for years after the starting year
growth_rates = {year: 1.0028 if year <= 2050 else 1.057 for year in range(starting_year + 1, 2061)}
computed_prices = {starting_year: starting_price}
for year in range(starting_year + 1, 2061):
prev_price = computed_prices[year - 1]
computed_prices[year] = round(prev_price * growth_rates[year], 3)

fuel_price.update(computed_prices)

# Initialize parameters and pass the fuel price dictionary
params = watts.Parameters()
params['fuel_price'] = fuel_price
# Display parameter summary
params.show_summary(show_metadata=True, sort_by='key')

# Set default path for results
results_path = Path.cwd() / 'results'
results_path.mkdir(exist_ok=True, parents=True)
watts.Database.set_default_path(results_path)

# Create ALEAF plugin
# aleaf_plugin = watts.PluginALEAF('Fuel.txt', extra_templates={'Simulation Configuration': 'Simulation Configuration.txt'})
aleaf_plugin = watts.PluginALEAF('Fuel.txt')
# Run ALEAF
aleaf_result = aleaf_plugin(params)
print('ALEAF simulation completed.')

# Get the technology summary
techsummary = aleaf_result.csv_data
# keep only the capacity columns
techsummary = techsummary[['Year', 'UnitGroup', 'Unit_Type', 'Fuel', 'ICAP', 'ICap_New', 'ICap_Ret']]

# group the technology summary by year and fuel
grouped_df = techsummary.groupby(['Year','Fuel']).sum()
# print out the grouped dataframe
print(grouped_df)


1 change: 1 addition & 0 deletions src/watts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# SPDX-License-Identifier: MIT

from .plugin import *
from .plugin_aleaf import *
from .plugin_accert import *
from .plugin_openmc import *
from .plugin_moose import *
Expand Down
158 changes: 158 additions & 0 deletions src/watts/plugin_aleaf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
from pathlib import Path
import shutil
import pandas as pd
from typing import List, Optional, Dict
import os

from .plugin import Plugin
from .results import Results, ExecInfo
from .fileutils import PathLike
from .parameters import Parameters
from .template import TemplateRenderer
import subprocess



class ResultsALEAF(Results):
"""ALEAF simulation results.

Parameters
----------
params : Parameters
Parameters used for the simulation.
exec_info : ExecInfo
Execution information.
inputs : List[PathLike]
List of input files used for the simulation.
outputs : List[PathLike]
List of output files generated by the simulation.

Attributes
----------
csv_data : pd.DataFrame
DataFrame containing the results from the ALEAF simulation.
"""


def __init__(self, params: Parameters, exec_info: ExecInfo,
inputs: List[PathLike], outputs: List[PathLike]):
super().__init__(params, exec_info, inputs, outputs)
self.csv_data = self._get_aleaf_csv_data()

def _get_aleaf_csv_data(self) -> pd.DataFrame:
"""Read ALEAF output CSV file and return results as a DataFrame."""
output_file = next((p for p in self.outputs if p.name.endswith('_system_tech_summary_EXP.csv')), None)
if output_file and output_file.exists():
return pd.read_csv(output_file)
else:
return pd.DataFrame() # Return an empty DataFrame if no CSV file is found


class PluginALEAF(Plugin):
"""Plugin for running ALEAF.

Parameters
----------
template_file : PathLike
Path to the template file for ALEAF.
extra_templates : Optional[Dict[str, PathLike]]
Additional templates to be used in the simulation.
show_stdout : bool
Whether to show standard output during execution.
show_stderr : bool
Whether to show standard error during execution.
"""

def __init__(self, template_file: PathLike, extra_templates: Optional[Dict[str, PathLike]] = None,
show_stdout: bool = False, show_stderr: bool = False):
super().__init__(extra_inputs=[], show_stdout=show_stdout, show_stderr=show_stderr)
self.template_file = template_file
self.extra_templates = extra_templates or {}
self.plugin_name = 'ALEAF'
self.renderer = TemplateRenderer(template_file)
self.aleaf_dir = os.getenv('ALEAF_DIR')
if not self.aleaf_dir:
raise EnvironmentError("ALEAF_DIR environment variable is not set.")
self.output_folder = None

def prerun(self, params: Parameters) -> None:
"""Generate ALEAF input files and check for pre-existing output.

Parameters
----------
params : Parameters
Parameters to be used for the simulation.
"""

# Copy the original ALEAF input file to the working directory
original_input_path = Path(self.aleaf_dir) / "setting/ALEAF_Master_LC_GTEP.xlsx"
modified_input_path = Path("ALEAF_Master_LC_GTEP.xlsx")
shutil.copy(original_input_path, modified_input_path)

# Load the Excel file and modify the 'Fuel' sheet
self.renderer(params = params, filename = self.template_file)
fuel_data = pd.read_csv("Fuel.txt", sep="\t")

with pd.ExcelWriter(modified_input_path, engine='openpyxl', mode='a', if_sheet_exists='replace') as writer:
fuel_sheet = pd.read_excel(writer, sheet_name='Fuel')
fuel_sheet.update(fuel_data)
fuel_sheet.to_excel(writer, sheet_name='Fuel', index=False)

# Render and apply additional templates if provided
for sheet_name, template_path in self.extra_templates.items():
template_renderer = TemplateRenderer(template_path)
sheet_data = template_renderer(params)
with pd.ExcelWriter(modified_input_path, engine='openpyxl', mode='a', if_sheet_exists='replace') as writer:
sheet_data.to_excel(writer, sheet_name=sheet_name, index=False)

# Copy the modified input file back to the original directory in ALEAF_DIR
shutil.copy(modified_input_path, original_input_path)

# Check the 'Simulation Configuration' sheet to find the correct case and CaseID remove the first row
config_sheet = pd.read_excel(modified_input_path, sheet_name='Simulation Configuration', header=1)
# Find the row with Run_Flag set to TRUE
case_row = config_sheet[config_sheet['Run_Flag'] == True].iloc[0]
case_id = case_row['Case_ID']
# Build the expected output directory and file path based on CaseID
output_folder = Path(self.aleaf_dir) / f"output/LC_GTEP/USA/case_id_{case_row.name+1}_{case_id}"
output_file = output_folder / f"{case_id}__system_tech_summary_EXP.csv"

# If the output file exists, back it up or delete it before running a new simulation
if output_file.exists():
backup_folder = output_folder / "backup"
backup_folder.mkdir(parents=True, exist_ok=True)
shutil.move(str(output_file), backup_folder / output_file.name)

# Save the output folder path for later use in postrun
self.output_folder = output_folder


def run(self):
"""Run ALEAF.

Parameters
----------
None
"""
# Ensure the ALEAF directory exists
command = ['julia', 'execute_ALEAF.jl']
subprocess.run(command, cwd=self.aleaf_dir)

def postrun(self, params: Parameters, exec_info: ExecInfo) -> ResultsALEAF:
"""Collect information from ALEAF simulation and create results object.
Parameters
----------
params : Parameters
Parameters used for the simulation.
exec_info : ExecInfo
exec_info : ExecInfo
Execution information.

Returns
-------
ResultsALEAF
Results object containing the simulation results.
"""
output_folder = Path(self.aleaf_dir) / f"output/LC_GTEP/USA/case_id_1_Test EXP"
outputs = [output_folder / "Test EXP__system_tech_summary_EXP.csv"]
return ResultsALEAF(params, exec_info, self.extra_inputs, outputs)