mirror of
https://github.com/Cantera/cantera.git
synced 2026-08-07 03:34:48 -05:00
1456 lines
54 KiB
Python
1456 lines
54 KiB
Python
import numpy as np
|
|
import pickle
|
|
import pytest
|
|
from pytest import approx
|
|
import re
|
|
from ruamel import yaml
|
|
|
|
import cantera as ct
|
|
from .utilities import load_yaml
|
|
|
|
try:
|
|
ct.composite._import_pandas()
|
|
except ImportError:
|
|
pass
|
|
|
|
from cantera.composite import _pandas
|
|
|
|
|
|
class TestModels:
|
|
|
|
@pytest.fixture(scope='class', autouse=True)
|
|
def yml_file(self, test_data_path):
|
|
return test_data_path / "thermo-models.yaml"
|
|
|
|
@pytest.fixture(scope='class')
|
|
def yml(self, yml_file):
|
|
return load_yaml(yml_file)
|
|
|
|
def test_invalid(self):
|
|
with pytest.raises(TypeError):
|
|
ct.Solution(None)
|
|
|
|
def test_load_thermo_models(self, yml, yml_file):
|
|
for ph in yml['phases']:
|
|
ph_name = ph['name']
|
|
try:
|
|
sol = ct.Solution(yml_file, ph_name)
|
|
|
|
T0, p0 = sol.TP
|
|
TD = sol.TD
|
|
z = sol.state # calls Phase::saveState
|
|
sol.TP = 300, 2*ct.one_atm
|
|
sol.state = z # calls Phase::restoreState
|
|
assert sol.T == T0
|
|
assert sol.P == p0
|
|
|
|
if sol.thermo_model in ('pure-fluid',):
|
|
assert sol.has_phase_transition
|
|
else:
|
|
assert not sol.has_phase_transition
|
|
|
|
if not sol.is_compressible:
|
|
with pytest.raises(ct.CanteraError,
|
|
match='Density is not an independent'):
|
|
sol.TD = TD
|
|
|
|
assert len(z) == sol.state_size
|
|
if sol.is_pure:
|
|
# stoich phase (fixed composition)
|
|
assert sol.n_species == 1
|
|
assert len(z) == 2
|
|
else:
|
|
assert len(z) == 2 + sol.n_species
|
|
|
|
except Exception as inst:
|
|
|
|
# raise meaningful error message without breaking test suite
|
|
# ignore deprecation warnings originating in C++ layer
|
|
# (converted to errors in test suite)
|
|
if 'Deprecated' not in str(inst):
|
|
|
|
msg = ("Error in processing of phase '{}' with type '{}'\n"
|
|
"TPX = {}")
|
|
msg = msg.format(ph['name'], ph['thermo'], sol.TPX)
|
|
raise TypeError(msg) from inst
|
|
|
|
def test_restore_thermo_models(self, yml, yml_file):
|
|
|
|
def check(a, b):
|
|
assert a.T == approx(b.T)
|
|
assert a.P == approx(b.P)
|
|
assert a.X == approx(b.X)
|
|
|
|
for ph in yml['phases']:
|
|
|
|
skipped = ['pure-fluid']
|
|
if ph['thermo'] in skipped:
|
|
continue
|
|
|
|
ph_name = ph['name']
|
|
|
|
try:
|
|
sol = ct.Solution(yml_file, ph_name)
|
|
a = ct.SolutionArray(sol, 10)
|
|
if ph['thermo'] == 'liquid-water-IAPWS95':
|
|
# ensure that phase remains liquid
|
|
a.TP = sol.T, sol.critical_pressure
|
|
|
|
# assign some state
|
|
T = 373.15 + 100*np.random.rand(10)
|
|
P = a.P * (1 + np.random.rand(10))
|
|
if sol.is_pure:
|
|
a.TP = T, P
|
|
else:
|
|
X = a.X
|
|
xmin = np.min(X[X>0])
|
|
ix = np.where(X == xmin)
|
|
X[ix] = .5 * X[ix]
|
|
X = np.diag(X.sum(axis=1)).dot(X)
|
|
assert not sol.is_pure
|
|
assert 'TPX' in sol._full_states.values()
|
|
a.TPX = T, P, X
|
|
|
|
# default columns
|
|
data = a.collect_data()
|
|
b = ct.SolutionArray(sol)
|
|
b.restore_data(data)
|
|
check(a, b)
|
|
|
|
except Exception as inst:
|
|
|
|
# raise meaningful error message without breaking test suite
|
|
# ignore deprecation warnings originating in C++ layer
|
|
# (converted to errors in test suite)
|
|
if 'Deprecated' not in str(inst):
|
|
|
|
msg = ("Error in processing of phase '{}' with type '{}'\n"
|
|
"TPX = {}")
|
|
msg = msg.format(ph['name'], ph['thermo'], sol.TPX)
|
|
raise TypeError(msg) from inst
|
|
|
|
|
|
class TestPickle:
|
|
|
|
def test_pickle_gas(self):
|
|
gas = ct.Solution("h2o2.yaml", transport_model=None)
|
|
gas.TPX = 500, 500000, "H2:.75,O2:.25"
|
|
with open(self.test_work_path / "gas.pkl", "wb") as pkl:
|
|
pickle.dump(gas, pkl)
|
|
|
|
with open(self.test_work_path / "gas.pkl", "rb") as pkl:
|
|
gas2 = pickle.load(pkl)
|
|
assert gas.T == approx(gas2.T)
|
|
assert gas.P == approx(gas2.P)
|
|
assert gas.X == approx(gas2.X)
|
|
|
|
assert gas2.transport_model == "none"
|
|
|
|
def test_pickle_gas_with_transport(self):
|
|
gas = ct.Solution("h2o2.yaml")
|
|
gas.TPX = 500, 500000, "H2:.75,O2:.25"
|
|
gas.transport_model = "multicomponent"
|
|
with open(self.test_work_path / "gas.pkl", "wb") as pkl:
|
|
pickle.dump(gas, pkl)
|
|
|
|
with open(self.test_work_path / "gas.pkl", "rb") as pkl:
|
|
gas2 = pickle.load(pkl)
|
|
assert gas.T == approx(gas2.T)
|
|
assert gas.P == approx(gas2.P)
|
|
assert gas.X == approx(gas2.X)
|
|
|
|
assert gas2.transport_model == "multicomponent"
|
|
|
|
def test_pickle_interface(self):
|
|
interface = ct.Interface("diamond.yaml", "diamond_100")
|
|
|
|
with pytest.raises(NotImplementedError):
|
|
with open(self.test_work_path / "interface.pkl", "wb") as pkl:
|
|
pickle.dump(interface, pkl)
|
|
|
|
|
|
@pytest.fixture(params=[ct.ThermoPhase(), ct.Solution()])
|
|
def empty_object(request):
|
|
return request.param
|
|
|
|
def test_empty_report(empty_object):
|
|
with pytest.raises(NotImplementedError):
|
|
empty_object()
|
|
|
|
def test_empty_TP(empty_object):
|
|
with pytest.raises(NotImplementedError):
|
|
empty_object.TP = 300, ct.one_atm
|
|
|
|
def test_empty_equilibrate(empty_object):
|
|
with pytest.raises(NotImplementedError):
|
|
empty_object.equilibrate("TP")
|
|
|
|
@pytest.fixture
|
|
def empty_solution():
|
|
return ct.Solution()
|
|
|
|
def test_empty_composite(empty_solution):
|
|
""" Test empty Solution object """
|
|
assert empty_solution.thermo_model == "none"
|
|
assert empty_solution.composite == ("none", "none", "none")
|
|
|
|
|
|
class TestEmptyEdgeCases:
|
|
""" Test for edge cases where constructors are not allowed """
|
|
def test_empty_phase(self):
|
|
with pytest.raises(ValueError,
|
|
match="Arguments are insufficient to define a phase"):
|
|
ct.ThermoPhase(thermo="ideal-gas")
|
|
|
|
def test_empty_kinetics(self):
|
|
with pytest.raises(ValueError, match="Cannot instantiate"):
|
|
ct.Kinetics()
|
|
|
|
def test_empty_transport(self):
|
|
with pytest.raises(ValueError, match="Cannot instantiate"):
|
|
ct.Transport()
|
|
|
|
|
|
class TestSolutionArray:
|
|
""" Test SolutionArray basics """
|
|
|
|
@pytest.fixture
|
|
def gas(self):
|
|
return ct.Solution('h2o2.yaml', transport_model=None)
|
|
|
|
def test_from_state_scalar(self, gas):
|
|
state = list(gas.state)
|
|
arr = ct.SolutionArray(gas, states=[state])
|
|
assert arr.shape == (1,)
|
|
|
|
def test_from_state_list(self, gas):
|
|
states = [list(gas.state)] * 5
|
|
arr = ct.SolutionArray(gas, states=states)
|
|
assert arr.shape == (5,)
|
|
|
|
def test_from_state_array(self, gas):
|
|
states = [[list(gas.state)] * 5] * 3
|
|
arr = ct.SolutionArray(gas, states=states)
|
|
assert arr.shape == (3, 5) # shape is based on numpy conversion
|
|
|
|
def test_slice_twice(self, gas):
|
|
T_list = np.linspace(300, 1000, 8)
|
|
gas.TPX = T_list[0], ct.one_atm, {"H2": 1.}
|
|
arr = ct.SolutionArray(gas)
|
|
for T in T_list[1:]:
|
|
gas.TPX = T, ct.one_atm, {"H2": 1.}
|
|
arr.append(gas.state)
|
|
ix = 4
|
|
arr_trunc = arr[ix:]
|
|
assert arr_trunc.T[0] == arr.T[ix]
|
|
assert arr_trunc[0].T == arr.T[ix]
|
|
assert arr_trunc.T[-1] == arr.T[-1]
|
|
assert arr_trunc[-1].T == arr.T[-1]
|
|
assert (arr_trunc.T[:2] == arr.T[ix:ix+2]).all()
|
|
assert (arr_trunc[:2].T == arr.T[ix:ix+2]).all()
|
|
with pytest.raises(IndexError):
|
|
arr_trunc[10]
|
|
|
|
def test_invalid(self):
|
|
with pytest.raises(TypeError):
|
|
ct.SolutionArray(None)
|
|
with pytest.raises(TypeError):
|
|
ct.SolutionArray("gri30.yaml")
|
|
|
|
def test_from_state_numpy(self, gas):
|
|
states = np.array([[list(gas.state)] * 5] * 3)
|
|
arr = ct.SolutionArray(gas, states=states)
|
|
assert arr.shape == (3, 5)
|
|
|
|
def test_missing_attribute(self, gas):
|
|
arr = ct.SolutionArray(gas, 5, extra={"spam": 0})
|
|
assert len(arr.spam) == 5
|
|
with pytest.raises(AttributeError, match="no attribute"):
|
|
arr.eggs
|
|
|
|
def test_auxiliary(self, gas):
|
|
arr = ct.SolutionArray(gas, 5, extra={"spam": 0})
|
|
arr.spam = np.arange(5)
|
|
assert len(arr.spam) == 5
|
|
assert arr.get_auxiliary(4) == {"spam": 4}
|
|
arr.set_auxiliary(0, {"spam": 42})
|
|
assert arr.spam[0] == 42
|
|
|
|
def test_disables_add_species(self, gas):
|
|
states = [list(gas.state)] * 3
|
|
arr = ct.SolutionArray(gas, states=states)
|
|
|
|
species_x = ct.Species("X", {"H": 3})
|
|
species_x.thermo = ct.ConstantCp(200, 5000, ct.one_atm, coeffs=(0,0,0,0))
|
|
N = gas.n_species
|
|
with pytest.raises(ct.CanteraError, match="is being used"):
|
|
gas.add_species(species_x)
|
|
|
|
assert gas.n_species == N
|
|
|
|
# Adding species works again after the Solution is no longer in use
|
|
del arr
|
|
gas.add_species(species_x)
|
|
assert gas.n_species == N + 1
|
|
|
|
def test_selected_species(self, gas):
|
|
gas.TPX = 300, ct.one_atm, {"H2": .5, "O2": .5}
|
|
gas.equilibrate("HP")
|
|
gas.TP = 1500, ct.one_atm
|
|
siz = 10
|
|
arr = ct.SolutionArray(gas, shape=siz)
|
|
for spc in gas.species_names:
|
|
assert arr(spc).Y.shape == (siz, 1)
|
|
assert arr(spc).Y[0] == gas[spc].Y[0]
|
|
wi_dot = arr(spc).net_production_rates
|
|
assert wi_dot.shape == (siz, 1)
|
|
assert wi_dot[0] == gas[spc].net_production_rates[0]
|
|
spc = ["H2", "O2"]
|
|
assert arr(*spc).Y.shape == (siz, 2)
|
|
assert arr(*spc).net_production_rates.shape == (siz, 2)
|
|
|
|
def test_interface_wdot(self):
|
|
gas = ct.Solution("ptcombust.yaml", "gas", transport_model=None)
|
|
surf = ct.Interface("ptcombust.yaml", "Pt_surf", [gas])
|
|
arr = ct.SolutionArray(surf, shape=1)
|
|
with pytest.raises(NotImplementedError, match="containing Interface"):
|
|
arr.net_production_rates
|
|
arr = ct.SolutionArray(gas, shape=1)
|
|
assert arr.net_production_rates.size == gas.n_species
|
|
|
|
def test_pickle_solutionarray(self):
|
|
sol = ct.Solution("gri30.yaml")
|
|
solarr = ct.SolutionArray(sol, 10)
|
|
# Fill with some data
|
|
T = np.linspace(300, 2000, 10)
|
|
P = np.linspace(1e5, 5e5, 10)
|
|
X = np.zeros((10, sol.n_species))
|
|
X[:, sol.species_index("H2")] = 0.7
|
|
X[:, sol.species_index("O2")] = 0.3
|
|
solarr.TPX = T, P, X
|
|
|
|
outfile = self.test_work_path / "solarr.pkl"
|
|
with open(outfile, "wb") as f:
|
|
pickle.dump(solarr, f)
|
|
|
|
with open(outfile, "rb") as f:
|
|
solarr_loaded = pickle.load(f)
|
|
|
|
# Compare all fields
|
|
assert solarr.shape == solarr_loaded.shape
|
|
assert solarr.T == approx(solarr_loaded.T)
|
|
assert solarr.P == approx(solarr_loaded.P)
|
|
assert solarr.X == approx(solarr_loaded.X)
|
|
# Check all state vectors
|
|
for orig, loaded in zip(solarr, solarr_loaded):
|
|
assert orig.T == approx(loaded.T)
|
|
assert orig.P == approx(loaded.P)
|
|
assert orig.X == approx(loaded.X)
|
|
|
|
@pytest.fixture(scope='class')
|
|
def setup_solution_array_info_tests(request):
|
|
request.cls.gas = ct.Solution('h2o2.yaml', transport_model=None)
|
|
|
|
@pytest.fixture(scope='function')
|
|
def setup_solution_array_info_data(request, setup_solution_array_info_tests):
|
|
request.cls.gas.TPY = 300, ct.one_atm, "H2: 1"
|
|
|
|
@pytest.mark.usefixtures('setup_solution_array_info_data')
|
|
class TestSolutionArrayInfo:
|
|
""" Test SolutionArray summary output """
|
|
width = 80
|
|
|
|
def check(self, arr, repr, rows):
|
|
count = 0
|
|
width = None
|
|
header = None
|
|
for line in repr.split("\n"):
|
|
if not len(line):
|
|
break
|
|
if width is None:
|
|
width = len(line)
|
|
assert width <= self.width
|
|
header = line.split()
|
|
else:
|
|
assert width == len(line)
|
|
count += 1
|
|
|
|
if rows is not None:
|
|
assert count == rows + 1 # account for header
|
|
|
|
names = arr.component_names
|
|
if "..." not in header:
|
|
assert len(header) == len(names)
|
|
else:
|
|
assert len(header) > 1
|
|
header = {key for key in header if key != "..."}
|
|
assert not header.difference(names)
|
|
|
|
def test_short(self):
|
|
arr = ct.SolutionArray(self.gas, 5)
|
|
self.check(arr, arr.info(rows=10, width=self.width), 5)
|
|
|
|
def test_long(self):
|
|
arr = ct.SolutionArray(self.gas, 20, extra={"spam": "eggs"})
|
|
self.check(arr, arr.info(rows=10, width=self.width), 11)
|
|
|
|
def test_scientific(self):
|
|
arr = ct.SolutionArray(self.gas, 20)
|
|
arr.set_equivalence_ratio(np.linspace(.5, 1.5, 20), "H2", "O2:1,N2:10")
|
|
arr.equilibrate("HP")
|
|
self.check(arr, arr.info(rows=7, width=self.width), 8)
|
|
|
|
def test_plus_minus_i(self):
|
|
arr = ct.SolutionArray(self.gas, 20,
|
|
extra={"foo": 10 * np.arange(-10, 10, dtype=int)})
|
|
self.check(arr, arr.info(rows=12, width=self.width), 13)
|
|
|
|
def test_plus_minus_f(self):
|
|
arr = ct.SolutionArray(self.gas, 20, extra={"foo": "bar", "spam": "eggs"})
|
|
self.check(arr, arr.info(rows=9), 10)
|
|
arr.foo = np.linspace(-1, 1.5, 20)
|
|
self.check(arr, arr.info(rows=12, width=self.width), 13)
|
|
|
|
def test_plus_minus_e(self):
|
|
arr = ct.SolutionArray(self.gas, 20, extra={"foo": "bar", "spam": "eggs"})
|
|
self.check(arr, arr.info(rows=9, width=100), 10)
|
|
arr.foo = np.linspace(-1e6, 1.5e6, 20)
|
|
self.check(arr, arr.info(rows=12, width=self.width), 13)
|
|
|
|
def test_strings(self):
|
|
arr = ct.SolutionArray(self.gas, 26, extra={"foo": "bar", "spam": "eggs"})
|
|
arr.spam = ["abcdefghijklmnopqrstuvwxyz"[:ix+1] for ix in range(26)]
|
|
self.check(arr, arr.info(rows=12, width=self.width), 13)
|
|
|
|
def test_double_vector(self):
|
|
arr = ct.SolutionArray(self.gas, 15, extra={"spam": "eggs"})
|
|
arr.spam = [[1.1, 2.2, 3.3] for _ in range(15)]
|
|
self.check(arr, arr.info(rows=12, width=self.width), 13)
|
|
|
|
def test_integer_vector(self):
|
|
arr = ct.SolutionArray(self.gas, 15, extra={"spam": "eggs"})
|
|
arr.spam = [np.array([1, 2, 3], dtype=int) for _ in range(15)]
|
|
self.check(arr, arr.info(rows=12, width=self.width), 13)
|
|
|
|
def test_string_vector(self):
|
|
arr = ct.SolutionArray(self.gas, 15, extra={"spam": "eggs"})
|
|
arr.spam = [["foo", "bar"] for _ in range(15)]
|
|
self.check(arr, arr.info(rows=12, width=self.width), 13)
|
|
|
|
def test_select_species(self):
|
|
arr = ct.SolutionArray(self.gas, 5)
|
|
arr2 = arr("H2")
|
|
lines = arr2.info(width=self.width).split("\n")
|
|
assert lines[0].split() == ["T", "D", "H2"]
|
|
|
|
def test_select_rows(self):
|
|
arr = ct.SolutionArray(self.gas, 25)
|
|
ix = [2, 5, 6, 9, 15, 3]
|
|
arr2 = arr[ix]
|
|
self.check(arr2, arr2.info(width=self.width), 6)
|
|
lines = arr2.info(width=self.width).split("\n")[1:-2]
|
|
loc = [int(line.split()[0]) for line in lines]
|
|
assert loc == ix
|
|
|
|
def test_water_simple(self):
|
|
w = ct.Water()
|
|
arr = ct.SolutionArray(w, 10)
|
|
self.check(arr, arr.info(rows=12, width=self.width), 10)
|
|
|
|
def test_water_extra(self):
|
|
w = ct.Water()
|
|
arr = ct.SolutionArray(w, 15, extra={"spam": np.arange(15, dtype=int)})
|
|
self.check(arr, arr.info(rows=7, width=self.width), 8)
|
|
|
|
@pytest.fixture(scope='class')
|
|
def setup_solution_array_io_tests(request):
|
|
request.cls.gas = ct.Solution('h2o2.yaml', transport_model=None)
|
|
|
|
@pytest.mark.usefixtures('setup_solution_array_io_tests')
|
|
class TestSolutionArrayIO:
|
|
""" Test SolutionArray file IO """
|
|
|
|
def test_collect_data(self):
|
|
states = ct.SolutionArray(self.gas)
|
|
collected = states.collect_data(tabular=True)
|
|
assert isinstance(collected, dict)
|
|
assert 'Y_H2' in collected
|
|
assert len(collected['Y_H2']) == 0
|
|
|
|
states = ct.SolutionArray(self.gas)
|
|
collected = states.collect_data(tabular=False, species='X')
|
|
assert 'X' in collected
|
|
assert collected['X'].shape == (0, self.gas.n_species)
|
|
|
|
def test_getitem(self):
|
|
states = ct.SolutionArray(self.gas, 10, extra={"index": range(10)})
|
|
for ix, state in enumerate(states):
|
|
assert state.index == ix
|
|
|
|
assert list(states[:2].index) == [0, 1]
|
|
assert list(states[100:102].index) == [] # outside of range
|
|
|
|
def test_append_state(self):
|
|
gas = ct.Solution("h2o2.yaml")
|
|
gas.TPX = 300, ct.one_atm, 'H2:0.5, O2:0.4'
|
|
states = ct.SolutionArray(gas)
|
|
states.append(gas.state)
|
|
assert states[0].T == gas.T
|
|
assert states[0].P == gas.P
|
|
assert states[0].X == approx(gas.X)
|
|
assert len(states) == 1
|
|
assert states.shape == (1,)
|
|
assert states.ndim == 1
|
|
assert states.size == 1
|
|
|
|
def test_append_no_norm_data(self):
|
|
gas = ct.Solution("h2o2.yaml")
|
|
gas.TP = 300, ct.one_atm
|
|
gas.set_unnormalized_mass_fractions(np.full(gas.n_species, 0.3))
|
|
states = ct.SolutionArray(gas)
|
|
states.append(T=gas.T, P=gas.P, Y=gas.Y, normalize=False)
|
|
assert states[0].T == gas.T
|
|
assert states[0].P == gas.P
|
|
assert states[0].Y == approx(gas.Y)
|
|
|
|
def test_append_scrambled_input(self):
|
|
gas = ct.Solution("h2o2.yaml")
|
|
gas.TP = 300, ct.one_atm
|
|
gas.set_unnormalized_mass_fractions(np.full(gas.n_species, 0.3))
|
|
states = ct.SolutionArray(gas)
|
|
states.append(Y=gas.Y, P=gas.P, normalize=False, T=gas.T)
|
|
assert states[0].T == gas.T
|
|
assert states[0].P == gas.P
|
|
assert states[0].Y == approx(gas.Y)
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
def test_import_no_norm_data_h5(self):
|
|
outfile = self.test_work_path / "solutionarray_no_norm.h5"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
gas = ct.Solution("h2o2.yaml")
|
|
gas.transport_model = "multicomponent"
|
|
gas.set_unnormalized_mole_fractions(np.full(gas.n_species, 0.3))
|
|
states = ct.SolutionArray(gas, 5)
|
|
states.save(outfile, "group0")
|
|
|
|
gas_new = ct.Solution("h2o2.yaml")
|
|
b = ct.SolutionArray(gas_new)
|
|
b.restore(outfile, "group0") #, normalize=False)
|
|
assert states.T == approx(b.T)
|
|
assert states.P == approx(b.P)
|
|
assert states.X == approx(b.X)
|
|
assert gas_new.transport_model == "multicomponent"
|
|
|
|
def test_import_no_norm_data_yaml(self):
|
|
outfile = self.test_work_path / "solutionarray_no_norm.yaml"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
gas = ct.Solution("h2o2.yaml")
|
|
gas.transport_model = "multicomponent"
|
|
gas.set_unnormalized_mole_fractions(np.full(gas.n_species, 0.3))
|
|
states = ct.SolutionArray(gas, 5)
|
|
states.save(outfile, "group0")
|
|
|
|
gas_new = ct.Solution("h2o2.yaml")
|
|
b = ct.SolutionArray(gas_new)
|
|
b.restore(outfile, "group0") #, normalize=False)
|
|
assert states.T == approx(b.T)
|
|
assert states.P == approx(b.P)
|
|
assert states.X == approx(b.X)
|
|
assert gas_new.transport_model == "multicomponent"
|
|
|
|
def check_arrays(self, a, b, rtol=1e-8):
|
|
assert a.T == approx(b.T, rel=rtol)
|
|
assert a.P == approx(b.P, rel=rtol)
|
|
assert a.X == approx(b.X, rel=rtol)
|
|
for key in a.extra:
|
|
value = getattr(a, key)
|
|
if isinstance(value[0], str):
|
|
assert (getattr(b, key) == value).all()
|
|
else:
|
|
assert getattr(b, key) == approx(value, rel=rtol)
|
|
if b.meta:
|
|
# not all output formats preserve metadata
|
|
for key, value in a.meta.items():
|
|
assert b.meta[key] == value
|
|
|
|
def test_write_csv(self):
|
|
outfile = self.test_work_path / "solutionarray_new.csv"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
arr = ct.SolutionArray(self.gas, 7)
|
|
arr.TPX = np.linspace(300, 1000, 7), 2e5, "H2:0.5, O2:0.4"
|
|
arr.equilibrate("HP")
|
|
arr.save(outfile, basis="mole")
|
|
|
|
with open(outfile, "r") as fid:
|
|
header = fid.readline()
|
|
assert "X_H2" in header.split(",")
|
|
|
|
b = ct.SolutionArray(self.gas)
|
|
b.read_csv(outfile)
|
|
self.check_arrays(arr, b)
|
|
|
|
with pytest.raises(ct.CanteraError, match="already exists"):
|
|
arr.save(outfile)
|
|
|
|
def test_write_csv_fancy(self):
|
|
outfile = self.test_work_path / "solutionarray_fancy.csv"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
extra = {"foo": range(7), "bar": range(7), "spam": "eggs"}
|
|
arr = ct.SolutionArray(self.gas, 7, extra=extra)
|
|
arr.TPX = np.linspace(300, 1000, 7), 2e5, "H2:0.5, O2:0.4"
|
|
arr.equilibrate("HP")
|
|
arr.save(outfile)
|
|
|
|
with open(outfile, "r") as fid:
|
|
header = fid.readline()
|
|
assert "Y_H2" in header.split(",")
|
|
|
|
b = ct.SolutionArray(self.gas)
|
|
b.read_csv(outfile)
|
|
self.check_arrays(arr, b)
|
|
|
|
def test_write_csv_escaped(self):
|
|
outfile = self.test_work_path / "solutionarray_escaped.csv"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
extra = {"foo": range(7), "bar": range(7), "spam,eggs": "a,b,"}
|
|
arr = ct.SolutionArray(self.gas, 7, extra=extra)
|
|
arr.TPX = np.linspace(300, 1000, 7), 2e5, "H2:0.5, O2:0.4"
|
|
arr.equilibrate("HP")
|
|
arr.save(outfile, basis="mass")
|
|
|
|
with open(outfile, "r") as fid:
|
|
header = fid.readline()
|
|
assert "Y_H2" in header.split(",")
|
|
|
|
b = ct.SolutionArray(self.gas)
|
|
if _pandas is None:
|
|
with pytest.raises(ValueError):
|
|
# np.genfromtxt does not support escaped characters
|
|
b.read_csv(outfile)
|
|
return
|
|
|
|
b.read_csv(outfile)
|
|
self.check_arrays(arr, b)
|
|
|
|
df = _pandas.read_csv(outfile)
|
|
b.from_pandas(df)
|
|
self.check_arrays(arr, b)
|
|
|
|
def test_write_csv_exceptions(self):
|
|
outfile = self.test_work_path / f"solutionarray_invalid.csv"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
arr = ct.SolutionArray(self.gas, (2, 5))
|
|
with pytest.raises(ct.CanteraError, match="only works for 1D SolutionArray"):
|
|
arr.save(outfile)
|
|
|
|
arr = ct.SolutionArray(self.gas, 10, extra={'spam"eggs': "foo"})
|
|
with pytest.raises(NotImplementedError, match="double quotes or line feeds"):
|
|
arr.save(outfile)
|
|
|
|
arr = ct.SolutionArray(self.gas, 10, extra={"foo": 'spam\neggs'})
|
|
with pytest.raises(NotImplementedError, match="double quotes or line feeds"):
|
|
arr.save(outfile)
|
|
|
|
arr = ct.SolutionArray(self.gas, 10)
|
|
with pytest.raises(ct.CanteraError, match="Invalid species basis"):
|
|
arr.save(outfile, basis="foo")
|
|
|
|
@pytest.mark.skipif(_pandas is None, reason="pandas is not installed")
|
|
def test_to_pandas(self):
|
|
states = ct.SolutionArray(self.gas, 7, extra={"props": range(7)})
|
|
states.TPX = np.linspace(300, 1000, 7), 2e5, 'H2:0.5, O2:0.4'
|
|
df = states.to_pandas()
|
|
assert df.shape[0] == 7
|
|
states.props = np.zeros((7,2,))
|
|
with pytest.raises(NotImplementedError, match='not supported'):
|
|
states.to_pandas()
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
def test_write_hdf(self):
|
|
outfile = self.test_work_path / "solutionarray_fancy.h5"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
extra = {'foo': range(7), 'bar': range(7)}
|
|
meta = {'spam': 'eggs', 'hello': 'world'}
|
|
states = ct.SolutionArray(self.gas, 7, extra=extra, meta=meta)
|
|
states.TPX = np.linspace(300, 1000, 7), 2e5, 'H2:0.5, O2:0.4'
|
|
states.equilibrate('HP')
|
|
|
|
states.save(outfile, "group0")
|
|
|
|
b = ct.SolutionArray(self.gas)
|
|
attr = b.restore(outfile, "group0")
|
|
self.check_arrays(states, b)
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
def test_write_hdf_str_column(self):
|
|
self.run_write_str_column("h5")
|
|
|
|
def test_write_yaml_str_column(self):
|
|
self.run_write_str_column("yaml")
|
|
|
|
def run_write_str_column(self, mode):
|
|
outfile = self.test_work_path / f"solutionarray_str.{mode}"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
states = ct.SolutionArray(self.gas, 3, extra={'spam': 'eggs'})
|
|
states.save(outfile, "arr")
|
|
|
|
b = ct.SolutionArray(self.gas, extra={'spam'})
|
|
b.restore(outfile, "arr")
|
|
self.check_arrays(states, b)
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
def test_write_hdf_multidim_column(self):
|
|
self.run_write_multidim_column("h5")
|
|
|
|
def test_write_yaml_multidim_column(self):
|
|
self.run_write_multidim_column("yaml")
|
|
|
|
def run_write_multidim_column(self, mode):
|
|
outfile = self.test_work_path / f"solutionarray_multi.{mode}"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
states = ct.SolutionArray(self.gas, 3, extra={'spam': [[1, 2], [3, 4], [5, 6]]})
|
|
states.save(outfile, "arr")
|
|
|
|
b = ct.SolutionArray(self.gas, extra={'spam'})
|
|
b.restore(outfile, "arr")
|
|
self.check_arrays(states, b)
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
def test_write_hdf_2d(self):
|
|
self.run_write_2d("h5")
|
|
|
|
def test_write_yaml_2d(self):
|
|
self.run_write_2d("yaml")
|
|
|
|
def run_write_2d(self, mode):
|
|
outfile = self.test_work_path / f"solutionarray_2d.{mode}"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
states = ct.SolutionArray(self.gas, (2, 5))
|
|
states.save(outfile, "arr")
|
|
|
|
b = ct.SolutionArray(self.gas)
|
|
b.restore(outfile, "arr")
|
|
assert b.shape == states.shape
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
def test_overwrite_h5(self):
|
|
self.run_overwrite("h5")
|
|
|
|
def test_overwrite_yaml(self):
|
|
overwritten, fresh = self.run_overwrite("yaml")
|
|
|
|
# Check that keys are written in the same order in both newly-created
|
|
# and overwritten files
|
|
reader = yaml.YAML(typ="rt")
|
|
yml1 = reader.load(fresh)
|
|
yml2 = reader.load(overwritten)
|
|
|
|
assert list(yml1["arr"]) == list(yml2["arr"])
|
|
assert list(yml1["arr"]["data"]) == list(yml2["arr"]["data"])
|
|
|
|
# Header fields come first, and in the desired order
|
|
assert (list(yml1["arr"])[:4]
|
|
== ["generator", "cantera-version", "git-commit", "date"])
|
|
|
|
def run_overwrite(self, mode):
|
|
outfile = self.test_work_path / f"solutionarray_overwrite.{mode}"
|
|
outfile2 = self.test_work_path / f"solutionarray_fresh.{mode}"
|
|
outfile.unlink(missing_ok=True)
|
|
outfile2.unlink(missing_ok=True)
|
|
|
|
states = ct.SolutionArray(self.gas, 8)
|
|
states.TPX = np.linspace(300, 1000, 8), 2e5, 'H2:0.5, O2:0.4'
|
|
states.save(outfile, "arr")
|
|
|
|
b = ct.SolutionArray(self.gas)
|
|
b.restore(outfile, "arr")
|
|
assert b.shape == states.shape
|
|
assert b.T[-1] == states.T[-1]
|
|
|
|
states.equilibrate('HP')
|
|
|
|
with pytest.raises(ct.CanteraError, match="use 'overwrite' argument"):
|
|
states.save(outfile, "arr")
|
|
|
|
states.save(outfile, "arr", overwrite=True)
|
|
states.save(outfile2, "arr")
|
|
|
|
c = ct.SolutionArray(self.gas)
|
|
c.restore(outfile, "arr")
|
|
assert c.shape == states.shape
|
|
assert c.T[-1] == states.T[-1]
|
|
|
|
return outfile, outfile2
|
|
|
|
|
|
@pytest.fixture(scope='function')
|
|
def setup_legacy_hdf_tests(request):
|
|
request.cls.gas = ct.Solution('h2o2.yaml', transport_model=None)
|
|
|
|
@pytest.mark.usefixtures('setup_legacy_hdf_tests')
|
|
class TestLegacyHDF:
|
|
"""
|
|
Test SolutionArray legacy HDF file input
|
|
|
|
All input files were created using the Cantera 2.6 Python test suite:
|
|
- solutionarray_fancy_legacy.h5
|
|
-> test_composite.py::TestSolutionArrayIO::test_write_hdf
|
|
- solutionarray_str_legacy.h5
|
|
-> test_composite.py::TestSolutionArrayIO::test_write_hdf_str_column
|
|
- solutionarray_multi_legacy.h5
|
|
-> test_composite.py::TestSolutionArrayIO::test_write_hdf_multi_column
|
|
- solutionarray_no_norm_legacy.h5
|
|
-> test_composite.py::TestSolutionArrayIO::test_import_no_norm_data
|
|
- solutionarray_water_legacy.h5
|
|
-> test_composite.py::TestRestorePureFluid::test_import_no_norm_water
|
|
"""
|
|
|
|
@pytest.mark.xfail(reason="Unable to read fixed length strings from HDF")
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
@pytest.mark.filterwarnings("ignore:.*legacy HDF.*:UserWarning")
|
|
def test_legacy_hdf_str_column(self):
|
|
# h5py writes strings with fixed length, which require a priori knowledge of
|
|
# length in order to be read with HighFive (which currently only supports
|
|
# fixed string lengths based on compile-time templates)
|
|
self.run_read_legacy_hdf_str_column()
|
|
|
|
def run_read_legacy_hdf_str_column(self, test_data_path, legacy=False):
|
|
# recreate states used to create legacy HDF file
|
|
arr = ct.SolutionArray(self.gas, 3, extra={'spam': 'eggs'})
|
|
|
|
b = ct.SolutionArray(self.gas, extra={'spam'})
|
|
infile = test_data_path / f"solutionarray_str_legacy.h5"
|
|
|
|
if legacy:
|
|
b.read_hdf(infile)
|
|
else:
|
|
b.restore(infile, "group0")
|
|
assert all(arr.spam == b.spam)
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
@pytest.mark.filterwarnings("ignore:.*legacy HDF.*:UserWarning")
|
|
def test_legacy_hdf_multidim(self, test_data_path):
|
|
# recreate states used to create legacy HDF file
|
|
arr = ct.SolutionArray(self.gas, 3, extra={'spam': [[1, 2], [3, 4], [5, 6]]})
|
|
b = ct.SolutionArray(self.gas, extra={'spam'})
|
|
infile = test_data_path / f"solutionarray_multi_legacy.h5"
|
|
|
|
b.restore(infile, "group0")
|
|
assert arr.spam == approx(b.spam)
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
@pytest.mark.filterwarnings("ignore:.*legacy HDF.*:UserWarning")
|
|
def test_legacy_hdf(self, test_data_path):
|
|
# recreate states used to create legacy HDF file (valid portion)
|
|
extra = {'foo': range(7), 'bar': range(7)}
|
|
meta = {'spam': 'eggs', 'hello': 'world'}
|
|
states = ct.SolutionArray(self.gas, 7, extra=extra, meta=meta)
|
|
states.TPX = np.linspace(300, 1000, 7), 2e5, 'H2:0.5, O2:0.4'
|
|
states.equilibrate('HP')
|
|
|
|
infile = test_data_path / f"solutionarray_fancy_legacy.h5"
|
|
b = ct.SolutionArray(self.gas)
|
|
attr = b.restore(infile, "group0")
|
|
assert states.T == approx(b.T)
|
|
assert states.P == approx(b.P)
|
|
assert states.X == approx(b.X)
|
|
assert states.foo == approx(b.foo)
|
|
assert states.bar == approx(b.bar)
|
|
assert b.meta['spam'] == 'eggs'
|
|
assert b.meta['hello'] == 'world'
|
|
assert attr['foobar'] == 'spam and eggs'
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
@pytest.mark.filterwarnings("ignore:.*legacy HDF.*:UserWarning")
|
|
def test_read_legacy_hdf_no_norm(self, test_data_path):
|
|
# recreate states used to create legacy HDF file
|
|
self.gas.set_unnormalized_mole_fractions(np.full(self.gas.n_species, 0.3))
|
|
states = ct.SolutionArray(self.gas, 5)
|
|
|
|
infile = test_data_path / "solutionarray_no_norm_legacy.h5"
|
|
|
|
b = ct.SolutionArray(self.gas)
|
|
b.restore(infile, "group0")
|
|
assert states.T == approx(b.T, rel=1e-7)
|
|
assert states.P == approx(b.P, rel=1e-7)
|
|
assert states.X == approx(b.X, rel=1e-7)
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
@pytest.mark.filterwarnings("ignore:.*legacy HDF.*:UserWarning")
|
|
def test_import_no_norm_water(self, test_data_path):
|
|
# recreate states used to create legacy HDF file
|
|
w = ct.Water()
|
|
w.TQ = 300, 0.5
|
|
states = ct.SolutionArray(w, 5)
|
|
|
|
w_new = ct.Water()
|
|
infile = test_data_path / "solutionarray_water_legacy.h5"
|
|
c = ct.SolutionArray(w_new)
|
|
c.restore(infile, "group0")
|
|
assert states.T == approx(c.T, rel=1e-7)
|
|
assert states.P == approx(c.P, rel=1e-7)
|
|
assert states.Q == approx(c.Q, rel=1e-7)
|
|
|
|
|
|
@pytest.fixture(scope='class')
|
|
def setup_restore_ideal_gas_tests(request):
|
|
request.cls.gas = ct.Solution('h2o2.yaml', transport_model=None)
|
|
|
|
@pytest.mark.usefixtures('setup_restore_ideal_gas_tests')
|
|
class TestRestoreIdealGas:
|
|
""" Test restoring of the IdealGas class """
|
|
|
|
def test_restore_gas(self):
|
|
|
|
def check(a, b, atol=None):
|
|
if atol is None:
|
|
assert a.T == approx(b.T)
|
|
assert a.P == approx(b.P)
|
|
assert a.X == approx(b.X)
|
|
else:
|
|
assert a.T == approx(b.T, abs=atol)
|
|
assert a.P == approx(b.P, abs=atol)
|
|
assert a.X == approx(b.X, abs=atol)
|
|
|
|
# test ThermoPhase
|
|
a = ct.SolutionArray(self.gas)
|
|
for i in range(10):
|
|
T = 300 + 1800*np.random.random()
|
|
P = ct.one_atm*(1 + 10*np.random.random())
|
|
X = np.random.random(self.gas.n_species)
|
|
X[-1] = 0.
|
|
X /= X.sum()
|
|
a.append(T=T, P=P, X=X)
|
|
|
|
data = a.collect_data()
|
|
|
|
# basic restore
|
|
b = ct.SolutionArray(self.gas)
|
|
b.restore_data(data, normalize=True)
|
|
check(a, b)
|
|
|
|
# skip concentrations
|
|
b = ct.SolutionArray(self.gas)
|
|
b.restore_data({'T': data['T'], 'density': data['density']})
|
|
assert a.T == approx(b.T)
|
|
assert a.density == approx(b.density)
|
|
assert not a.X == approx(b.X)
|
|
|
|
# wrong data shape
|
|
b = ct.SolutionArray(self.gas)
|
|
with pytest.raises(ValueError):
|
|
b.restore_data({k: v[np.newaxis, :] for k, v in data.items()})
|
|
|
|
# inconsistent shape of receiving SolutionArray
|
|
b = ct.SolutionArray(self.gas, 9)
|
|
with pytest.raises(ValueError):
|
|
b.restore_data(data)
|
|
|
|
# incomplete state
|
|
b = ct.SolutionArray(self.gas)
|
|
with pytest.raises(ValueError):
|
|
b.restore_data(dict([tup for i, tup in enumerate(data.items()) if i]))
|
|
|
|
# add extra column
|
|
t = np.arange(10, dtype=float)
|
|
|
|
# auto-detection of extra
|
|
b = ct.SolutionArray(self.gas)
|
|
data_mod = dict(data) # create a copy
|
|
data_mod['time'] = t
|
|
b.restore_data(data_mod)
|
|
check(a, b)
|
|
|
|
# explicit extra
|
|
b = ct.SolutionArray(self.gas, extra=('time',))
|
|
b.restore_data(data_mod)
|
|
check(a, b)
|
|
assert b.time == approx(t)
|
|
|
|
# wrong extra
|
|
b = ct.SolutionArray(self.gas, extra=('xyz',))
|
|
with pytest.raises(KeyError):
|
|
b.restore_data(data_mod)
|
|
|
|
# missing extra
|
|
b = ct.SolutionArray(self.gas, extra=('time'))
|
|
with pytest.raises(KeyError):
|
|
b.restore_data(data)
|
|
|
|
# inconsistent species
|
|
data_mod = a.collect_data(tabular=True)
|
|
val = data_mod.pop('Y_AR')
|
|
data_mod['Y_invalid'] = val
|
|
b = ct.SolutionArray(self.gas)
|
|
with pytest.raises(ValueError):
|
|
b.restore_data(data_mod)
|
|
|
|
# incomplete species info (using threshold)
|
|
data = a.collect_data(threshold=1e-6)
|
|
|
|
# basic restore
|
|
b = ct.SolutionArray(self.gas)
|
|
b.restore_data(data)
|
|
check(a, b, atol=1e-6)
|
|
|
|
# skip calculated properties
|
|
cols = ('T', 'P', 'X', 'gibbs_mass', 'forward_rates_of_progress')
|
|
data = a.collect_data(cols=cols, threshold=1e-6)
|
|
|
|
b = ct.SolutionArray(self.gas)
|
|
b.restore_data(data)
|
|
check(a, b)
|
|
assert len(b.extra) == 0
|
|
|
|
@pytest.fixture(scope='class')
|
|
def setup_restore_pure_fluid_tests(request):
|
|
request.cls.water = ct.Water()
|
|
|
|
@pytest.mark.usefixtures('setup_restore_pure_fluid_tests')
|
|
class TestRestorePureFluid:
|
|
""" Test restoring of the PureFluid class """
|
|
|
|
def test_restore_water(self):
|
|
|
|
def check(a, b):
|
|
assert a.T == approx(b.T)
|
|
assert a.P == approx(b.P)
|
|
assert a.Q == approx(b.Q)
|
|
|
|
assert self.water.has_phase_transition
|
|
|
|
# benchmark
|
|
a = ct.SolutionArray(self.water, 10)
|
|
a.TQ = 373.15, np.linspace(0., 1., 10)
|
|
|
|
# complete data
|
|
cols = ('T', 'P', 'Q')
|
|
data = a.collect_data(cols=cols)
|
|
b = ct.SolutionArray(self.water)
|
|
b.restore_data(data)
|
|
check(a, b)
|
|
|
|
# partial data
|
|
cols = ('T', 'Q')
|
|
data = a.collect_data(cols=cols)
|
|
b = ct.SolutionArray(self.water)
|
|
b.restore_data(data)
|
|
check(a, b)
|
|
|
|
# default columns
|
|
data = a.collect_data()
|
|
assert list(data.keys()) == ['T', 'density']
|
|
b = ct.SolutionArray(self.water)
|
|
b.restore_data(data)
|
|
check(a, b)
|
|
|
|
# default state plus Y
|
|
cols = ('T', 'D', 'Y')
|
|
data = a.collect_data(cols=cols)
|
|
b = ct.SolutionArray(self.water)
|
|
b.restore_data(data)
|
|
check(a, b)
|
|
|
|
@pytest.mark.skipif("native" not in ct.hdf_support(),
|
|
reason="Cantera compiled without HDF support")
|
|
def test_import_no_norm_water(self):
|
|
outfile = self.test_work_path / "solutionarray_water.h5"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
w = ct.Water()
|
|
w.TQ = 300, 0.5
|
|
states = ct.SolutionArray(w, 5)
|
|
states.save(outfile, "group0")
|
|
|
|
w_new = ct.Water()
|
|
c = ct.SolutionArray(w_new)
|
|
c.restore(outfile, "group0") # normalize=False)
|
|
assert states.T == approx(c.T)
|
|
assert states.P == approx(c.P)
|
|
assert states.Q == approx(c.Q)
|
|
|
|
def test_append_no_norm_water(self):
|
|
w = ct.Water()
|
|
states = ct.SolutionArray(w)
|
|
w.TQ = 300, 0.5
|
|
states.append(w.state)
|
|
assert states[0].T == w.T
|
|
assert states[0].P == w.P
|
|
assert states[0].Q == w.Q
|
|
|
|
def test_phase_of_matter(self):
|
|
# based on test_thermo.py::TestSolutionArray::test_phase_of_matter
|
|
outfile = self.test_work_path / "solutionarray_pom.yaml"
|
|
outfile.unlink(missing_ok=True)
|
|
|
|
water = ct.Water()
|
|
states = ct.SolutionArray(water, 5)
|
|
T = [300, 500, water.critical_temperature * 2, 300]
|
|
P = [101325, 101325, 101325, water.critical_pressure*2]
|
|
states[:4].TP = T, P
|
|
states[4].TQ = 300, .4
|
|
pom = ['liquid', 'gas', 'supercritical', 'supercritical', 'liquid-gas-mix']
|
|
assert list(states.phase_of_matter) == pom
|
|
states.save(outfile, "group0")
|
|
|
|
saved = ct.SolutionArray(water)
|
|
saved.restore(outfile, "group0") # normalize=False)
|
|
assert states.T == approx(saved.T)
|
|
assert states.P == approx(saved.P)
|
|
assert states.Q == approx(saved.Q)
|
|
assert list(saved.phase_of_matter) == pom
|
|
|
|
|
|
class TestSolutionSerialization:
|
|
""" Test Solution serialization """
|
|
|
|
def test_input_data_simple(self):
|
|
gas = ct.Solution('h2o2.yaml')
|
|
data = gas.input_data
|
|
assert data['name'] == 'ohmech'
|
|
assert data['thermo'] == 'ideal-gas'
|
|
assert data['kinetics'] == 'bulk'
|
|
assert data['transport'] == 'mixture-averaged'
|
|
|
|
def test_input_data_user_modifications(self):
|
|
gas = ct.Solution("h2o2.yaml")
|
|
data1 = gas.input_data
|
|
gas.update_user_data({"foo": True}) # should get overwritten
|
|
extra = {"foo": [1.2, 3.4], "bar": [[1, 2], [3, 4]]}
|
|
gas.update_user_data(extra)
|
|
data2 = gas.input_data
|
|
assert extra["foo"] == data2["foo"]
|
|
assert extra["bar"] == data2["bar"]
|
|
gas.clear_user_data()
|
|
data3 = gas.input_data
|
|
assert data1 == data3
|
|
|
|
def test_input_data_state(self):
|
|
gas = ct.Solution('h2o2.yaml', transport_model=None)
|
|
data = gas.input_data
|
|
assert gas.T == data['state']['T']
|
|
assert gas.density == data['state']['density']
|
|
|
|
gas.TP = 500, 3.14e5
|
|
data = gas.input_data
|
|
assert gas.T == data['state']['T']
|
|
assert gas.density == data['state']['density']
|
|
|
|
def test_input_data_custom(self):
|
|
gas = ct.Solution('ideal-gas.yaml')
|
|
data = gas.input_data
|
|
assert data['custom-field']['first'] == True
|
|
assert data['custom-field']['last'] == [100, 200, 300]
|
|
|
|
# Check that items are ordered as expected
|
|
assert list(data) == ["name", "thermo", "elements", "species", "state",
|
|
"custom-field", "literal-string"]
|
|
assert list(data["custom-field"]) == ["first", "second", "last"]
|
|
assert data["literal-string"] == "spam\nand\neggs\n"
|
|
|
|
def test_input_data_debye_huckel(self):
|
|
soln = ct.Solution('thermo-models.yaml', 'debye-huckel-B-dot-ak')
|
|
data = soln.input_data
|
|
assert data['thermo'] == 'Debye-Huckel'
|
|
act_data = data['activity-data']
|
|
assert act_data['model'] == 'B-dot-with-variable-a'
|
|
assert act_data['default-ionic-radius'] == 4e-10
|
|
assert 'kinetics' not in data
|
|
assert 'transport' not in data
|
|
|
|
def test_yaml_simple(self):
|
|
gas = ct.Solution('h2o2.yaml')
|
|
gas.TPX = 500, ct.one_atm, 'H2: 1.0, O2: 1.0'
|
|
gas.equilibrate('HP')
|
|
gas.TP = 1500, ct.one_atm
|
|
gas.write_yaml(self.test_work_path / "h2o2-generated.yaml")
|
|
generated = load_yaml(self.test_work_path / "h2o2-generated.yaml")
|
|
for key in ('generator', 'date', 'phases', 'species', 'reactions'):
|
|
assert key in generated
|
|
assert generated['phases'][0]['transport'] == 'mixture-averaged'
|
|
for i, species in enumerate(generated['species']):
|
|
assert species['composition'] == gas.species(i).composition
|
|
for blessed, generated in zip(gas.reactions(), generated["reactions"]):
|
|
assert blessed.equation == generated["equation"]
|
|
|
|
gas2 = ct.Solution(self.test_work_path / "h2o2-generated.yaml")
|
|
assert gas.concentrations == approx(gas2.concentrations)
|
|
assert gas.partial_molar_enthalpies == approx(gas2.partial_molar_enthalpies)
|
|
assert gas.forward_rate_constants == approx(gas2.forward_rate_constants)
|
|
assert gas.mix_diff_coeffs == approx(gas2.mix_diff_coeffs)
|
|
|
|
def test_yaml_outunits1(self, cantera_data_path):
|
|
gas = ct.Solution('h2o2.yaml')
|
|
gas.TPX = 500, ct.one_atm, 'H2: 1.0, O2: 1.0'
|
|
gas.equilibrate('HP')
|
|
gas.TP = 1500, ct.one_atm
|
|
units = {'length': 'cm', 'quantity': 'mol', 'energy': 'cal'}
|
|
gas.write_yaml(self.test_work_path / "h2o2-generated.yaml", units=units)
|
|
generated = load_yaml(self.test_work_path / "h2o2-generated.yaml")
|
|
original = load_yaml(cantera_data_path / "h2o2.yaml")
|
|
assert generated['units'] == units
|
|
|
|
for r1, r2 in zip(original['reactions'], generated['reactions']):
|
|
if 'rate-constant' in r1:
|
|
assert r1['rate-constant']['A'] == approx(r2['rate-constant']['A'])
|
|
assert r1['rate-constant']['Ea'] == approx(r2['rate-constant']['Ea'])
|
|
|
|
gas2 = ct.Solution(self.test_work_path / "h2o2-generated.yaml")
|
|
assert gas.concentrations == approx(gas2.concentrations)
|
|
assert gas.partial_molar_enthalpies == approx(gas2.partial_molar_enthalpies)
|
|
assert gas.forward_rate_constants == approx(gas2.forward_rate_constants)
|
|
assert gas.mix_diff_coeffs == approx(gas2.mix_diff_coeffs)
|
|
|
|
def test_yaml_outunits2(self, cantera_data_path):
|
|
gas = ct.Solution('h2o2.yaml')
|
|
gas.TPX = 500, ct.one_atm, 'H2: 1.0, O2: 1.0'
|
|
gas.equilibrate('HP')
|
|
gas.TP = 1500, ct.one_atm
|
|
units = {'length': 'cm', 'quantity': 'mol', 'energy': 'cal'}
|
|
system = ct.UnitSystem(units)
|
|
gas.write_yaml(self.test_work_path / "h2o2-generated.yaml", units=system)
|
|
generated = load_yaml(self.test_work_path / "h2o2-generated.yaml")
|
|
original = load_yaml(cantera_data_path / "h2o2.yaml")
|
|
|
|
for r1, r2 in zip(original['reactions'], generated['reactions']):
|
|
if 'rate-constant' in r1:
|
|
assert r1['rate-constant']['A'] == approx(r2['rate-constant']['A'])
|
|
assert r1['rate-constant']['Ea'] == approx(r2['rate-constant']['Ea'])
|
|
|
|
gas2 = ct.Solution(self.test_work_path / "h2o2-generated.yaml")
|
|
assert gas.concentrations == approx(gas2.concentrations)
|
|
assert gas.partial_molar_enthalpies == approx(gas2.partial_molar_enthalpies)
|
|
assert gas.forward_rate_constants == approx(gas2.forward_rate_constants)
|
|
assert gas.mix_diff_coeffs == approx(gas2.mix_diff_coeffs)
|
|
|
|
def check_ptcombust(self, gas, surf):
|
|
generated = load_yaml(self.test_work_path / "ptcombust-generated.yaml")
|
|
for key in ("phases", "species", "gas-reactions", "Pt_surf-reactions"):
|
|
assert key in generated
|
|
assert len(generated["gas-reactions"]) == gas.n_reactions
|
|
assert len(generated["Pt_surf-reactions"]) == surf.n_reactions
|
|
assert len(generated["species"]) == surf.n_total_species
|
|
|
|
surf2 = ct.Solution(self.test_work_path / "ptcombust-generated.yaml", "Pt_surf")
|
|
assert surf.concentrations == approx(surf2.concentrations)
|
|
assert surf.partial_molar_enthalpies == approx(surf2.partial_molar_enthalpies)
|
|
assert surf.forward_rate_constants == approx(surf2.forward_rate_constants)
|
|
|
|
def test_yaml_surface_explicit(self):
|
|
gas = ct.Solution("ptcombust.yaml", "gas")
|
|
surf = ct.Interface("ptcombust.yaml", "Pt_surf", [gas])
|
|
gas.TPY = 900, ct.one_atm, np.ones(gas.n_species)
|
|
surf.coverages = np.ones(surf.n_species)
|
|
surf.write_yaml(self.test_work_path / "ptcombust-generated.yaml")
|
|
self.check_ptcombust(gas, surf)
|
|
|
|
def test_yaml_surface_adjacent(self):
|
|
surf = ct.Interface("ptcombust.yaml", "Pt_surf")
|
|
gas = surf.adjacent["gas"]
|
|
gas.TPY = 900, ct.one_atm, np.ones(gas.n_species)
|
|
surf.coverages = np.ones(surf.n_species)
|
|
surf.write_yaml(self.test_work_path / "ptcombust-generated.yaml")
|
|
self.check_ptcombust(gas, surf)
|
|
|
|
def test_yaml_eos(self):
|
|
ice = ct.Solution('water.yaml', 'ice')
|
|
ice.TP = 270, 2 * ct.one_atm
|
|
ice.write_yaml(self.test_work_path / "ice-generated.yaml", units={'length': 'mm', 'mass': 'g'})
|
|
|
|
ice2 = ct.Solution(self.test_work_path / "ice-generated.yaml")
|
|
assert ice.density == approx(ice2.density)
|
|
assert ice.entropy_mole == approx(ice2.entropy_mole)
|
|
|
|
def test_yaml_inconsistent_species(self):
|
|
gas = ct.Solution('h2o2.yaml', transport_model=None)
|
|
gas2 = ct.Solution('h2o2.yaml', transport_model=None)
|
|
gas2.name = 'modified'
|
|
# modify the NASA coefficients for one species
|
|
h2 = gas2.species('H2')
|
|
nasa_coeffs = h2.thermo.coeffs
|
|
nasa_coeffs[1] += 0.1
|
|
nasa_coeffs[8] += 0.1
|
|
h2.thermo = ct.NasaPoly2(h2.thermo.min_temp, h2.thermo.max_temp,
|
|
h2.thermo.reference_pressure, nasa_coeffs)
|
|
gas2.modify_species(gas2.species_index('H2'), h2)
|
|
with pytest.raises(ct.CanteraError, match="different definitions"):
|
|
gas.write_yaml(self.test_work_path / "h2o2-error.yaml", phases=gas2)
|
|
|
|
def test_yaml_user_data(self):
|
|
gas = ct.Solution("h2o2.yaml")
|
|
extra = {"spam": {"A": 1, "B": 2}, "eggs": [1, 2.3, 4.5]}
|
|
gas.update_user_data(extra)
|
|
S = gas.species(2)
|
|
S.update_user_data({"foo": "bar"})
|
|
S.transport.update_user_data({"baz": 1234.5})
|
|
S.thermo.update_user_data({"something": (False, True)})
|
|
gas.reaction(5).update_user_data({"baked-beans": True})
|
|
|
|
gas.write_yaml(self.test_work_path / "h2o2-generated-user-data.yaml")
|
|
gas2 = ct.Solution(self.test_work_path / "h2o2-generated-user-data.yaml")
|
|
data2 = gas2.species(2).input_data
|
|
|
|
assert gas2.input_data["spam"] == extra["spam"]
|
|
assert gas2.input_data["eggs"] == extra["eggs"]
|
|
assert data2["foo"] == "bar"
|
|
assert data2["transport"]["baz"] == 1234.5
|
|
assert data2["thermo"]["something"] == [False, True]
|
|
assert gas2.reaction(5).input_data["baked-beans"] is True
|
|
|
|
def test_yaml_strings(self):
|
|
yaml = """
|
|
phases:
|
|
- name: ohmech
|
|
thermo: ideal-gas
|
|
species: [{h2o2.yaml/species: all}]
|
|
extra: {key1: '1.0', key2: 2.0} # string values in a flow mapping
|
|
"""
|
|
gas = ct.Solution(yaml=yaml)
|
|
desc = " Line 1\n Line 2\n Line 3"
|
|
note = "First\n\nSecond\n Third"
|
|
note2 = "123199" # scalar string
|
|
note3 = ["77", "888", "9"] # list with all strings
|
|
note4 = ["111213", 444, "5.10"] # mixed types
|
|
gas.update_user_header({"description": desc})
|
|
gas.species(1).update_user_data({"note": note})
|
|
gas.species(2).update_user_data({"note": note2})
|
|
gas.species(3).update_user_data({"note": note3})
|
|
gas.species(4).update_user_data({"note": note4})
|
|
|
|
generated_file = self.test_work_path / "h2o2-generated-user-header.yaml"
|
|
gas.write_yaml(generated_file)
|
|
gas2 = ct.Solution(generated_file)
|
|
|
|
# Ideally, multi-line YAML emitter would indicate stripping of the final newline
|
|
# (element annotated with '|-' instead of just '|') but this doesn't seem to be
|
|
# possible as of yaml-cpp 0.8.0.
|
|
assert gas2.input_header["description"].strip() == desc.strip()
|
|
assert gas2.species(1).input_data["note"].strip() == note.strip()
|
|
|
|
# number-like strings should be preserved as strings
|
|
assert gas2.species(2).input_data["note"] == note2
|
|
assert gas2.species(3).input_data["note"] == note3
|
|
assert gas2.species(4).input_data["note"] == note4
|
|
assert gas2.input_data["extra"]["key1"] == "1.0"
|
|
assert gas2.input_data["extra"]["key2"] == 2.0
|
|
|
|
# User-defined input in flow style should remain in flow style
|
|
yaml_gen = generated_file.read_text()
|
|
assert re.search("extra:.*key1.*key2", yaml_gen)
|
|
|
|
def test_duplicate_reactions(self):
|
|
R = [
|
|
ct.Reaction(equation='H2 + O = OH + H', rate=ct.ArrheniusRate(100, 0, 0)),
|
|
ct.Reaction(equation='H2 + O = OH + H', rate=ct.ArrheniusRate(50, 0.5, 0)),
|
|
ct.Reaction(equation='OH + H2 = H + H2O', rate=ct.ArrheniusRate(10, 2, 0)),
|
|
]
|
|
R[2].duplicate = True
|
|
|
|
gas = ct.Solution(thermo='ideal-gas', kinetics='gas',
|
|
species=ct.Species.list_from_file('h2o2.yaml'), reactions=R)
|
|
gas.TPX = 900, 2 * ct.one_atm, 'H2:1.0, O2:1.0'
|
|
|
|
yaml_file = self.test_work_path / "marking-duplicates.yaml"
|
|
gas.write_yaml(yaml_file)
|
|
restored = ct.Solution(yaml_file)
|
|
assert restored.reaction(0).duplicate is True
|
|
assert restored.reaction(1).duplicate is True
|
|
assert restored.reaction(2).duplicate is False
|
|
|
|
assert gas.forward_rate_constants == approx(restored.forward_rate_constants)
|
|
|
|
assert 'duplicate' in restored.reaction(0).input_data
|
|
restored.reaction(0).duplicate = False
|
|
assert 'duplicate' not in restored.reaction(0).input_data
|
|
|
|
class TestSpeciesSerialization:
|
|
|
|
def test_species_simple(self):
|
|
gas = ct.Solution('h2o2.yaml', transport_model=None)
|
|
data = gas.species('H2O').input_data
|
|
assert data['name'] == 'H2O'
|
|
assert data['composition'] == {'H': 2, 'O': 1}
|
|
|
|
def test_species_thermo(self):
|
|
gas = ct.Solution('h2o2.yaml', transport_model=None)
|
|
data = gas.species('H2O').input_data['thermo']
|
|
assert data['model'] == 'NASA7'
|
|
assert data['temperature-ranges'] == [200, 1000, 3500]
|
|
assert data['note'] == 'L8/89'
|
|
|
|
def test_species_transport(self):
|
|
gas = ct.Solution('h2o2.yaml')
|
|
data = gas.species('H2O').input_data['transport']
|
|
assert data['model'] == 'gas'
|
|
assert data['geometry'] == 'nonlinear'
|
|
assert data['dipole'] == approx(1.844)
|
|
|
|
|
|
class TestInterfaceAdjacent:
|
|
def test_surface(self):
|
|
surf = ct.Interface("ptcombust.yaml", "Pt_surf")
|
|
assert list(surf.adjacent) == ["gas"]
|
|
assert surf.phase_index(surf) == 0
|
|
assert surf.phase_index("gas") == 1
|
|
assert surf.phase_index(surf.adjacent["gas"]) == 1
|
|
|
|
def test_named_adjacent(self):
|
|
# override the adjacent-phases to change the order
|
|
surf = ct.Interface("surface-phases.yaml", "anode-surface",
|
|
adjacent=["electrolyte", "graphite"])
|
|
assert list(surf.adjacent), ["electrolyte", "graphite"]
|
|
|
|
def test_edge(self):
|
|
tpb = ct.Interface("sofc.yaml", "tpb")
|
|
assert set(tpb.adjacent) == {"metal_surface", "oxide_surface", "metal"}
|
|
assert isinstance(tpb.adjacent["metal_surface"], ct.Interface)
|
|
assert not isinstance(tpb.adjacent["metal"], ct.Interface)
|
|
gas1 = tpb.adjacent["metal_surface"].adjacent["gas"]
|
|
gas2 = tpb.adjacent["oxide_surface"].adjacent["gas"]
|
|
gas1.X = [0.1, 0.4, 0.3, 0.2]
|
|
assert gas1.X == approx(gas2.X)
|
|
|
|
def test_invalid(self):
|
|
with pytest.raises(ct.CanteraError, match="does not contain"):
|
|
surf = ct.Interface("ptcombust.yaml", "Pt_surf", ["foo"])
|
|
|
|
with pytest.raises(TypeError):
|
|
surf = ct.Interface("ptcombust.yaml", "Pt_surf", [2])
|
|
|
|
def test_remote_file(self):
|
|
yaml = """
|
|
phases:
|
|
- name: Pt_surf
|
|
thermo: ideal-surface
|
|
adjacent-phases: [{ptcombust.yaml/phases: [gas]}]
|
|
species: [{ptcombust.yaml/species: all}]
|
|
kinetics: surface
|
|
reactions: [{ptcombust.yaml/reactions: all}]
|
|
site-density: 2.7063e-09
|
|
"""
|
|
|
|
surf = ct.Interface(yaml=yaml)
|
|
assert surf.adjacent["gas"].n_species == 32
|
|
assert surf.n_reactions == 24
|