Skip to content

Commit ef02533

Browse files
authored
Merge pull request #12 from paulromano/unit-improvements
Several improvements for unit conversion
2 parents 745b1c8 + 2ed634e commit ef02533

File tree

14 files changed

+171
-156
lines changed

14 files changed

+171
-156
lines changed

doc/source/conf.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@
4848
'python': ('https://docs.python.org/3', None),
4949
'numpy': ('https://numpy.org/doc/stable/', None),
5050
'openmc': ('https://docs.openmc.org/en/stable/', None),
51-
'h5py': ('https://docs.h5py.org/en/stable', None)
51+
'h5py': ('https://docs.h5py.org/en/stable', None),
52+
'astropy': ('https://docs.astropy.org/en/stable/', None)
5253
}
5354

5455
# -- Options for HTML output -------------------------------------------------

doc/source/reference/index.rst

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ API Reference
1212
watts.Parameters
1313
watts.Plugin
1414
watts.PluginOpenMC
15-
watts.PluginSAM
15+
watts.PluginMOOSE
16+
watts.PluginPyARC
1617
watts.Results
1718
watts.ResultsOpenMC
18-
watts.ResultsSAM
19+
watts.ResultsMOOSE
20+
watts.ResultsPyARC

doc/source/user/usage.rst

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -48,31 +48,35 @@ workflows.
4848
Units
4949
~~~~~
5050

51-
To handle codes that use different unit systems, WATTS relies on `Astropy <https://www.astropy.org>`_ to perform unit conversion on parameters to ensure that the correct units are used for each code. For instance, MOOSE-based codes use the SI units while OpenMC uses the CGS units. With the built-in unit-conversion capability, a parameter needs only to be set once in any unit system and WATTS can automatically convert it to the correct unit for different codes. To use the unit-conversion capability, parameters need to be set to the ``astropy.units.quantity.Quantity`` class
52-
as follows::
53-
54-
from astropy import units as u
55-
56-
Quantity = u.Quantity
51+
To handle codes that use different unit systems, WATTS relies on the
52+
:class:`~astropy.units.Quantity` class from :mod:`astropy.units` to perform unit
53+
conversion on parameters to ensure that the correct units are used for each
54+
code. For instance, MOOSE-based codes use the SI units while OpenMC uses the CGS
55+
units. With the built-in unit-conversion capability, a parameter needs only to
56+
be set once in any unit system and WATTS can automatically convert it to the
57+
correct unit for different codes. To use the unit-conversion capability,
58+
parameters need to be set using the :class:`~astropy.units.Quantity` class as
59+
follows::
5760

58-
params['control_pin_rad'] = Quantity(9.9, "mm")
59-
params['He_inlet_temp'] = Quantity(600, "Celsius")
60-
params['He_cp'] = Quantity(4.9184126, "BTU/(kg*K)")
61+
from astropy.units import Quantity
6162

62-
with the format of ``Quantity(<value>, <current unit>)``. Imperial units can also be enabled as
63-
follows::
63+
params['radius'] = Quantity(9.9, "mm")
64+
params['inlet_temperature'] = Quantity(600, "Celsius")
65+
params['c_p'] = Quantity(4.9184126, "BTU/(kg*K)")
6466

65-
u.imperial.enable()
67+
with the format of ``Quantity(value, unit)``.
6668

6769
Plugins
6870
+++++++
6971

70-
Using a particular code within WATTS requires a "plugin" that controls input file
71-
generation, execution, and post-processing. Three plugin classes,
72-
:class:`~watts.PluginMOOSE`, :class:`~watts.PluginOpenMC`, and :class:`~watts.PluginPyARC`, have already been added to WATTS and are available for your use.
72+
Using a particular code within WATTS requires a "plugin" that controls input
73+
file generation, execution, and post-processing. Three plugin classes,
74+
:class:`~watts.PluginMOOSE`, :class:`~watts.PluginOpenMC`, and
75+
:class:`~watts.PluginPyARC`, have already been added to WATTS and are available
76+
for your use.
7377

