Skip to content

Commit 1100b78

Browse files
authored
Merge pull request #114 from JiaZhou-PU/gcmat
GCMat plugin - thanks @JiaZhou-PU
2 parents 333d225 + bb07a62 commit 1100b78

File tree

8 files changed

+323
-0
lines changed

8 files changed

+323
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
1212
* The `Plugin.__call__` method now accepts an `output_dir` argument that
1313
specifies the directory created in the database
1414
([#107](https://github.com/watts-dev/watts/pull/107))
15+
* GCMAT plugin via the `PluginGCMAT` class ([114](https://github.com/watts-dev/watts/pull/114))
1516

1617
### Changes
1718

doc/source/reference/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ API Reference
1414
watts.PluginGeneric
1515
watts.PluginABCE
1616
watts.PluginACCERT
17+
watts.PluginGCMat
1718
watts.PluginMCNP
1819
watts.PluginMOOSE
1920
watts.PluginOpenMC

doc/source/user/plugins.rst

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -472,3 +472,43 @@ As with other plugins, :class:`~watts.PluginACCERT` is used by::
472472

473473
accert_plugin = watts.PluginACCERT('accert_template')
474474
accert_result = accert_plugin(params)
475+
476+
GCMat Plugin
477+
++++++++++++
478+
479+
The :class:`~watts.PluginGCMat` class enables simulations with Argonne's global
480+
critical materials agent-based model (GCMat). This code simulates dynamic
481+
economic markets that are composed of agents who have complex decision-making
482+
behaviors, and interact with and influence each other, possibly indirectly
483+
through market signals.
484+
485+
The GCMat plugin requires a template input file that can be templated as follows:
486+
487+
.. code-block:: jinja
488+
489+
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
490+
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}}
491+
492+
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}}
493+
494+
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}}
495+
496+
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}}
497+
498+
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}}
499+
500+
501+
The GCMat plugin can be instantiated with the following command line::
502+
503+
gcmat_plugin = watts.PluginGCMat('gcmat_template')
504+
505+
Before running the GCMat plugin, the directory that contains the executable
506+
'run_repast.sh' must be set. This can be done by setting the ``GCMAT_DIR``
507+
environment variable::
508+
509+
export GCMAT_DIR='/path/to/gcmat/output'
510+
511+
As with other plugins, :class:`~watts.PluginGCMat` is used by::
512+
513+
gcmat_plugin = watts.PluginGCMat('gcmat_template')
514+
gcmat_result = gcmat_plugin(params)

examples/1App_GCMat/README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# 1App_GCMat
2+
3+
## Purpose
4+
5+
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.
6+
7+
## Code(s)
8+
9+
- GCMat
10+
- Java (GCMat dependency)
11+
- Repast Simphony agent-based modeling toolkit
12+
13+
## Keywords
14+
15+
- Rare Earths Supply Chain
16+
- Agent Based Modeling
17+
- Dynamic economic markets
18+
19+
## File descriptions
20+
21+
- [__watts_exec.py__](watts_exec.py): WATTS workflow for this example. This is the file to execute to run the problem described above.
22+
- [__gcmat_template__](gcmat_template.txt): Templated GCMat model for the Uranium demand of nuclear scenarios.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
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
2+
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
3+
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
4+
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
5+
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
6+
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

