Skip to content

Commit 2fd8573

Browse files
K20shoresclaude
andcommitted
Expand export round-trip test to cover all supported reaction types
Adds Troe, Branched, TernaryChemicalActivation, Tunneling, UserDefined, and Surface reactions plus USER and SURF rate parameters alongside the existing Arrhenius, Photolysis, Emission, and FirstOrderLoss coverage. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 01b97c1 commit 2fd8573

1 file changed

Lines changed: 162 additions & 80 deletions

File tree

python/tests/integration/test_export_round_trip.py

Lines changed: 162 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,25 @@
11
"""
2-
Round-trip export test: build a MusicBox entirely in code with every possible
2+
Round-trip export test: build a MusicBox entirely in code with every supported
33
option, run it, export to a v1 JSON file via export(), reload with loadJson(),
44
run again, and assert the two result DataFrames are exactly equal.
55
6-
"Every possible option" covers:
7-
- All five BoxModelOptions fields (grid, chem_step_time, output_step_time,
8-
simulation_length, max_iterations)
9-
- Four reaction types: Arrhenius (all A/B/C/D/E params), Photolysis, Emission,
10-
FirstOrderLoss
11-
- Initial conditions at t=0: temperature, pressure, concentrations for all
12-
species, and all rate-parameter types (PHOTO, EMIS, LOSS)
13-
- Evolving conditions at t=30: temperature, pressure, concentration reset,
14-
and updated rate parameters
6+
Covers all five BoxModelOptions fields and one of each supported reaction type:
7+
- Arrhenius (all A/B/C/D/E params)
8+
- Troe (k0_A/B/C, kinf_A/B/C, Fc, N)
9+
- Branched (X, Y, a0, n)
10+
- TernaryChemicalActivation (same params as Troe)
11+
- Tunneling (A, B, C)
12+
- Photolysis (scaling_factor) — requires PHOTO rate parameter
13+
- Emission (scaling_factor) — requires EMIS rate parameter
14+
- FirstOrderLoss (scaling_factor) — requires LOSS rate parameter
15+
- UserDefined (scaling_factor) — requires USER rate parameter
16+
- Surface (reaction_probability) — requires SURF particle concentration
17+
and effective radius rate parameters
18+
19+
Initial conditions at t=0 set temperature, pressure, concentrations for every
20+
species, and all user-defined rate-parameter types (PHOTO, EMIS, LOSS, USER, SURF).
21+
Evolving conditions at t=30 reset temperature, pressure, one species concentration,
22+
and all rate parameters to new values.
1523
"""
1624

1725
import json
@@ -24,44 +32,96 @@
2432

2533
def _build_model():
2634
"""Return a fully-configured MusicBox using only the in-code API."""
35+
# Surface-reactive species requires molecular weight and a diffusion
36+
# coefficient on the gas-phase entry.
37+
srf = mc.Species(name="Srf", molecular_weight_kg_mol=0.029)
2738
A = mc.Species(name="A")
2839
B = mc.Species(name="B")
2940
C = mc.Species(name="C")
3041
D = mc.Species(name="D")
3142
E = mc.Species(name="E")
32-
species_list = [A, B, C, D, E]
33-
gas = mc.Phase(name="gas", species=species_list)
43+
F = mc.Species(name="F")
44+
G = mc.Species(name="G")
45+
H = mc.Species(name="H")
46+
I = mc.Species(name="I")
47+
J = mc.Species(name="J")
48+
all_species = [srf, A, B, C, D, E, F, G, H, I, J]
3449