7478
MOOSE Plugin
75-
~~~~~~~~~~
79+
~~~~~~~~~~~~
7680

7781
The :class:`~watts.PluginMOOSE` class enables MOOSE simulations using a
7882
templated input file. This is demonstrated here for a SAM application, but other
@@ -96,7 +100,8 @@ follows:
96100
Tsolid_sf = 1e-3
97101
[]
98102
99-
If the templated input file is ``sam_template.inp``, the SAM code will rely the general MOOSE plugin that can be created as::
103+
If the templated input file is ``sam_template.inp``, the SAM code will rely on
104+
the general MOOSE plugin that can be created as::
100105

101106
moose_plugin = watts.PluginMOOSE('sam_template.inp')
102107

@@ -137,7 +142,7 @@ OpenMC Plugin
137142
~~~~~~~~~~~~~
138143

139144
The :class:`~watts.PluginOpenMC` class handles OpenMC execution in a similar
140-
manner to the :class:`~watts.PluginSAM` class for SAM. However, for OpenMC,
145+
manner to the :class:`~watts.PluginMOOSE` class for MOOSE. However, for OpenMC,
141146
inputs are generated programmatically through the OpenMC Python API. Instead of
142147
writing a text template, for the OpenMC plugin you need to write a function that
143148
accepts an instance of :class:`~watts.Parameters` and generates the necessary
@@ -183,7 +188,7 @@ PyARC Plugin
183188
~~~~~~~~~~~~~
184189

185190
The :class:`~watts.PluginPyARC` class handles PyARC execution in a similar
186-
manner to the :class:`~watts.PluginSAM` class for SAM. PyARC use text-based
191+
manner to the :class:`~watts.PluginMOOSE` class for MOOSE. PyARC use text-based
187192
input files which can be templated as follows:
188193

189194
.. code-block:: jinja
@@ -194,7 +199,8 @@ input files which can be templated as follows:
194199
plane ( z10 ) { z = {{ assembly_length }} }
195200
}
196201
197-
If the templated input file is `pyarc_template`, then the PyARC plugin can be instantiated with following command line::
202+
If the templated input file is `pyarc_template`, then the PyARC plugin can be
203+
instantiated with following command line::
198204

199205
pyarc_plugin = watts.PluginPyARC('pyarc_template', show_stdout=True, extra_inputs=['lumped_test5.son'])
200206

@@ -248,9 +254,9 @@ k-effective value at the end of the simulation:
248254
>>> results.keff
249255
1.0026170700986219+/-0.003342785895893627
250256
251-
For SAM, the :class:`~watts.ResultsMOOSE` class
252-
provides a :attr:`~watts.ResultsMOOSE.csv_data` attribute that gathers the
253-
results from every CSV files generated by MOOSE applications (such as SAM or BISON)::
257+
For MOOSE, the :class:`~watts.ResultsMOOSE` class provides a
258+
:attr:`~watts.ResultsMOOSE.csv_data` attribute that gathers the results from
259+
every CSV files generated by MOOSE applications (such as SAM or BISON)::
254260

255261
moose_result = moose_plugin.workflow(params)
256262
for key in moose_result.csv_data:

examples/example1a_SAM/example1a.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,8 @@
44
from math import cos, pi
55
import os
66
import watts
7-
from astropy import units as u
7+
from astropy.units import Quantity
88

9-
# Uses Astropy for unit conversion
10-
u.imperial.enable() # Enable imperial units
11-
Quantity = u.Quantity
129

1310
params = watts.Parameters()
1411

@@ -30,7 +27,7 @@
3027
params['ax_ref'] = 20 # cm
3128
params['num_cool_pins'] = 1*6+2*6+6*2/2
3229
params['num_fuel_pins'] = 6+6+6+3*6+2*6/2+6/3
33-
params['Height_FC'] = Quantity(2000, "mm") # Automatically converts to 'm' for MOOSE and 'cm' for openmc
30+
params['Height_FC'] = Quantity(2000, "mm") # Automatically converts to 'm' for MOOSE and 'cm' for openmc
3431
params['Lattice_pitch'] = 2.0
3532
params['FuelPin_rad'] = 0.90 # cm
3633
params['cool_hole_rad'] = 0.60 # cm
@@ -56,4 +53,4 @@
5653
for key in moose_result.csv_data:
5754
print(key, moose_result.csv_data[key])
5855
print(moose_result.inputs)
59-
print(moose_result.outputs)
56+
print(moose_result.outputs)