examples/1App_GCMat/watts_exec.py

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
# SPDX-FileCopyrightText: 2022-2023 UChicago Argonne, LLC
2+
# SPDX-License-Identifier: MIT
3+
4+
"""
5+
This example demonstrates how to use WATTS to run a series of GCMAT calculations for a nuclear scenario.
6+
7+
In this example, a set of GCMAT simulations are performed, each with a different `end_year` parameter.
8+
The `end_year` parameter specifies the final year of the simulation, allowing us to explore how
9+
extending the simulation period impacts the output. Additionally, we specify different `output_folder`
10+
names for each simulation to organize the results separately.
11+
12+
By running multiple simulations with varying `end_year` values, this example demonstrates the sensitivity
13+
of the GCMAT model to changes in the simulation period and the resulting effects on key output metrics.
14+
15+
Note that the `end_year` parameter's unit is in week, and starting from the year is 2010, so the end_year
16+
2028 is equivalent to YEAR 2049, and 2080 is equivalent to YEAR 2050.
17+
"""
18+
19+
import watts
20+
from pathlib import Path
21+
import numpy as np
22+
import time
23+
24+
params = watts.Parameters()
25+
template_name = 'gcmat_template.txt'
26+
27+
###############################################################################
28+
# Example of Uranium demand from 2025 to 2035 the original values are from the GCMAT example
29+
# The final demand is the sum of the demand from China, US, Europe, and the rest of the world
30+
# unit in tonnes
31+
32+
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}
33+
# Below are the shares of the demand from China, US, Europe, and the rest of the world
34+
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}
35+
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}
36+
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}
37+
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}
38+
39+
###############################################################################
40+
# Example of the new US Uranium demand from 2025 to 2035, these values can be calculated from DYMOND or other sources
41+
42+
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}
43+
44+
# As we are changing the US demand, we need to recalculate the shares for all the regions
45+
for i in range(2025, 2036):
46+
china_demand = final_demand_org[str(i)] * china_shares[str(i)]
47+
europe_demand = final_demand_org[str(i)] * europe_shares[str(i)]
48+
row_demand = final_demand_org[str(i)] * row_shares[str(i)]
49+
# Original US demand, the demand is calculated based on the shares
50+
# Not used in the calculation, here for reference
51+
us_demand = final_demand_org[str(i)] * us_shares[str(i)]
52+
# New US demand
53+
us_new_demand = us_new_demands[str(i)]
54+
# New final demand
55+
new_final_demand = china_demand + europe_demand + row_demand + us_new_demand
56+
params[f'final_demand_{i}'] = new_final_demand
57+
params[f'china_{i}'] = china_demand/new_final_demand
58+
params[f'us_{i}'] = us_new_demand/new_final_demand
59+
params[f'eu_{i}'] = europe_demand/new_final_demand
60+
params[f'row_{i}'] = row_demand/new_final_demand
61+
62+
# Create a directory for storing results
63+
results_path = Path.cwd() / 'results'
64+
results_path.mkdir(exist_ok=True, parents=True)
65+
66+
# Set the default path for the database
67+
watts.Database.set_default_path(results_path)
68+
print('results_path',results_path)
69+
# Define simulation parameters for multiple runs
70+
# The parameter `end_year` is specified in weeks since the start of the simulation in 2010.
71+
# For example:
72+
# - 1040 weeks corresponds to the year 2030
73+
# - 1274 weeks corresponds to the year 2040
74+
# - 2080 weeks corresponds to the year 2050
75+
76+
output_years = [2030, 2040, 2050] # Target years for the end of each simulation
77+
# Convert each target year to the corresponding number of weeks since 2010
78+
end_years = [int((year - 2010) * 52) for year in output_years]
79+
# Generate output folder names based on target years
80+
output_folders = [f"output_{year}" for year in output_years]
81+
82+
# Start timing the simulation for performance measurement
83+
start = time.perf_counter()
84+
85+
# Loop through the defined variations in end_years and output_folders to run simulations
86+
for output_year, end_year, output_folder in zip(output_years, end_years, output_folders):
87+
# Update parameters for the current simulation run
88+
params['end_year'] = end_year
89+
params['output_folder'] = output_folder
90+
params['DATABASE_NAME'] = f'GCMAT_{end_year}.db'
91+
92+
# Display the current parameter settings for transparency and debugging
93+
params.show_summary(show_metadata=True, sort_by='key')
94+
95+
# Create the GCMAT plugin instance with the specified template file
96+
gcmat_plugin = watts.PluginGCMAT('gcmat_template.txt', show_stdout=True, show_stderr=True)
97+
98+
# Run the GCMAT simulation with the current set of parameters
99+
gcmat_result = gcmat_plugin(params, end_year=params['end_year'], output_folder=params['output_folder'])
100+
101+
# Print the U buyer price in the US for the specified output year from the end of the simulation results
102+
print(f'Output year {output_year} price: {gcmat_result.csv_data["U buyer price US"].iloc[-1]}')
103+
104+
# End timing the simulation
105+
end = time.perf_counter()
106+
107+
# Output the total simulation time for all runs
108+
print(f'TOTAL SIMULATION TIME: {np.round((end - start) / 60, 2)} minutes')

src/watts/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from .plugin_serpent import *
1313
from .plugin_abce import *
1414
from .plugin_dakota import *
15+
from .plugin_gcmat import *
1516
from .results import *
1617
from .template import *
1718
from .parameters import *