35-
# Arrhenius: all five parameters set
36-
arr1 = mc.Arrhenius(
37-
name="A->B", A=4.0e-3, B=0.0, C=50.0, D=300.0, E=0.0,
38-
reactants=[A], products=[B], gas_phase=gas,
39-
)
40-
arr2 = mc.Arrhenius(
41-
name="B->C", A=1.2e-4, B=2.5, C=75.0, D=50.0, E=0.5,
42-
reactants=[B], products=[C], gas_phase=gas,
43-
)
44-
# Photolysis: requires PHOTO rate parameter
45-
photo = mc.Photolysis(
46-
name="photo_D", scaling_factor=1.0,
47-
reactants=[D], products=[E], gas_phase=gas,
48-
)
49-
# Emission: requires EMIS rate parameter
50-
emis = mc.Emission(
51-
name="emis_A", scaling_factor=1.0,
52-
products=[A], gas_phase=gas,
53-
)
54-
# FirstOrderLoss: requires LOSS rate parameter
55-
loss = mc.FirstOrderLoss(
56-
name="loss_E", scaling_factor=1.0,
57-
reactants=[E], gas_phase=gas,
50+
gas = mc.Phase(
51+
name="gas",
52+
species=[
53+
mc.PhaseSpecies(srf.name, diffusion_coefficient_m2_s=1e-5),
54+
A, B, C, D, E, F, G, H, I, J,
55+
],
5856
)
5957

58+
reactions = [
59+
# Arrhenius: all five kinetic parameters
60+
mc.Arrhenius(
61+
name="arr1", A=4.0e-3, B=0.5, C=50.0, D=300.0, E=0.1,
62+
reactants=[A], products=[B], gas_phase=gas,
63+
),
64+
# Troe: pressure-dependent falloff
65+
mc.Troe(
66+
name="troe1",
67+
k0_A=1.0e-28, k0_B=-3.0, k0_C=0.0,
68+
kinf_A=1.0e-11, kinf_B=0.0, kinf_C=0.0,
69+
Fc=0.45, N=1.0,
70+
reactants=[B], products=[C], gas_phase=gas,
71+
),
72+
# Branched: two product channels; n must be an integer
73+
mc.Branched(
74+
name="branched1", X=1.2e-3, Y=1.0, a0=0.15, n=9,
75+
reactants=[C],
76+
nitrate_products=[D],
77+
alkoxy_products=[E],
78+
gas_phase=gas,
79+
),
80+
# TernaryChemicalActivation: three-body activation
81+
mc.TernaryChemicalActivation(
82+
name="tca1",
83+
k0_A=1.0e-28, k0_B=-3.0, k0_C=0.0,
84+
kinf_A=1.0e-11, kinf_B=0.0, kinf_C=0.0,
85+
Fc=0.45, N=1.0,
86+
reactants=[D], products=[F], gas_phase=gas,
87+
),
88+
# Tunneling: quantum tunneling correction
89+
mc.Tunneling(
90+
name="tunnel1", A=1.0e-3, B=100.0, C=200.0,
91+
reactants=[E], products=[G], gas_phase=gas,
92+
),
93+
# Photolysis: rate set by PHOTO.photo1 condition
94+
mc.Photolysis(
95+
name="photo1", scaling_factor=1.0,
96+
reactants=[F], products=[H], gas_phase=gas,
97+
),
98+
# Emission: rate set by EMIS.emis1 condition
99+
mc.Emission(
100+
name="emis1", scaling_factor=1.0,
101+
products=[I], gas_phase=gas,
102+
),
103+
# FirstOrderLoss: rate set by LOSS.loss1 condition
104+
mc.FirstOrderLoss(
105+
name="loss1", scaling_factor=1.0,
106+
reactants=[H], gas_phase=gas,
107+
),
108+
# UserDefined: rate set by USER.ud1 condition
109+
mc.UserDefined(
110+
name="ud1", scaling_factor=1.0,
111+
reactants=[I], products=[J], gas_phase=gas,
112+
),
113+
# Surface: heterogeneous reaction; rates set by SURF.surf1 conditions
114+
mc.Surface(
115+
name="surf1", reaction_probability=0.1,
116+
gas_phase_species=srf, gas_phase_products=[A], gas_phase=gas,
117+
),
118+
]
119+
60120
mechanism = mc.Mechanism(
61121
name="all_options_test",
62-
species=species_list,
122+
species=all_species,
63123
phases=[gas],
64-
reactions=[arr1, arr2, photo, emis, loss],
124+
reactions=reactions,
65125
)
66126

67127
box = MusicBox()
@@ -74,83 +134,105 @@ def _build_model():
74134
box.box_model_options.simulation_length = 60.0
75135
box.box_model_options.max_iterations = 100
76136

77-
# Initial conditions (t=0): temperature, pressure, all species,
78-
# and all rate-parameter types
137+
# Initial conditions (t=0): temperature, pressure, all species
138+
# concentrations, and every user-defined rate-parameter type
79139
(box
80140
.set_condition(
81141
time=0.0,
82142
temperature=298.15,
83143
pressure=101325.0,
84-
concentrations={"A": 1.0, "B": 0.0, "C": 0.0, "D": 0.5, "E": 0.0},
144+
concentrations={
145+
"Srf": 1.0,
146+
"A": 1.0, "B": 0.0, "C": 0.0, "D": 0.0,
147+
"E": 0.0, "F": 0.5, "G": 0.0, "H": 0.0,
148+
"I": 0.5, "J": 0.0,
149+
},
85150
rate_parameters={
86-
"PHOTO.photo_D.s-1": 1.0e-4,
87-
"EMIS.emis_A.mol m-3 s-1": 1.0e-8,
88-
"LOSS.loss_E.s-1": 1.0e-3,
151+
"PHOTO.photo1.s-1": 1.0e-4,
152+
"EMIS.emis1.mol m-3 s-1": 1.0e-8,
153+
"LOSS.loss1.s-1": 1.0e-3,
154+
"USER.ud1.s-1": 1.0e-5,
155+
"SURF.surf1.particle number concentration.# m-3": 1.0e12,
156+
"SURF.surf1.effective radius.m": 1.0e-7,
89157
},
90158
)
91-
# Evolving conditions (t=30): temperature, pressure, concentration reset,
92-
# and updated rate parameters
159+
# Evolving conditions (t=30): updated temperature, pressure,
160+
# concentration reset, and all rate parameters changed
93161
.set_condition(
94162
time=30.0,
95163
temperature=300.0,
96164
pressure=101000.0,
97-
concentrations={"D": 0.3},
165+
concentrations={"A": 0.5},
98166
rate_parameters={
99-
"PHOTO.photo_D.s-1": 2.0e-4,
100-
"EMIS.emis_A.mol m-3 s-1": 5.0e-9,
101-
"LOSS.loss_E.s-1": 2.0e-3,
167+
"PHOTO.photo1.s-1": 2.0e-4,
168+
"EMIS.emis1.mol m-3 s-1": 5.0e-9,
169+
"LOSS.loss1.s-1": 2.0e-3,
170+
"USER.ud1.s-1": 2.0e-5,
171+
"SURF.surf1.particle number concentration.# m-3": 5.0e11,
172+
"SURF.surf1.effective radius.m": 2.0e-7,
102173
},
103174
))
104175

105176
return box
106177

107178

179+
EXPECTED_REACTION_TYPES = {
180+
"ARRHENIUS", "TROE", "BRANCHED_NO_RO2", "TERNARY_CHEMICAL_ACTIVATION",
181+
"TUNNELING", "PHOTOLYSIS", "EMISSION", "FIRST_ORDER_LOSS",
182+
"USER_DEFINED", "SURFACE",
183+
}
184+
185+
108186
class TestExportRoundTrip:
109187

110188
def test_export_file_structure(self, tmp_path):
111-
"""export() writes a well-formed v1 JSON file."""
189+
"""export() writes a well-formed v1 JSON file with every option."""
112190
box = _build_model()
113191
config_file = tmp_path / "config.json"
114192
box.export(str(config_file))
115193
with open(config_file) as f:
116194
config = json.load(f)
117195

118-
assert "box model options" in config
119-
assert "mechanism" in config
120-
assert "conditions" in config
121-
196+
# Box model options
122197
opts = config["box model options"]
123198
assert opts["grid"] == "box"
124199
assert opts["chemistry time step [sec]"] == 2.0
125200
assert opts["output time step [sec]"] == 6.0
126201
assert opts["simulation length [sec]"] == 60.0
127202
assert opts["max iterations"] == 100
128203

129-
mech = config["mechanism"]
130-
assert mech["version"] == "1.0.0"
131-
reaction_types = {r["type"] for r in mech["reactions"]}
132-
assert reaction_types == {"ARRHENIUS", "PHOTOLYSIS", "EMISSION", "FIRST_ORDER_LOSS"}
133-
134-
conds = config["conditions"]
135-
assert "data" in conds
136-
# Two time points → two data blocks
137-
assert len(conds["data"]) == 2
138-
139-
# t=0 block must contain all condition types
140-
t0_block = conds["data"][0]
141-
assert "CONC.A.mol m-3" in t0_block["headers"]
142-
assert "CONC.D.mol m-3" in t0_block["headers"]
143-
assert "ENV.temperature.K" in t0_block["headers"]
144-
assert "ENV.pressure.Pa" in t0_block["headers"]
145-
assert "PHOTO.photo_D.s-1" in t0_block["headers"]
146-
assert "EMIS.emis_A.mol m-3 s-1" in t0_block["headers"]
147-
assert "LOSS.loss_E.s-1" in t0_block["headers"]
148-
149-
# t=30 block must contain updated evolving conditions
150-
t30_block = conds["data"][1]
151-
assert "ENV.temperature.K" in t30_block["headers"]
152-
assert "CONC.D.mol m-3" in t30_block["headers"]
153-
assert "PHOTO.photo_D.s-1" in t30_block["headers"]
204+
# Mechanism contains every reaction type
205+
assert config["mechanism"]["version"] == "1.0.0"
206+
exported_types = {r["type"] for r in config["mechanism"]["reactions"]}
207+
assert exported_types == EXPECTED_REACTION_TYPES
208+
209+
# Conditions: two time-point blocks
210+
blocks = config["conditions"]["data"]
211+
assert len(blocks) == 2
212+
213+
# t=0 block: all condition types present
214+
t0_headers = set(blocks[0]["headers"])
215+
assert "ENV.temperature.K" in t0_headers
216+
assert "ENV.pressure.Pa" in t0_headers
217+
assert "CONC.Srf.mol m-3" in t0_headers
218+
assert "CONC.A.mol m-3" in t0_headers
219+
assert "PHOTO.photo1.s-1" in t0_headers
220+
assert "EMIS.emis1.mol m-3 s-1" in t0_headers
221+
assert "LOSS.loss1.s-1" in t0_headers
222+
assert "USER.ud1.s-1" in t0_headers
223+
assert "SURF.surf1.particle number concentration.# m-3" in t0_headers
224+
assert "SURF.surf1.effective radius.m" in t0_headers
225+
226+
# t=30 block: all rate-parameter types updated
227+
t30_headers = set(blocks[1]["headers"])
228+
assert "ENV.temperature.K" in t30_headers
229+
assert "CONC.A.mol m-3" in t30_headers
230+
assert "PHOTO.photo1.s-1" in t30_headers
231+
assert "EMIS.emis1.mol m-3 s-1" in t30_headers
232+
assert "LOSS.loss1.s-1" in t30_headers
233+
assert "USER.ud1.s-1" in t30_headers
234+
assert "SURF.surf1.particle number concentration.# m-3" in t30_headers
235+
assert "SURF.surf1.effective radius.m" in t30_headers
154236

155237
def test_round_trip_results_exactly_equal(self, tmp_path):
156238
"""
@@ -175,7 +257,7 @@ def test_round_trip_results_exactly_equal(self, tmp_path):
175257
f"Result shapes differ: {df1.shape} vs {df2.shape}"
176258
)
177259

178-
# Sort columns so ordering differences don't cause false failures
260+
# Sort columns to handle any ordering differences
179261
df1_sorted = df1.reindex(sorted(df1.columns), axis=1)
180262
df2_sorted = df2.reindex(sorted(df2.columns), axis=1)
181263

0 commit comments

Comments
 (0)