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

Issue929 #1

Draft
wants to merge 61 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
57eea20
Revert "switch to nl_v1 (for now)"
bknueven Feb 17, 2023
c02dec2
doing some debugging
bknueven Mar 30, 2023
e321ead
Merge branch 'main' into issue929
bknueven May 17, 2023
6a209fd
Modify and rescaled following vars and constraints for chemistry test…
May 26, 2023
9c41335
Update tests for chem_scaling_utils modifications
Jun 5, 2023
09ac4c8
Enthalpy unit corrected for EDB data
Jun 7, 2023
c31cd09
chem_scaling_utils updated for corrected enthalpy
Jun 7, 2023
d16fc70
Temperature bound modified to avoid breaking out the solvent critical…
Jun 7, 2023
21b5c69
Correct enthalpy units and update assertion in all tests, all pass
Jun 7, 2023
15b4f09
Revert "switch to nl_v1 (for now)"
bknueven Feb 17, 2023
4ca862c
doing some debugging
bknueven Mar 30, 2023
4ed5c2e
Modify and rescaled following vars and constraints for chemistry test…
May 26, 2023
216f7f4
Update tests for chem_scaling_utils modifications
Jun 5, 2023
c6011bc
Enthalpy unit corrected for EDB data
Jun 7, 2023
ae9a156
chem_scaling_utils updated for corrected enthalpy
Jun 7, 2023
f358836
Temperature bound modified to avoid breaking out the solvent critical…
Jun 7, 2023
a54427c
Correct enthalpy units and update assertion in all tests, all pass
Jun 7, 2023
4be6732
Chemistry tests updated after adding mutable parameters to reformulat…
Jun 15, 2023
f1035c4
Correct TNK to use components updated in anaerobic environment, break…
Jun 26, 2023
7cd9b05
Update documentation for asm1_adm1 translator
Jun 26, 2023
b207563
Typo/equations corrected for asm1_adm1 translator documentation
Jun 27, 2023
bc2e2a7
Merge remote-tracking branch 'upstream/main' into issue929
bknueven Jun 28, 2023
0aab640
Merge remote-tracking branch 'upstream/main' into lxhowl_issue929
bknueven Jun 28, 2023
1f64faa
Merge branch 'lxhowl_issue929' into issue929
bknueven Jun 28, 2023
564104e
black
bknueven Jun 28, 2023
c84efd3
remove errant merge conflict
bknueven Jun 28, 2023
d83dc38
pylint
bknueven Jun 28, 2023
4907892
Merge branch 'main' into issue929
bknueven Aug 1, 2023
72d4f16
using idaes-pse with Xinhong's fixes
bknueven Aug 2, 2023
7b74ae0
pylint
Aug 2, 2023
90dd26c
Fix NaOCl flowsheet
Aug 2, 2023
494faa9
Remode model.clone in gac test
Aug 2, 2023
c5af80b
Update scaling in ion exchange test
Aug 2, 2023
debc6cd
Merge pull request #3 from lxhowl/issue929
bknueven Aug 2, 2023
37718f5
Update IDAES requirement to 2.2.0.dev0.watertap.23.08.03 tag
lbianchi-lbl Aug 3, 2023
9160ef1
Merge remote-tracking branch 'lbianchi-lbl/update-idaes-2.2.0.dev0' i…
bknueven Aug 3, 2023
bd39f51
Test NaOCl flowsheet on linux
Aug 3, 2023
cf5ec25
Relax the bounds of dP_dx (#1094)
luohezhiming Aug 3, 2023
a5fe549
Test 2 NaOCl flowsheet on linux
Aug 3, 2023
868abb4
NF w/ bypass flosheet test
Aug 3, 2023
b8d1d89
Add scaling option
Aug 3, 2023
1a74626
Merge pull request #4 from lxhowl/issue929
bknueven Aug 3, 2023
a5be500
run black
Aug 3, 2023
25617a4
Merge pull request #5 from lxhowl/issue929
bknueven Aug 3, 2023
06db0a9
Fix pylint
Aug 3, 2023
83f4a1e
Fix docs test
Aug 3, 2023
6b6cf66
Update monte carlo flowsheet and documentation to use new parameter s…
shelman Aug 3, 2023
259ea4a
Remove skip for nf flowsheet notebook
Aug 3, 2023
55c846e
Merge pull request #6 from lxhowl/issue929
bknueven Aug 3, 2023
427f56f
Merge branch 'main' into issue929
bknueven Aug 3, 2023
e59323c
Skip macOS failed tests
Aug 4, 2023
cfd3805
Import pytest to skip ui tests for macOS
Aug 4, 2023
0305d01
test ui tests
Aug 5, 2023
28c7d88
run black
Aug 5, 2023
82c37b6
Update parameter_sweep.py (#1097)
avdudchenko Aug 6, 2023
7dcff56
Fix a warning in ED costing (#1096)
lbibl Aug 7, 2023
e2ae67e
Allow passing build_outputs=None to parameter sweep (#1099)
shelman Aug 8, 2023
1b8b04b
Merge pull request #7 from lxhowl/issue929
bknueven Aug 9, 2023
ddde7f7
cleanup nf_with_bypass_ui
bknueven Aug 9, 2023
f86506d
skip nf_with_bypass_ui test on macOS
bknueven Aug 9, 2023
3d90146
Merge remote-tracking branch 'upstream/main' into issue929
bknueven Aug 9, 2023
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
6 changes: 1 addition & 5 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -258,10 +258,6 @@ jobs:
echo '::group::Output of "idaes get-extensions" command'
idaes get-extensions --verbose
echo '::endgroup::'
- name: Exclude notebooks that cause errors on Windows
if: startswith(matrix.os, 'win')
run: |
rm tutorials/nawi_spring_meeting2023.ipynb
- name: Run pytest with nbmake
run:
pytest --nbmake **/*.ipynb
Expand Down Expand Up @@ -304,4 +300,4 @@ jobs:
pyomo build-extensions || python -c "from pyomo.contrib.pynumero.asl import AmplInterface; exit(0) if AmplInterface.available() else exit(1)"
- name: Run pytest
run: |
pytest --pyargs watertap -k 'not nf_dspmde.nf_ui'
pytest --pyargs watertap -k 'not (nf_dspmde.nf_ui or nf_dspmde.nf_with_bypass_ui)'
6 changes: 6 additions & 0 deletions docs/how_to_guides/how_to_use_a_property_model.rst
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ A portion of the displayed output is shown below.

.. testoutput::

WARNING: model contains export suffix 'fs.state_block[0].scaling_factor' that
contains 4 component keys that are not exported as part of the NL file.
Skipping.
WARNING: model contains export suffix 'fs.state_block[0].scaling_factor' that
contains 4 component keys that are not exported as part of the NL file.
Skipping.
Block fs.state_block[0]

Variables:
Expand Down
57 changes: 32 additions & 25 deletions docs/how_to_guides/how_to_use_parameter_sweep.rst
Original file line number Diff line number Diff line change
Expand Up @@ -45,37 +45,43 @@ Once this is done, import the parameter sweep tool

Conceptually, regardless of the number of iterations necessary to test each possible combination of variables, it is only necessary to build, simulate, and set up the model once.
Thus, these steps are left to the user and handled outside the parameter sweep function.
Depending on how the functions you've defined work, this could be as straightforward as

In order to support native parallelism within the parameter sweep class, the preferred way of setting up a model is to create a standalone function
that can produce it. Depending on how the functions you've defined work, this could be as straightforward as

.. testcode::

# replace these function calls with
# those in your own flowsheet module
def build_model(**kwargs):

# set up system
m = RO_flowsheet.build()
RO_flowsheet.set_operating_conditions(m)
RO_flowsheet.initialize_system(m)
# replace these function calls with
# those in your own flowsheet module

# simulate
RO_flowsheet.solve(m)
# set up system
m = RO_flowsheet.build()
RO_flowsheet.set_operating_conditions(m)
RO_flowsheet.initialize_system(m)

# set up the model for optimization
RO_flowsheet.optimize_set_up(m)
# simulate
RO_flowsheet.solve(m)

.. testoutput::
# set up the model for optimization
RO_flowsheet.optimize_set_up(m)

...
return m

where ``m`` is the flowsheet model that results after the initial "build" step and subsequent operations are performed on that object.

Once this sequence of setup steps is performed, the parameters to be varied should be identified with a dictionary:
Once this sequence of setup steps is performed, the parameters to be varied should be identified with a dictionary. Similarly to the way a
model is produced, in order to support native parallelism the preferred way to define parameters is by defining a function that creates them
(the generated model will automatically be passed in as the first argument):

.. testcode::

sweep_params = dict()
sweep_params['Feed Mass NaCl'] = LinearSample(m.fs.feed.flow_mass_phase_comp[0, 'Liq', 'NaCl'], 0.005, 0.155, 4)
sweep_params['Water Recovery'] = LinearSample(m.fs.RO.recovery_mass_phase_comp[0, 'Liq', 'H2O'], 0.3, 0.7, 4)
def build_sweep_params(model, **kwargs):
sweep_params = dict()
sweep_params['Feed Mass NaCl'] = LinearSample(model.fs.feed.flow_mass_phase_comp[0, 'Liq', 'NaCl'], 0.005, 0.155, 4)
sweep_params['Water Recovery'] = LinearSample(model.fs.RO.recovery_mass_phase_comp[0, 'Liq', 'H2O'], 0.3, 0.7, 4)
return sweep_params

where the basic pattern is ``dict_name['Short/Pretty-print Name'] = LinearSample(m.path.to.model.variable, lower_limit, upper_limit, num_samples)``.
For example, "Feed Mass NaCl" (the feed mass flow rate of NaCl), which is accessed through the model variable ``m.fs.feed.flow_mass_phase_comp[0, 'Liq', 'NaCl']``, is to be varied between 0.005 and 0.155 with 4 equally-spaced values, i.e., ``[0.005, 0.055, 0.105, 0.155]``.
Expand All @@ -88,24 +94,25 @@ For this RO flowsheet we'll report the levelized cost of water, the optimized RO

.. testcode::

outputs = dict()
outputs['RO membrane area'] = m.fs.RO.area
outputs['Pump 1 pressure'] = m.fs.P1.control_volume.properties_out[0].pressure
outputs['Levelized Cost of Water'] = m.fs.costing.LCOW
def build_outputs(model, sweep_params):
outputs = dict()
outputs['RO membrane area'] = model.fs.RO.area
outputs['Pump 1 pressure'] = model.fs.P1.control_volume.properties_out[0].pressure
outputs['Levelized Cost of Water'] = model.fs.costing.LCOW
return outputs

Once the problem is setup and the parameters are identified, the parameter_sweep function can finally be invoked which will perform the adjustment and optimization of the model using each combination of variables specified above (utilizing the solve method defined in our flowsheet module).
If specified, the parameter_sweep function will optionally write results in CSV format to the path specified in `csv_results_file_name` or in H5 format to the path specified in `h5_results_file_name`.
The file `outputs_results.csv` contains the `sweep_param` values and `outputs` values in an array format, while `outputs_results.h5` contains a dictionary containing the `sweep_params`, `outputs`, and a boolean list of successful or failed solves.
The H5 writer also creates a companion text file containing the metadata of the h5 file in `outputs_results.h5.txt`.
Note that if `outputs = None` and an H5 results file is specified, all of the pyomo model variables will be stored in the `outputs_results.h5` and `outputs_results.h5.txt` files.

In future versions, to allow for parallel implementations, the arguments for the model, sweep params, and outputs
to the parameter_sweep function will be callable functions that return the appropriate objects. Passing the objects
in directly still works but is deprecated.
Passing in a model, sweep params, and outputs directly to the parameter_sweep function is currently supported but is deprecated and will be removed in
future versions. The preferred way is to pass in generating functions as shown below:

.. testcode::

parameter_sweep(m, sweep_params, outputs, csv_results_file_name='outputs_results.csv', h5_results_file_name='outputs_results.h5')
parameter_sweep(build_model, build_sweep_params, build_outputs, csv_results_file_name='outputs_results.csv', h5_results_file_name='outputs_results.h5')

.. testoutput::

Expand Down
54 changes: 29 additions & 25 deletions docs/how_to_guides/how_to_use_parameter_sweep_monte_carlo.rst
Original file line number Diff line number Diff line change
Expand Up @@ -50,59 +50,63 @@ The parameter sweep tool currently offers 6 classes that can be broadly categori

Note that sampling types within ``FIXED`` can be specified with a different number of samples. However, the number of samples must be the same for all sweep variables within ``RANDOM`` and ``RANDOM_LHS``.

We will use the :ref:`same setup steps as before<how_to_use_parameter_sweep>` which returns a flowsheet model ``m``, and performs some initialization
We will use the :ref:`same setup steps as before<how_to_use_parameter_sweep>` to set up the generating functions for our model, sweep params, and outputs:

.. testcode::

# replace these function calls with
# those in your own flowsheet module
def build_model(**kwargs):

# set up system
m = RO_flowsheet.build()
RO_flowsheet.set_operating_conditions(m)
RO_flowsheet.initialize_system(m)
# replace these function calls with
# those in your own flowsheet module

# simulate
RO_flowsheet.solve(m)
# set up system
m = RO_flowsheet.build()
RO_flowsheet.set_operating_conditions(m)
RO_flowsheet.initialize_system(m)

# set up the model for optimization
RO_flowsheet.optimize_set_up(m)
# simulate
RO_flowsheet.solve(m)

.. testoutput::
# set up the model for optimization
RO_flowsheet.optimize_set_up(m)

...
return m

Once the model has been setup, we specify the variables to randomly sample using a dictionary

.. testcode::

num_samples = 25
sweep_params = dict()
sweep_params['Spacer_porosity'] = UniformSample(m.fs.RO.feed_side.spacer_porosity, 0.95, 0.99, num_samples)
sweep_params['A_comp'] = NormalSample(m.fs.RO.A_comp, 4.0e-12, 0.5e-12, num_samples)
sweep_params['B_comp'] = NormalSample(m.fs.RO.B_comp, 3.5e-8, 0.5e-8, num_samples)
def build_sweep_params(model, num_samples=1):
sweep_params = dict()
sweep_params['Spacer_porosity'] = UniformSample(model.fs.RO.feed_side.spacer_porosity, 0.95, 0.99, num_samples)
sweep_params['A_comp'] = NormalSample(model.fs.RO.A_comp, 4.0e-12, 0.5e-12, num_samples)
sweep_params['B_comp'] = NormalSample(model.fs.RO.B_comp, 3.5e-8, 0.5e-8, num_samples)
return sweep_params

where the ``spacer_porosity`` attribute will be randomly selected from a uniform distribution of values in the range :math:`[0.95, 0.99]` and model values ``A_comp`` and ``B_comp`` will be drawn from normal distributions centered at :math:`4.0\times10^{-12}` and :math:`3.5\times10^{-8}` with standard deviations of :math:`12-14\%`, respectively. For this example, we'll extract flowsheet outputs associated with cost, the levelized cost of water (LCOW) and energy consumption (EC), defined via another dictionary

.. testcode::

outputs = dict()
outputs['EC'] = m.fs.costing.specific_energy_consumption
outputs['LCOW'] = m.fs.costing.LCOW
def build_outputs(model, sweep_params):
outputs = dict()
outputs['EC'] = model.fs.costing.specific_energy_consumption
outputs['LCOW'] = model.fs.costing.LCOW
return outputs


With the flowsheet defined and suitably initialized, along with the definitions for ``sweep_params`` and ``outputs`` on hand, we can call the ``parameter_sweep`` function as before, where we exercise four new keyword arguments: (1) the ability to pass in custom optimization routines to be executed for each sample, (2) the ability to save per-process results for parallel debugging, (3) the specification of the number of samples to draw, and (4) the ability to set a seed for the randomly-generated values which allows consistency to be enforced between runs. The function passed in to `optimize_function` should return a Pyomo results object (i.e., the return value from calling the `solve` method).
With the generating functions defined and suitably initialized, we can call the ``parameter_sweep`` function as before, where we exercise five new keyword arguments: (1) the ability to pass in custom optimization routines to be executed for each sample, (2) the ability to save per-process results for parallel debugging, (3) the specification of the number of samples to draw, (4) the ability to set a seed for the randomly-generated values which allows consistency to be enforced between runs, and (5) the ability to pass a keyword arg into the build_sweep_params function. The function passed in to `optimize_function` should return a Pyomo results object (i.e., the return value from calling the `solve` method).

.. testcode::

# Define the local results directory, num_samples, and seed (if desired)
debugging_data_dir = 'local_results'
# Recall that num_samples = 25
num_samples = 25
seed = None

# Run the parameter sweep
global_results = parameter_sweep(m, sweep_params, outputs, csv_results_file_name='monte_carlo_results.csv',
optimize_function=RO_flowsheet.optimize, debugging_data_dir=debugging_data_dir, num_samples=num_samples, seed=seed)
global_results = parameter_sweep(build_model, build_sweep_params, build_outputs, csv_results_file_name='monte_carlo_results.csv',
optimize_function=RO_flowsheet.optimize, debugging_data_dir=debugging_data_dir, num_samples=num_samples, seed=seed,
build_sweep_params_kwargs=dict(num_samples=num_samples))

.. testoutput::

Expand Down
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
# update with a tag from the nawi-hub/idaes-pse
# when a version of IDAES newer than the latest stable release from PyPI
# will become needed for the watertap development
"idaes-pse==2.1.0",
"idaes-pse @ git+https://github.com/watertap-org/idaes-pse@2.2.0.dev0.watertap.23.08.03",
]

# Arguments marked as "Required" below must be included for upload to PyPI.
Expand Down
2 changes: 1 addition & 1 deletion watertap/core/membrane_channel0d.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ def _add_pressure_change(self, pressure_change_type=PressureChangeType.calculate
self.dP_dx = Var(
self.flowsheet().config.time,
initialize=-5e4,
bounds=(-2e5, -1e3),
bounds=(-2e5, None),
domain=NegativeReals,
units=units_meta("pressure") * units_meta("length") ** -1,
doc="pressure drop per unit length across channel",
Expand Down
6 changes: 0 additions & 6 deletions watertap/core/plugins/solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
#################################################################################

import pyomo.environ as pyo
from pyomo.opt import WriterFactory
from pyomo.core.base.block import _BlockData
from pyomo.core.kernel.block import IBlock
from pyomo.solvers.plugins.solvers.IPOPT import IPOPT
Expand All @@ -25,7 +24,6 @@
from idaes.logger import getLogger

_log = getLogger("watertap.core")
_default_nl_writer = WriterFactory.get_class("nl")


@pyo.SolverFactory.register(
Expand Down Expand Up @@ -56,9 +54,6 @@ def _presolve(self, *args, **kwds):
if "constr_viol_tol" not in self.options:
self.options["constr_viol_tol"] = 1e-08

# temporarily switch to nl_v1 writer
WriterFactory.register("nl")(WriterFactory.get_class("nl_v1"))

if not self._is_user_scaling():
self._cleanup_needed = False
return super()._presolve(*args, **kwds)
Expand Down Expand Up @@ -152,7 +147,6 @@ def _presolve(self, *args, **kwds):
raise

def _cleanup(self):
WriterFactory.register("nl")(_default_nl_writer)
if self._cleanup_needed:
self._reset_scaling_factors()
self._reset_bounds()
Expand Down
8 changes: 0 additions & 8 deletions watertap/core/plugins/tests/test_solvers.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
import pyomo.environ as pyo
import idaes.core.util.scaling as iscale

from pyomo.opt import WriterFactory
from pyomo.solvers.plugins.solvers.IPOPT import IPOPT
from pyomo.common.errors import ApplicationError
from idaes.core.util.scaling import (
Expand All @@ -24,8 +23,6 @@
from idaes.core.solvers import get_solver
from watertap.core.plugins.solvers import IpoptWaterTAP

_default_nl_writer = WriterFactory.get_class("nl")


class TestIpoptWaterTAP:
@pytest.fixture(scope="class")
Expand Down Expand Up @@ -63,11 +60,6 @@ def _test_bounds(self, m):
def s(self):
return pyo.SolverFactory("ipopt-watertap")

@pytest.mark.unit
def test_nl_writer_held_harmless(self, m, s):
s.solve(m, tee=True)
assert _default_nl_writer == WriterFactory.get_class("nl")

@pytest.mark.unit
def test_pyomo_registration(self, s):
assert s.__class__ is IpoptWaterTAP
Expand Down
62 changes: 32 additions & 30 deletions watertap/costing/units/electrodialysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,6 @@ def cost_electrodialysis(blk, cost_electricity_flow=True, has_rectifier=False):
"""
t0 = blk.flowsheet().time.first()

cost_electrodialysis_stack(blk)

# Changed this to grab power from performance table which is identified
# by same key regardless of whether the Electrodialysis unit is 0D or 1D
if cost_electricity_flow:
Expand All @@ -75,20 +73,7 @@ def cost_electrodialysis(blk, cost_electricity_flow=True, has_rectifier=False):
else:
power = blk.unit_model.get_power_electrical(blk.flowsheet().time.first())
cost_rectifier(blk, power=power, ac_dc_conversion_efficiency=0.9)
blk.capital_cost_constraint = pyo.Constraint(
expr=blk.capital_cost
== pyo.units.convert(
blk.costing_package.electrodialysis.membrane_capital_cost
* (
2
* blk.unit_model.cell_pair_num
* blk.unit_model.cell_width
* blk.unit_model.cell_length
),
to_units=blk.costing_package.base_currency,
)
+ blk.capital_cost_rectifier
)
cost_electrodialysis_stack(blk)


def cost_electrodialysis_stack(blk):
Expand All @@ -100,22 +85,39 @@ def cost_electrodialysis_stack(blk):
"""
make_capital_cost_var(blk)
make_fixed_operating_cost_var(blk)

blk.capital_cost_constraint = pyo.Constraint(
expr=blk.capital_cost
== pyo.units.convert(
blk.costing_package.electrodialysis.membrane_capital_cost
* (
2
* blk.unit_model.cell_pair_num
* blk.unit_model.cell_width
* blk.unit_model.cell_length
if blk.find_component("capital_cost_rectifier") is not None:
blk.capital_cost_constraint = pyo.Constraint(
expr=blk.capital_cost
== pyo.units.convert(
blk.costing_package.electrodialysis.membrane_capital_cost
* (
2
* blk.unit_model.cell_pair_num
* blk.unit_model.cell_width
* blk.unit_model.cell_length
)
+ blk.costing_package.electrodialysis.stack_electrode_captical_cost
* (2 * blk.unit_model.cell_width * blk.unit_model.cell_length),
to_units=blk.costing_package.base_currency,
)
+ blk.capital_cost_rectifier
)
else:
blk.capital_cost_constraint = pyo.Constraint(
expr=blk.capital_cost
== pyo.units.convert(
blk.costing_package.electrodialysis.membrane_capital_cost
* (
2
* blk.unit_model.cell_pair_num
* blk.unit_model.cell_width
* blk.unit_model.cell_length
)
+ blk.costing_package.electrodialysis.stack_electrode_captical_cost
* (2 * blk.unit_model.cell_width * blk.unit_model.cell_length),
to_units=blk.costing_package.base_currency,
)
+ blk.costing_package.electrodialysis.stack_electrode_captical_cost
* (2 * blk.unit_model.cell_width * blk.unit_model.cell_length),
to_units=blk.costing_package.base_currency,
)
)
blk.fixed_operating_cost_constraint = pyo.Constraint(
expr=blk.fixed_operating_cost
== pyo.units.convert(
Expand Down
2 changes: 1 addition & 1 deletion watertap/edb/data/base.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"temperature": [
273.15,
300,
650
647
],
"pressure": [
50000,
Expand Down
Loading
Loading