src/watts/plugin_gcmat.py

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# SPDX-FileCopyrightText: 2024 UChicago Argonne, LLC
2+
# SPDX-License-Identifier: MIT
3+
4+
from pathlib import Path
5+
import shutil
6+
import subprocess
7+
from typing import List, Optional
8+
import os
9+
import pandas as pd
10+
11+
from .plugin import Plugin
12+
from .results import Results, ExecInfo
13+
from .fileutils import PathLike
14+
from .parameters import Parameters
15+
from .template import TemplateRenderer
16+
17+
18+
class ResultsGCMAT(Results):
19+
"""GCMAT simulation results
20+
21+
Parameters
22+
----------
23+
params
24+
Parameters used to generate inputs
25+
exec_info
26+
Execution information (job ID, plugin name, time, etc.)
27+
inputs
28+
List of input files
29+
outputs
30+
List of output files
31+
32+
Attributes
33+
----------
34+
stdout
35+
Standard output from GCMAT run
36+
csv_data
37+
Data from the output CSV file
38+
"""
39+
def __init__(self, params: Parameters, exec_info: ExecInfo,
40+
inputs: List[PathLike], outputs: List[PathLike]):
41+
super().__init__(params, exec_info, inputs, outputs)
42+
self.csv_data = self._get_gcmat_csv_data()
43+
44+
def _get_gcmat_csv_data(self) -> pd.DataFrame:
45+
"""Read GCMAT output CSV file and return results as a DataFrame"""
46+
output_file = next((p for p in self.outputs if p.name == 'GUIOutputs.csv'), None)
47+
if output_file and output_file.exists():
48+
return pd.read_csv(output_file)
49+
else:
50+
return pd.DataFrame() # Return an empty DataFrame if no CSV file is found
51+
52+
53+
class PluginGCMAT(Plugin):
54+
"""Plugin for running GCMAT
55+
56+
Parameters
57+
----------
58+
template_file
59+
Template file used to generate the input files
60+
extra_inputs
61+
Extra (non-templated) input files
62+
show_stdout
63+
Whether to display output from stdout when GCMAT is run
64+
show_stderr
65+
Whether to display output from stderr when GCMAT is run
66+
67+
"""
68+
def __init__(self, template_file: PathLike,
69+
extra_inputs: Optional[List[PathLike]] = None,
70+
show_stdout: bool = False, show_stderr: bool = False):
71+
super().__init__(extra_inputs, show_stdout, show_stderr)
72+
self.template_file = template_file
73+
self.plugin_name = 'GCMAT'
74+
self.renderer = TemplateRenderer(template_file)
75+
self.gcmat_dir = os.getenv('GCMAT_DIR')
76+
if not self.gcmat_dir:
77+
raise EnvironmentError("GCMAT_DIR environment variable is not set.")
78+
79+
# Include './run_repast.sh' as the executable and all files in the 'data' folder as default extra inputs
80+
self.executable = Path(self.gcmat_dir) / "run_repast.sh"
81+
self.default_extra_inputs = list((Path(self.gcmat_dir) / "complete_model" / "data").glob("**/*"))
82+
83+
# Initialize output_folder attribute
84+
self.output_folder = None
85+
86+
def prerun(self, params: Parameters) -> None:
87+
"""Generate GCMAT input files
88+
89+
Parameters
90+
----------
91+
params
92+
Parameters used by the GCMAT template
93+
"""
94+
# Render the template to create the input file
95+
input_file = Path("gc_input.txt")
96+
self.renderer(params, filename=input_file)
97+
98+
# Copy the input file to the required directory
99+
model_directory = Path(self.gcmat_dir) / "complete_model"
100+
target_directory = model_directory / "data/scenariosNuclear/default_UserInputs"
101+
target_directory.mkdir(parents=True, exist_ok=True)
102+
shutil.copy(input_file, target_directory / "demandScenarioV2.txt")
103+
104+
def run(self, end_year: int = 2080, output_folder: str = "testout", **kwargs):
105+
"""Run GCMAT
106+
107+
Parameters
108+
----------
109+
end_year
110+
The year to end the simulation
111+
output_folder
112+
The folder where outputs will be stored
113+
kwargs
114+
Additional keyword arguments to pass to the subprocess
115+
"""
116+
# use the absolute path for the output folder
117+
self.output_folder = os.path.join(self.gcmat_dir, output_folder)
118+
param_string = f'1\tendAt\t{end_year}'
119+
command = [str(self.executable), param_string, subprocess.check_output('realpath .', shell=True).strip().decode('utf-8'), output_folder]
120+
# Run the GCMAT simulation
121+
subprocess.run(command, cwd=self.gcmat_dir, **kwargs)
122+
123+
def postrun(self, params: Parameters, exec_info: ExecInfo) -> ResultsGCMAT:
124+
"""Collect information from GCMAT simulation and create results object
125+
126+
Parameters
127+
----------
128+
params
129+
Parameters used to create GCMAT model
130+
exec_info
131+
Execution information
132+
133+
Returns
134+
-------
135+
GCMAT results object
136+
"""
137+
output_folder = Path(self.output_folder) # Retrieve the stored
138+
# Only collect the GUIOutputs.csv file
139+
# can add more files if needed
140+
outputs = []
141+
gui_outputs_file = output_folder / "GUIOutputs.csv"
142+
if gui_outputs_file.exists():
143+
outputs.append(gui_outputs_file)
144+
return ResultsGCMAT(params, exec_info, self.extra_inputs, outputs)

0 commit comments

Comments
 (0)