examples/example1c_PyARC/example1c.py

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,15 @@
11
# SPDX-FileCopyrightText: 2022 UChicago Argonne, LLC
22
# SPDX-License-Identifier: MIT
33

4-
import os
54
import watts
6-
from astropy import units as u
7-
8-
# Uses Astropy for unit conversion
9-
u.imperial.enable() # Enable imperial units
10-
Quantity = u.Quantity
11-
params = watts.Parameters()
5+
from astropy.units import Quantity
126

137
# TH params
14-
8+
params = watts.Parameters()
159
params['assembly_pitch'] = Quantity(20, "cm") # 20e-2 m
1610
params['assembly_length'] = Quantity(13, "cm") # 0.13 m
1711
params['temp'] = Quantity(26.85, "Celsius") # 300 K
1812

19-
20-
2113
params.show_summary(show_metadata=False, sort_by='key')
2214

2315
# PyARC Workflow

examples/example2_SAM_OpenMC/example2.py

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,9 @@
66
import watts
77
from statistics import mean
88
from openmc_template import build_openmc_model
9-
from astropy import units as u
9+
from astropy.units import Quantity
1010

1111

12-
# Uses Astropy for unit conversion
13-
u.imperial.enable() # Enable imperial units
14-
Quantity = u.Quantity
15-
1612
params = watts.Parameters()
1713

1814
# TH params
@@ -91,4 +87,4 @@
9187
for i, power_frac in enumerate(power_fractions):
9288
params[f'Init_P_{i+1}'] = power_frac
9389

94-
params.show_summary(show_metadata=True, sort_by='time')
90+
params.show_summary(show_metadata=True, sort_by='time')

src/watts/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,6 @@
88
from .template import *
99
from .parameters import *
1010
from .database import *
11+
12+
# This allows a user to write watts.Quantity
13+
from astropy.units import Quantity

src/watts/parameters.py

Lines changed: 84 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,69 @@
11
# SPDX-FileCopyrightText: 2022 UChicago Argonne, LLC
22
# SPDX-License-Identifier: MIT
33

4-
import textwrap
4+
from __future__ import annotations
5+
import copy
56
from collections import namedtuple
67
from collections.abc import MutableMapping, Mapping, Iterable
78
from datetime import datetime
89
from getpass import getuser
10+
import textwrap
911
from typing import Any, Union
1012
from warnings import warn
1113

14+
import astropy.units as u
1215
import h5py
1316
from prettytable import PrettyTable
1417

1518

19+
# Enable imperial units
20+
u.imperial.enable()
21+
22+
# Normally saving parameters to HDF5 is as simple as calling:
23+
#
24+
# obj.create_dataset(key, data=value)
25+
#
26+
# However, in some cases we need to transform the value before writing or add
27+
# extra metadata in the dataset. To do this, we setup a mapping of Python types
28+
# to functions that create a dataset.
29+
30+
def _generate_save_func(dtype):
31+
def make_dataset(obj, key, value):
32+
dataset = obj.create_dataset(key, data=dtype(value))
33+
return dataset
34+
return make_dataset
35+
36+
_default_save_func = _generate_save_func(lambda x: x)
37+
38+
def _quantity_save_func(obj, key, value):
39+
dataset = _default_save_func(obj, key, value)
40+
dataset.attrs['unit'] = str(value.unit)
41+
return dataset
42+
1643
_SAVE_FUNCS = {
17-
'set': list
44+
'set': _generate_save_func(list),
45+
'Quantity': _quantity_save_func
1846
}
1947

