Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GCMat plugin #114

Merged
merged 16 commits into from
Aug 27, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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.PluginGCMat
watts.PluginMCNP
watts.PluginMOOSE
watts.PluginOpenMC
Expand Down
40 changes: 40 additions & 0 deletions doc/source/user/plugins.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
22 changes: 22 additions & 0 deletions examples/1App_GCMat/README.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 6 additions & 0 deletions examples/1App_GCMat/gcmat_template.txt
Original file line number Diff line number Diff line change
@@ -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
108 changes: 108 additions & 0 deletions examples/1App_GCMat/watts_exec.py
Original file line number Diff line number Diff line change
@@ -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}
JiaZhou-PU marked this conversation as resolved.
Show resolved Hide resolved
# 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):
JiaZhou-PU marked this conversation as resolved.
Show resolved Hide resolved
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')
1 change: 1 addition & 0 deletions src/watts/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down
144 changes: 144 additions & 0 deletions src/watts/plugin_gcmat.py
Original file line number Diff line number Diff line change
@@ -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)