diff --git a/CHANGELOG.md b/CHANGELOG.md index 30b2d2f..72d18fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. * The `Plugin.__call__` method now accepts an `output_dir` argument that specifies the directory created in the database ([#107](https://github.com/watts-dev/watts/pull/107)) +* GCMAT plugin via the `PluginGCMAT` class ([114](https://github.com/watts-dev/watts/pull/114)) ### Changes diff --git a/doc/source/reference/index.rst b/doc/source/reference/index.rst index 9a70687..2c83cbf 100644 --- a/doc/source/reference/index.rst +++ b/doc/source/reference/index.rst @@ -14,6 +14,7 @@ API Reference watts.PluginGeneric watts.PluginABCE watts.PluginACCERT + watts.PluginGCMat watts.PluginMCNP watts.PluginMOOSE watts.PluginOpenMC diff --git a/doc/source/user/plugins.rst b/doc/source/user/plugins.rst index 1cb7805..2e2530a 100644 --- a/doc/source/user/plugins.rst +++ b/doc/source/user/plugins.rst @@ -472,3 +472,43 @@ As with other plugins, :class:`~watts.PluginACCERT` is used by:: accert_plugin = watts.PluginACCERT('accert_template') accert_result = accert_plugin(params) + +GCMat Plugin +++++++++++++ + +The :class:`~watts.PluginGCMat` class enables simulations with Argonne's global +critical materials agent-based model (GCMat). This code simulates dynamic +economic markets that are composed of agents who have complex decision-making +behaviors, and interact with and influence each other, possibly indirectly +through market signals. + +The GCMat plugin requires a template input file that can be templated as follows: + +.. code-block:: jinja + + region final demand agent final demand product reference product unit 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 2022 2023 2024 2025 2026 2027 2028 2029 2030 + final demand U U U tonnes 111847.841748839 112977.61792812 114118.805988 115271.5212 116435.88 117612 118800 120000 121200 122412 123636.12 124872.4812 126121.206012 127382.41807212 128656.242252841 {{final_demand_2025}} {{final_demand_2026}} {{final_demand_2027}} {{final_demand_2028}} {{final_demand_2029}} {{final_demand_2030}} + + China final demand U U shares of total 0.107142857142857 0.106698999696878 0.112674964564139 0.114434523188336 0.116194081812533 0.1278 0.1299 0.132 0.1341 0.1362 0.1383 0.1404 0.1425 0.1446 0.1467 {{china_2025}} {{china_2026}} {{china_2027}} {{china_2028}} {{china_2029}} {{china_2030}} + + US final demand U U shares of total 0.206589879692216 0.201409879668034 0.199574650237538 0.196450274218913 0.194620873740305 0.193447312012611 0.190358597294858 0.187635077997256 0.185744587021863 0.183688235605066 0.180393178767648 0.177208294866757 0.174389224194135 0.171923735369984 0.169723351626385 {{us_2025}} {{us_2026}} {{us_2027}} {{us_2028}} {{us_2029}} {{us_2030}} + + Europe final demand U U shares of total 0.16491345183516 0.160710857760063 0.154355276635327 0.149054613139685 0.145906896593099 0.143775880528747 0.141896860630669 0.140266791187998 0.138026220404039 0.135574967728323 0.132758006582095 0.130102830325263 0.127653579579804 0.125407763844851 0.123338987757727 {{eu_2025}} {{eu_2026}} {{eu_2027}} {{eu_2028}} {{eu_2029}} {{eu_2030}} + + ROW final demand U U shares of total 0.521353811330767 0.531180262874025 0.533394108562996 0.539080588452066 0.543278147354063 0.535073807414382 0.536243130077473 0.536734120790743 0.541204905572035 0.541712831061545 0.548846808065192 0.554835894215335 0.556026422605261 0.556024685007383 0.556929270113103 {{row_2025}} {{row_2026}} {{row_2027}} {{row_2028}} {{row_2029}} {{row_2030}} + + +The GCMat plugin can be instantiated with the following command line:: + + gcmat_plugin = watts.PluginGCMat('gcmat_template') + +Before running the GCMat plugin, the directory that contains the executable +'run_repast.sh' must be set. This can be done by setting the ``GCMAT_DIR`` +environment variable:: + + export GCMAT_DIR='/path/to/gcmat/output' + +As with other plugins, :class:`~watts.PluginGCMat` is used by:: + + gcmat_plugin = watts.PluginGCMat('gcmat_template') + gcmat_result = gcmat_plugin(params) diff --git a/examples/1App_GCMat/README.md b/examples/1App_GCMat/README.md new file mode 100644 index 0000000..eb37e88 --- /dev/null +++ b/examples/1App_GCMat/README.md @@ -0,0 +1,22 @@ +# 1App_GCMat + +## Purpose + +This example provides a demonstration for using WATTS to explore supply chain dynamics and uncertainty with GCMat under nuclear scenarios of Uranium fuel demand growth or shrinkage, supply disruptions. + +## Code(s) + +- GCMat +- Java (GCMat dependency) +- Repast Simphony agent-based modeling toolkit + +## Keywords + +- Rare Earths Supply Chain +- Agent Based Modeling +- Dynamic economic markets + +## 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. +- [__gcmat_template__](gcmat_template.txt): Templated GCMat model for the Uranium demand of nuclear scenarios. diff --git a/examples/1App_GCMat/gcmat_template.txt b/examples/1App_GCMat/gcmat_template.txt new file mode 100644 index 0000000..f750f0d --- /dev/null +++ b/examples/1App_GCMat/gcmat_template.txt @@ -0,0 +1,6 @@ +region final demand agent final demand product reference product unit 2010 2011 2012 2013 2014 2015 2016 2017 2018 2019 2020 2021 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 min max + final demand U U U tonnes 111847.841748839 112977.61792812 114118.805988 115271.5212 116435.88 117612 118800 120000 121200 122412 123636.12 124872.4812 126121.206012 127382.41807212 128656.242252841 {{final_demand_2025}} {{final_demand_2026}} {{final_demand_2027}} {{final_demand_2028}} {{final_demand_2029}} {{final_demand_2030}} {{final_demand_2031}} {{final_demand_2032}} {{final_demand_2033}} {{final_demand_2034}} {{final_demand_2035}} 144973.074053224 146422.804793756 147887.032841694 149365.903170111 150859.562201812 152368.15782383 153891.839402068 155430.757796089 156985.06537405 158554.91602779 160140.465188068 161741.869839949 163359.288538348 164992.881423732 166642.810237969 +China final demand U U shares of total 0.107142857142857 0.106698999696878 0.112674964564139 0.114434523188336 0.116194081812533 0.1278 0.1299 0.132 0.1341 0.1362 0.1383 0.1404 0.1425 0.1446 0.1467 {{china_2025}} {{china_2026}} {{china_2027}} {{china_2028}} {{china_2029}} {{china_2030}} {{china_2031}} {{china_2032}} {{china_2033}} {{china_2034}} {{china_2035}} 0.1278 0.1299 0.132 0.1341 0.1362 0.1383 0.1404 0.1425 0.1446 0.1467 0.1488 0.1509 0.153 0.1551 0.1572 +US final demand U U shares of total 0.206589879692216 0.201409879668034 0.199574650237538 0.196450274218913 0.194620873740305 0.193447312012611 0.190358597294858 0.187635077997256 0.185744587021863 0.183688235605066 0.180393178767648 0.177208294866757 0.174389224194135 0.171923735369984 0.169723351626385 {{us_2025}} {{us_2026}} {{us_2027}} {{us_2028}} {{us_2029}} {{us_2030}} {{us_2031}} {{us_2032}} {{us_2033}} {{us_2034}} {{us_2035}} 0.152496418425382 0.151658383543477 0.150908291406386 0.150241487394684 0.149652671086803 0.149135752743505 0.148685634386723 0.148295241364446 0.147957208168876 0.147664797716698 0.147411985046748 0.147191173081355 0.146995417723395 0.146818253525768 0.146654390040071 +Europe final demand U U shares of total 0.16491345183516 0.160710857760063 0.154355276635327 0.149054613139685 0.145906896593099 0.143775880528747 0.141896860630669 0.140266791187998 0.138026220404039 0.135574967728323 0.132758006582095 0.130102830325263 0.127653579579804 0.125407763844851 0.123338987757727 {{eu_2025}} {{eu_2026}} {{eu_2027}} {{eu_2028}} {{eu_2029}} {{eu_2030}} {{eu_2031}} {{eu_2032}} {{eu_2033}} {{eu_2034}} {{eu_2035}} 0.106430277535327 0.105529571878334 0.104692501292963 0.103915786801794 0.103196689222561 0.102532436392848 0.101925752229963 0.101373812203799 0.100874530060506 0.100425596750137 0.100024888835686 9.96627454580256E-02 9.93362033316521E-02 9.90427398919858E-02 9.87799471985866E-02 +ROW final demand U U shares of total 0.521353811329767 0.531180262875026 0.533395108562995 0.540060589453066 0.543278147854063 0.534976807458641 0.537844542074473 0.540098130814746 0.542129192574098 0.544536796666611 0.548548814650257 0.552288874807981 0.555457196226061 0.558068500785166 0.560237660615888 {{row_2025}} {{row_2026}} {{row_2027}} {{row_2028}} {{row_2029}} {{row_2030}} {{row_2031}} {{row_2032}} {{row_2033}} {{row_2034}} {{row_2035}} 0.613273304039291 0.612912044578189 0.612399207300651 0.611742725803522 0.610950639690636 0.610031810863646 0.608988613383314 0.607830946431755 0.606568261770617 0.605209605533165 0.603763126117566 0.602246081460619 0.600668378944953 0.599039006582246 0.597365662761342 diff --git a/examples/1App_GCMat/watts_exec.py b/examples/1App_GCMat/watts_exec.py new file mode 100644 index 0000000..e807278 --- /dev/null +++ b/examples/1App_GCMat/watts_exec.py @@ -0,0 +1,108 @@ +# SPDX-FileCopyrightText: 2022-2023 UChicago Argonne, LLC +# SPDX-License-Identifier: MIT + +""" +This example demonstrates how to use WATTS to run a series of GCMAT calculations for a nuclear scenario. + +In this example, a set of GCMAT simulations are performed, each with a different `end_year` parameter. +The `end_year` parameter specifies the final year of the simulation, allowing us to explore how +extending the simulation period impacts the output. Additionally, we specify different `output_folder` +names for each simulation to organize the results separately. + +By running multiple simulations with varying `end_year` values, this example demonstrates the sensitivity +of the GCMAT model to changes in the simulation period and the resulting effects on key output metrics. + +Note that the `end_year` parameter's unit is in week, and starting from the year is 2010, so the end_year +2028 is equivalent to YEAR 2049, and 2080 is equivalent to YEAR 2050. +""" + +import watts +from pathlib import Path +import numpy as np +import time + +params = watts.Parameters() +template_name = 'gcmat_template.txt' + +############################################################################### +# Example of Uranium demand from 2025 to 2035 the original values are from the GCMAT example +# The final demand is the sum of the demand from China, US, Europe, and the rest of the world +# unit in tonnes + +final_demand_org = {'2025': 129942.80467537, '2026': 131242.232722123, '2027': 132554.655049345, '2028': 133880.201599838, '2029': 135219.003615836, '2030': 136571.193651995, '2031': 137936.905588515, '2032': 139316.2746444, '2033': 140709.437390844, '2034': 142116.531764752, '2035': 143537.6970824} +# Below are the shares of the demand from China, US, Europe, and the rest of the world +china_shares = {'2025': 0.1488, '2026': 0.1509, '2027': 0.153, '2028': 0.1551, '2029': 0.1572, '2030': 0.1593, '2031': 0.1614, '2032': 0.1635, '2033': 0.1656, '2034': 0.1677, '2035': 0.1698} +us_shares = {'2025': 0.167705168506287, '2026': 0.16583640931287, '2027': 0.164092357127913, '2028': 0.162454545105463, '2029': 0.160909323261296, '2030': 0.159448692009461, '2031': 0.158069206226392, '2032': 0.156774330726817, '2033': 0.155566621066295, '2034': 0.154449705570859, '2035': 0.153426207060785} +europe_shares = {'2025': 0.121418171095092, '2026': 0.119622503016436, '2027': 0.117931399291808, '2028': 0.116333503569193, '2029': 0.11482130552861, '2030': 0.113390051603732, '2031': 0.112036657179276, '2032': 0.110761596604602, '2033': 0.109563494760443, '2034': 0.108442445566371, '2035': 0.107398402556524} +row_shares = {'2025': 0.562076660398621, '2026': 0.563641087670695, '2027': 0.56497624358028, '2028': 0.566111951325344, '2029': 0.567069371210094, '2030': 0.567861256386808, '2031': 0.622751279451474, '2032': 0.625765072971703, '2033': 0.622194919609123, '2034': 0.622673325674434, '2035': 0.622981308570158} + +############################################################################### +# Example of the new US Uranium demand from 2025 to 2035, these values can be calculated from DYMOND or other sources + +us_new_demands = {'2025': 293500, '2026': 292100, '2027': 312300, '2028': 313400, '2029': 377000, '2030': 399100, '2031': 314900, '2032': 361100, '2033': 340200, '2034': 337200, '2035': 336800} + +# As we are changing the US demand, we need to recalculate the shares for all the regions +for i in range(2025, 2036): + china_demand = final_demand_org[str(i)] * china_shares[str(i)] + europe_demand = final_demand_org[str(i)] * europe_shares[str(i)] + row_demand = final_demand_org[str(i)] * row_shares[str(i)] + # Original US demand, the demand is calculated based on the shares + # Not used in the calculation, here for reference + us_demand = final_demand_org[str(i)] * us_shares[str(i)] + # New US demand + us_new_demand = us_new_demands[str(i)] + # New final demand + new_final_demand = china_demand + europe_demand + row_demand + us_new_demand + params[f'final_demand_{i}'] = new_final_demand + params[f'china_{i}'] = china_demand/new_final_demand + params[f'us_{i}'] = us_new_demand/new_final_demand + params[f'eu_{i}'] = europe_demand/new_final_demand + params[f'row_{i}'] = row_demand/new_final_demand + +# Create a directory for storing results +results_path = Path.cwd() / 'results' +results_path.mkdir(exist_ok=True, parents=True) + +# Set the default path for the database +watts.Database.set_default_path(results_path) +print('results_path',results_path) +# Define simulation parameters for multiple runs +# The parameter `end_year` is specified in weeks since the start of the simulation in 2010. +# For example: +# - 1040 weeks corresponds to the year 2030 +# - 1274 weeks corresponds to the year 2040 +# - 2080 weeks corresponds to the year 2050 + +output_years = [2030, 2040, 2050] # Target years for the end of each simulation +# Convert each target year to the corresponding number of weeks since 2010 +end_years = [int((year - 2010) * 52) for year in output_years] +# Generate output folder names based on target years +output_folders = [f"output_{year}" for year in output_years] + +# Start timing the simulation for performance measurement +start = time.perf_counter() + +# Loop through the defined variations in end_years and output_folders to run simulations +for output_year, end_year, output_folder in zip(output_years, end_years, output_folders): + # Update parameters for the current simulation run + params['end_year'] = end_year + params['output_folder'] = output_folder + params['DATABASE_NAME'] = f'GCMAT_{end_year}.db' + + # Display the current parameter settings for transparency and debugging + params.show_summary(show_metadata=True, sort_by='key') + + # Create the GCMAT plugin instance with the specified template file + gcmat_plugin = watts.PluginGCMAT('gcmat_template.txt', show_stdout=True, show_stderr=True) + + # Run the GCMAT simulation with the current set of parameters + gcmat_result = gcmat_plugin(params, end_year=params['end_year'], output_folder=params['output_folder']) + + # Print the U buyer price in the US for the specified output year from the end of the simulation results + print(f'Output year {output_year} price: {gcmat_result.csv_data["U buyer price US"].iloc[-1]}') + +# End timing the simulation +end = time.perf_counter() + +# Output the total simulation time for all runs +print(f'TOTAL SIMULATION TIME: {np.round((end - start) / 60, 2)} minutes') \ No newline at end of file diff --git a/src/watts/__init__.py b/src/watts/__init__.py index f63f272..83c0ada 100644 --- a/src/watts/__init__.py +++ b/src/watts/__init__.py @@ -12,6 +12,7 @@ from .plugin_serpent import * from .plugin_abce import * from .plugin_dakota import * +from .plugin_gcmat import * from .results import * from .template import * from .parameters import * diff --git a/src/watts/plugin_gcmat.py b/src/watts/plugin_gcmat.py new file mode 100644 index 0000000..6656cbc --- /dev/null +++ b/src/watts/plugin_gcmat.py @@ -0,0 +1,144 @@ +# SPDX-FileCopyrightText: 2024 UChicago Argonne, LLC +# SPDX-License-Identifier: MIT + +from pathlib import Path +import shutil +import subprocess +from typing import List, Optional +import os +import pandas as pd + +from .plugin import Plugin +from .results import Results, ExecInfo +from .fileutils import PathLike +from .parameters import Parameters +from .template import TemplateRenderer + + +class ResultsGCMAT(Results): + """GCMAT simulation results + + Parameters + ---------- + params + Parameters used to generate inputs + exec_info + Execution information (job ID, plugin name, time, etc.) + inputs + List of input files + outputs + List of output files + + Attributes + ---------- + stdout + Standard output from GCMAT run + csv_data + Data from the output CSV file + """ + 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_gcmat_csv_data() + + def _get_gcmat_csv_data(self) -> pd.DataFrame: + """Read GCMAT output CSV file and return results as a DataFrame""" + output_file = next((p for p in self.outputs if p.name == 'GUIOutputs.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 PluginGCMAT(Plugin): + """Plugin for running GCMAT + + Parameters + ---------- + template_file + Template file used to generate the input files + extra_inputs + Extra (non-templated) input files + show_stdout + Whether to display output from stdout when GCMAT is run + show_stderr + Whether to display output from stderr when GCMAT is run + + """ + def __init__(self, template_file: PathLike, + extra_inputs: Optional[List[PathLike]] = None, + show_stdout: bool = False, show_stderr: bool = False): + super().__init__(extra_inputs, show_stdout, show_stderr) + self.template_file = template_file + self.plugin_name = 'GCMAT' + self.renderer = TemplateRenderer(template_file) + self.gcmat_dir = os.getenv('GCMAT_DIR') + if not self.gcmat_dir: + raise EnvironmentError("GCMAT_DIR environment variable is not set.") + + # Include './run_repast.sh' as the executable and all files in the 'data' folder as default extra inputs + self.executable = Path(self.gcmat_dir) / "run_repast.sh" + self.default_extra_inputs = list((Path(self.gcmat_dir) / "complete_model" / "data").glob("**/*")) + + # Initialize output_folder attribute + self.output_folder = None + + def prerun(self, params: Parameters) -> None: + """Generate GCMAT input files + + Parameters + ---------- + params + Parameters used by the GCMAT template + """ + # Render the template to create the input file + input_file = Path("gc_input.txt") + self.renderer(params, filename=input_file) + + # Copy the input file to the required directory + model_directory = Path(self.gcmat_dir) / "complete_model" + target_directory = model_directory / "data/scenariosNuclear/default_UserInputs" + target_directory.mkdir(parents=True, exist_ok=True) + shutil.copy(input_file, target_directory / "demandScenarioV2.txt") + + def run(self, end_year: int = 2080, output_folder: str = "testout", **kwargs): + """Run GCMAT + + Parameters + ---------- + end_year + The year to end the simulation + output_folder + The folder where outputs will be stored + kwargs + Additional keyword arguments to pass to the subprocess + """ + # use the absolute path for the output folder + self.output_folder = os.path.join(self.gcmat_dir, output_folder) + param_string = f'1\tendAt\t{end_year}' + command = [str(self.executable), param_string, subprocess.check_output('realpath .', shell=True).strip().decode('utf-8'), output_folder] + # Run the GCMAT simulation + subprocess.run(command, cwd=self.gcmat_dir, **kwargs) + + def postrun(self, params: Parameters, exec_info: ExecInfo) -> ResultsGCMAT: + """Collect information from GCMAT simulation and create results object + + Parameters + ---------- + params + Parameters used to create GCMAT model + exec_info + Execution information + + Returns + ------- + GCMAT results object + """ + output_folder = Path(self.output_folder) # Retrieve the stored + # Only collect the GUIOutputs.csv file + # can add more files if needed + outputs = [] + gui_outputs_file = output_folder / "GUIOutputs.csv" + if gui_outputs_file.exists(): + outputs.append(gui_outputs_file) + return ResultsGCMAT(params, exec_info, self.extra_inputs, outputs)