48+
# In an HDF5 file, all iterable objects just appear as plain arrays (represented
49+
# by h5py as numpy arrays). To "round trip" data correctly, we again setup a
50+
# mapping of Python types to functions that load data out of a datset and
51+
# perform any transformation needed.
52+
53+
def _generate_load_func(dtype):
54+
return lambda obj, value: dtype(value)
55+
56+
def _quantity_load_func(obj, value):
57+
return u.Quantity(value, obj.attrs['unit'])
58+
2059
_LOAD_FUNCS = {
21-
'tuple': tuple,
22-
'list': list,
23-
'set': set,
24-
'float': float,
25-
'int': int,
26-
'bool': bool
60+
'tuple': _generate_load_func(tuple),
61+
'list': _generate_load_func(list),
62+
'set': _generate_load_func(set),
63+
'float': _generate_load_func(float),
64+
'int': _generate_load_func(int),
65+
'bool': _generate_load_func(bool),
66+
'Quantity': _quantity_load_func
2767
}
2868

2969
ParametersMetadata = namedtuple('ParametersMetadata', ['user', 'time'])
@@ -203,10 +243,8 @@ def add_metadata(obj, metadata):
203243
else:
204244
# Convert type if necessary. If the type is not listed, return a
205245
# "null" function that just returns the original value
206-
func = _SAVE_FUNCS.get(type(value).__name__, lambda x: x)
207-
file_value = func(value)
208-
209-
dset = h5_obj.create_dataset(key, data=file_value)
246+
func = _SAVE_FUNCS.get(type(value).__name__, _default_save_func)
247+
dset = func(h5_obj, key, value)
210248
dset.attrs['type'] = type(value).__name__
211249
if isinstance(mapping, type(self)):
212250
add_metadata(dset, self._metadata[key])
@@ -247,8 +285,8 @@ def metadata_from_obj(obj):
247285

248286
# Convert type if indicated. If the type is not listed, return a
249287
# "null" function that just returns the original value
250-
func = _LOAD_FUNCS.get(obj.attrs['type'], lambda x: x)
251-
mapping[key] = func(value)
288+
func = _LOAD_FUNCS.get(obj.attrs['type'], lambda obj, x: x)
289+
mapping[key] = func(obj, value)
252290

253291
if root:
254292
self._metadata[key] = metadata_from_obj(obj)
@@ -277,7 +315,7 @@ def load(self, filename_or_obj: Union[str, h5py.Group]):
277315
self._load_mapping(self, filename_or_obj)
278316

279317
@classmethod
280-
def from_hdf5(cls, filename_or_obj: Union[str, h5py.Group]):
318+
def from_hdf5(cls, filename_or_obj: Union[str, h5py.Group]) -> Parameters:
281319
"""Return parameters from HDF5 file/group
282320
283321
Parameters
@@ -287,4 +325,34 @@ def from_hdf5(cls, filename_or_obj: Union[str, h5py.Group]):
287325
"""
288326
params = cls()
289327
params.load(filename_or_obj)
290-
return params
328+
return params
329+
330+
def convert_units(self, system: str = 'si', temperature: str = 'K',
331+
inplace: bool = False) -> Parameters:
332+
"""Perform unit conversion
333+
334+
Parameters
335+
----------
336+
system
337+
Desired unit system: 'si' or 'cgs'
338+
temperature
339+
Desired unit for temperature conversions
340+
inplace
341+
Whether to modify the parameters (True) or return a copy (False)
342+
343+
Returns
344+
-------
345+
A :class:`Parameters` instance with converted units
346+
"""
347+
params = self if inplace else copy.deepcopy(self)
348+
349+
for key, value in params.items():
350+
if isinstance(value, u.Quantity):
351+
# Unit conversion for temperature needs to be done separately because
352+
# astropy uses a different method to convert temperature.
353+
if value.unit.physical_type == 'temperature':
354+
params[key] = value.to(temperature, equivalencies=u.temperature()).value
355+
else:
356+
params[key] = getattr(value, system).value
357+
358+
return params

0 commit comments

Comments
 (0)