mantis-rtm 0.1.1.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mantis/__init__.py +5 -0
- mantis/_version.py +24 -0
- mantis/diff_lib.py +1937 -0
- mantis/generate_equation_system.py +1975 -0
- mantis/live_plot_lib.py +623 -0
- mantis/model.py +942 -0
- mantis/monitor_plots.py +171 -0
- mantis/monolithic_solver.py +904 -0
- mantis/parallel_video.py +523 -0
- mantis/plot_data_new.py +1363 -0
- mantis/solvers_numba.py +219 -0
- mantis_rtm-0.1.1.dev0.dist-info/METADATA +175 -0
- mantis_rtm-0.1.1.dev0.dist-info/RECORD +17 -0
- mantis_rtm-0.1.1.dev0.dist-info/WHEEL +5 -0
- mantis_rtm-0.1.1.dev0.dist-info/entry_points.txt +6 -0
- mantis_rtm-0.1.1.dev0.dist-info/licenses/LICENSE +661 -0
- mantis_rtm-0.1.1.dev0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1975 @@
|
|
|
1
|
+
"""generate_equation_system.py
|
|
2
|
+
|
|
3
|
+
Standalone declarative parser and code generator for MANTIS reaction transport models.
|
|
4
|
+
|
|
5
|
+
Translates declarative reaction specifications (e.g. chemical_equations_new.py) into
|
|
6
|
+
a fully assembled Python equation module (e.g. equations.py) with exact validation of:
|
|
7
|
+
- Species definitions in species.py
|
|
8
|
+
- Reaction constants in reaction_constants.py
|
|
9
|
+
- Limiters in limiters.py
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
------
|
|
13
|
+
python -m mantis.generate_equation_system [OPTIONS]
|
|
14
|
+
python src/mantis/generate_equation_system.py -i chemical_equations_new.py -o equations.py
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import ast
|
|
21
|
+
import importlib.util
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
import re
|
|
24
|
+
import sys
|
|
25
|
+
from typing import Any, Dict, List, Optional, Set, Tuple
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ValidationError(Exception):
|
|
29
|
+
"""Raised when chemical reaction specifications fail validation checks."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ReactionSystemValidator:
|
|
34
|
+
"""Validates chemical reaction specifications against model definitions."""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
species_path: Path,
|
|
39
|
+
constants_path: Path,
|
|
40
|
+
limiters_path: Optional[Path] = None,
|
|
41
|
+
reactions_path: Optional[Path] = None,
|
|
42
|
+
):
|
|
43
|
+
self.species_path = Path(species_path)
|
|
44
|
+
self.constants_path = Path(constants_path)
|
|
45
|
+
self.limiters_path = Path(limiters_path) if limiters_path is not None else None
|
|
46
|
+
self.reactions_path = Path(reactions_path) if reactions_path is not None else None
|
|
47
|
+
|
|
48
|
+
self.valid_species: Set[str] = set()
|
|
49
|
+
self.valid_constants: Set[str] = set()
|
|
50
|
+
self.valid_limiters: Set[str] = set()
|
|
51
|
+
|
|
52
|
+
self._load_definitions()
|
|
53
|
+
|
|
54
|
+
def _load_definitions(self) -> None:
|
|
55
|
+
"""Loads and extracts defined species, constants, and limiters."""
|
|
56
|
+
# 1. Species
|
|
57
|
+
if not self.species_path.exists():
|
|
58
|
+
raise FileNotFoundError(f"Species file not found: {self.species_path}")
|
|
59
|
+
spec_species = importlib.util.spec_from_file_location("species_mod", self.species_path)
|
|
60
|
+
if spec_species is None or spec_species.loader is None:
|
|
61
|
+
raise ImportError(f"Could not load module from {self.species_path}")
|
|
62
|
+
mod_species = importlib.util.module_from_spec(spec_species)
|
|
63
|
+
spec_species.loader.exec_module(mod_species)
|
|
64
|
+
if hasattr(mod_species, "species") and isinstance(mod_species.species, dict):
|
|
65
|
+
self.valid_species = set(mod_species.species.keys())
|
|
66
|
+
self.species_defs = mod_species.species
|
|
67
|
+
# Also register dynamic/alias species if defined with equilibrium_of or alias_of
|
|
68
|
+
for sp_k, sp_meta in mod_species.species.items():
|
|
69
|
+
if isinstance(sp_meta, dict):
|
|
70
|
+
if "equilibrium_of" in sp_meta or "alias_of" in sp_meta or "fraction" in sp_meta:
|
|
71
|
+
self.valid_species.add(sp_k)
|
|
72
|
+
else:
|
|
73
|
+
raise ValueError(f"No 'species' dictionary found in {self.species_path}")
|
|
74
|
+
|
|
75
|
+
# 2. Reaction constants
|
|
76
|
+
if not self.constants_path.exists():
|
|
77
|
+
raise FileNotFoundError(f"Reaction constants file not found: {self.constants_path}")
|
|
78
|
+
spec_constants = importlib.util.spec_from_file_location("rc_mod", self.constants_path)
|
|
79
|
+
if spec_constants is None or spec_constants.loader is None:
|
|
80
|
+
raise ImportError(f"Could not load module from {self.constants_path}")
|
|
81
|
+
mod_constants = importlib.util.module_from_spec(spec_constants)
|
|
82
|
+
spec_constants.loader.exec_module(mod_constants)
|
|
83
|
+
|
|
84
|
+
# Attempt dynamic extraction via get_reaction_constants
|
|
85
|
+
self.valid_constants = set()
|
|
86
|
+
if hasattr(mod_constants, "get_reaction_constants"):
|
|
87
|
+
try:
|
|
88
|
+
_velde, k_vals = mod_constants.get_reaction_constants(7.5, 0.8)
|
|
89
|
+
if isinstance(k_vals, dict):
|
|
90
|
+
self.valid_constants.update(k_vals.keys())
|
|
91
|
+
if isinstance(_velde, dict):
|
|
92
|
+
self.valid_constants.update(_velde.keys())
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Static AST fallback to capture constants in velde dict or module variables
|
|
97
|
+
try:
|
|
98
|
+
rc_tree = ast.parse(self.constants_path.read_text(encoding="utf-8"))
|
|
99
|
+
for node in ast.walk(rc_tree):
|
|
100
|
+
if isinstance(node, ast.Dict):
|
|
101
|
+
for key in node.keys:
|
|
102
|
+
if isinstance(key, ast.Constant) and isinstance(key.value, str):
|
|
103
|
+
self.valid_constants.add(key.value)
|
|
104
|
+
elif isinstance(node, ast.Assign):
|
|
105
|
+
for target in node.targets:
|
|
106
|
+
if isinstance(target, ast.Name):
|
|
107
|
+
self.valid_constants.add(target.id)
|
|
108
|
+
except Exception:
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
# 3. Limiters (from limiters_path or reactions_path)
|
|
112
|
+
self.valid_limiters = set()
|
|
113
|
+
self.limiters_defs: Dict[str, str] = {}
|
|
114
|
+
|
|
115
|
+
def _extract_limiters_from_ast(tree: ast.AST):
|
|
116
|
+
extracted = {}
|
|
117
|
+
for node in ast.walk(tree):
|
|
118
|
+
if isinstance(node, ast.Assign):
|
|
119
|
+
for target in node.targets:
|
|
120
|
+
if isinstance(target, ast.Subscript):
|
|
121
|
+
if isinstance(target.value, ast.Name) and target.value.id == "limiters":
|
|
122
|
+
if isinstance(target.slice, ast.Constant) and isinstance(target.slice.value, str):
|
|
123
|
+
expr_code = ast.unparse(node.value) if hasattr(ast, "unparse") else ""
|
|
124
|
+
extracted[target.slice.value] = expr_code
|
|
125
|
+
elif isinstance(target, ast.Name) and target.id == "limiters":
|
|
126
|
+
if isinstance(node.value, ast.Dict):
|
|
127
|
+
for k_node, v_node in zip(node.value.keys, node.value.values):
|
|
128
|
+
if isinstance(k_node, ast.Constant) and isinstance(k_node.value, str):
|
|
129
|
+
expr_code = ast.unparse(v_node) if hasattr(ast, "unparse") else ""
|
|
130
|
+
extracted[k_node.value] = expr_code
|
|
131
|
+
return extracted
|
|
132
|
+
|
|
133
|
+
raw_limiters: Dict[str, str] = {}
|
|
134
|
+
# 1. First check if limiters are defined in reactions_path (modern declarative standard)
|
|
135
|
+
if self.reactions_path is not None and self.reactions_path.exists():
|
|
136
|
+
try:
|
|
137
|
+
rxn_tree = ast.parse(self.reactions_path.read_text(encoding="utf-8"))
|
|
138
|
+
raw_limiters.update(_extract_limiters_from_ast(rxn_tree))
|
|
139
|
+
except Exception:
|
|
140
|
+
pass
|
|
141
|
+
|
|
142
|
+
# 2. If not defined in reactions_path, fall back to limiters_path
|
|
143
|
+
if not raw_limiters and self.limiters_path is not None and self.limiters_path.exists():
|
|
144
|
+
try:
|
|
145
|
+
lim_tree = ast.parse(self.limiters_path.read_text(encoding="utf-8"))
|
|
146
|
+
raw_limiters.update(_extract_limiters_from_ast(lim_tree))
|
|
147
|
+
except Exception as e:
|
|
148
|
+
raise ValueError(f"Could not parse limiters file {self.limiters_path}: {e}")
|
|
149
|
+
|
|
150
|
+
# Expand limiters:
|
|
151
|
+
# Base implicit limiter L_imp = 1 / (C + K) -> derive _implicit, _explicit, _inhibit
|
|
152
|
+
for lim_name, expr in raw_limiters.items():
|
|
153
|
+
self.limiters_defs[lim_name] = expr
|
|
154
|
+
self.valid_limiters.add(lim_name)
|
|
155
|
+
|
|
156
|
+
# If defined as base form (e.g. 'O2' or 'SO4' or 'Fe3') or ending in '_implicit'
|
|
157
|
+
base_key = lim_name[:-9] if lim_name.endswith("_implicit") else lim_name
|
|
158
|
+
|
|
159
|
+
# Add implicit form
|
|
160
|
+
self.valid_limiters.add(f"{base_key}_implicit")
|
|
161
|
+
self.valid_limiters.add(f"{base_key}_explicit")
|
|
162
|
+
self.valid_limiters.add(f"{base_key}_inhibit")
|
|
163
|
+
self.valid_limiters.add(f"{base_key}_inhib")
|
|
164
|
+
|
|
165
|
+
if "_" in base_key:
|
|
166
|
+
main_k, sub_k = base_key.split("_", 1)
|
|
167
|
+
self.valid_limiters.add(f"{main_k}_implicit_{sub_k}")
|
|
168
|
+
self.valid_limiters.add(f"{main_k}_explicit_{sub_k}")
|
|
169
|
+
self.valid_limiters.add(f"{main_k}_inhibit_{sub_k}")
|
|
170
|
+
self.valid_limiters.add(f"{main_k}_inhib_{sub_k}")
|
|
171
|
+
|
|
172
|
+
@staticmethod
|
|
173
|
+
def parse_reaction_species(reaction_str: str) -> Tuple[List[Tuple[float, str]], List[Tuple[float, str]]]:
|
|
174
|
+
"""Parses a chemical reaction equation string into (reactants, products).
|
|
175
|
+
|
|
176
|
+
Each species entry is (stoichiometric_coefficient, species_name).
|
|
177
|
+
Correctly handles ions with trailing '+' or '-' (e.g. '2 H+', 'SO4--') by splitting on
|
|
178
|
+
whitespace-padded ' + ' delimiters.
|
|
179
|
+
"""
|
|
180
|
+
if "->" not in reaction_str:
|
|
181
|
+
raise ValueError(f"Reaction string missing '->' delimiter: '{reaction_str}'")
|
|
182
|
+
|
|
183
|
+
left, right = reaction_str.split("->", 1)
|
|
184
|
+
|
|
185
|
+
def _parse_side(side_str: str) -> List[Tuple[float, str]]:
|
|
186
|
+
terms = re.split(r"\s+\+\s+", side_str.strip())
|
|
187
|
+
parsed = []
|
|
188
|
+
for term in terms:
|
|
189
|
+
term = term.strip()
|
|
190
|
+
if not term:
|
|
191
|
+
continue
|
|
192
|
+
parts = term.split(None, 1)
|
|
193
|
+
if len(parts) == 1:
|
|
194
|
+
coeff = 1.0
|
|
195
|
+
sp = parts[0]
|
|
196
|
+
else:
|
|
197
|
+
try:
|
|
198
|
+
coeff = float(parts[0])
|
|
199
|
+
sp = parts[1]
|
|
200
|
+
except ValueError:
|
|
201
|
+
coeff = 1.0
|
|
202
|
+
sp = term
|
|
203
|
+
parsed.append((coeff, sp))
|
|
204
|
+
return parsed
|
|
205
|
+
|
|
206
|
+
return _parse_side(left), _parse_side(right)
|
|
207
|
+
|
|
208
|
+
def validate_reaction(self, rxn_dict: Dict[str, Any]) -> List[str]:
|
|
209
|
+
"""Validates a single reaction dictionary. Returns a list of error messages (empty if valid)."""
|
|
210
|
+
errors = []
|
|
211
|
+
rxn_name = rxn_dict.get("reaction_name", "<unnamed_reaction>")
|
|
212
|
+
rxn_str = rxn_dict.get("reaction", "")
|
|
213
|
+
dyn_vars = rxn_dict.get("dynamic_variables", {}) or {}
|
|
214
|
+
iso_map = rxn_dict.get("isotope_species", {}) or {}
|
|
215
|
+
k_val = rxn_dict.get("k_value_name")
|
|
216
|
+
limiters = rxn_dict.get("limiters")
|
|
217
|
+
|
|
218
|
+
known_local_species = self.valid_species.union(dyn_vars.keys())
|
|
219
|
+
|
|
220
|
+
# 1. Validate reaction equation and species names
|
|
221
|
+
if not rxn_str:
|
|
222
|
+
errors.append(f"[{rxn_name}] Reaction equation string is empty or missing.")
|
|
223
|
+
else:
|
|
224
|
+
try:
|
|
225
|
+
reactants, products = self.parse_reaction_species(rxn_str)
|
|
226
|
+
for coeff, sp in reactants + products:
|
|
227
|
+
if sp not in known_local_species:
|
|
228
|
+
errors.append(
|
|
229
|
+
f"[{rxn_name}] Species '{sp}' not found in species.py or dynamic_variables."
|
|
230
|
+
)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
errors.append(f"[{rxn_name}] Failed to parse reaction equation '{rxn_str}': {e}")
|
|
233
|
+
|
|
234
|
+
# 2. Validate kinetic constant(s)
|
|
235
|
+
if k_val is not None:
|
|
236
|
+
if isinstance(k_val, str):
|
|
237
|
+
if k_val not in self.valid_constants:
|
|
238
|
+
errors.append(
|
|
239
|
+
f"[{rxn_name}] Rate constant '{k_val}' not found in reaction_constants.py."
|
|
240
|
+
)
|
|
241
|
+
elif isinstance(k_val, dict):
|
|
242
|
+
for branch, k_sub in k_val.items():
|
|
243
|
+
if k_sub not in self.valid_constants:
|
|
244
|
+
errors.append(
|
|
245
|
+
f"[{rxn_name}] Rate constant '{k_sub}' (branch '{branch}') not found in reaction_constants.py."
|
|
246
|
+
)
|
|
247
|
+
else:
|
|
248
|
+
errors.append(f"[{rxn_name}] Invalid k_value_name type: {type(k_val)}.")
|
|
249
|
+
|
|
250
|
+
# 3. Validate limiters & inhibitors
|
|
251
|
+
def _check_lim_entry(key_label: str, val: Any):
|
|
252
|
+
if val is None:
|
|
253
|
+
return
|
|
254
|
+
if isinstance(val, dict):
|
|
255
|
+
for target_sp, lim_items in val.items():
|
|
256
|
+
if target_sp not in known_local_species and target_sp != "global":
|
|
257
|
+
errors.append(
|
|
258
|
+
f"[{rxn_name}] {key_label} target species '{target_sp}' not defined in species.py or dynamic_variables."
|
|
259
|
+
)
|
|
260
|
+
item_list = [lim_items] if isinstance(lim_items, str) else lim_items
|
|
261
|
+
for lim_name in item_list:
|
|
262
|
+
# Check bare name, _implicit, _inhibit, etc.
|
|
263
|
+
if not any(
|
|
264
|
+
cand in self.valid_limiters
|
|
265
|
+
for cand in (lim_name, f"{lim_name}_implicit", f"{lim_name}_inhibit", f"{lim_name}_inhib")
|
|
266
|
+
):
|
|
267
|
+
errors.append(
|
|
268
|
+
f"[{rxn_name}] Fatal Error: {key_label} '{lim_name}' is not defined in the limiters specification or reactions file."
|
|
269
|
+
)
|
|
270
|
+
elif isinstance(val, (list, tuple)):
|
|
271
|
+
for lim_name in val:
|
|
272
|
+
if not any(
|
|
273
|
+
cand in self.valid_limiters
|
|
274
|
+
for cand in (lim_name, f"{lim_name}_implicit", f"{lim_name}_inhibit", f"{lim_name}_inhib")
|
|
275
|
+
):
|
|
276
|
+
errors.append(
|
|
277
|
+
f"[{rxn_name}] Fatal Error: {key_label} '{lim_name}' is not defined in the limiters specification or reactions file."
|
|
278
|
+
)
|
|
279
|
+
elif isinstance(val, str):
|
|
280
|
+
if not any(
|
|
281
|
+
cand in self.valid_limiters
|
|
282
|
+
for cand in (val, f"{val}_implicit", f"{val}_inhibit", f"{val}_inhib")
|
|
283
|
+
):
|
|
284
|
+
errors.append(
|
|
285
|
+
f"[{rxn_name}] Fatal Error: {key_label} '{val}' is not defined in the limiters specification or reactions file."
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
errors.append(f"[{rxn_name}] Invalid {key_label} format: {type(val)}.")
|
|
289
|
+
|
|
290
|
+
_check_lim_entry("Limiter", limiters)
|
|
291
|
+
inhibitors = rxn_dict.get("inhibitors")
|
|
292
|
+
_check_lim_entry("Inhibitor", inhibitors)
|
|
293
|
+
|
|
294
|
+
# 4. Validate isotope mapping
|
|
295
|
+
if iso_map:
|
|
296
|
+
if not isinstance(iso_map, dict):
|
|
297
|
+
errors.append(f"[{rxn_name}] isotope_species must be a dictionary.")
|
|
298
|
+
else:
|
|
299
|
+
for base_sp, iso_sp in iso_map.items():
|
|
300
|
+
if base_sp not in known_local_species:
|
|
301
|
+
errors.append(
|
|
302
|
+
f"[{rxn_name}] Base isotope species '{base_sp}' not defined in species.py or dynamic_variables."
|
|
303
|
+
)
|
|
304
|
+
if iso_sp not in known_local_species:
|
|
305
|
+
errors.append(
|
|
306
|
+
f"[{rxn_name}] Isotope species '{iso_sp}' not defined in species.py or dynamic_variables."
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# 5. Validate dynamic variables expressions (ensure valid Python syntax)
|
|
310
|
+
for var_name, expr in dyn_vars.items():
|
|
311
|
+
try:
|
|
312
|
+
ast.parse(expr)
|
|
313
|
+
except SyntaxError as se:
|
|
314
|
+
errors.append(
|
|
315
|
+
f"[{rxn_name}] Invalid Python syntax in dynamic variable '{var_name}': '{expr}' ({se})."
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
return errors
|
|
319
|
+
|
|
320
|
+
def validate_reactions(self, reactions: List[Dict[str, Any]]) -> List[str]:
|
|
321
|
+
"""Validates all reactions in the specification list."""
|
|
322
|
+
all_errors = []
|
|
323
|
+
for rxn_dict in reactions:
|
|
324
|
+
errs = self.validate_reaction(rxn_dict)
|
|
325
|
+
all_errors.extend(errs)
|
|
326
|
+
return all_errors
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def load_reactions_from_file(input_path: Path) -> List[Dict[str, Any]]:
|
|
330
|
+
"""Loads the reactions list from a chemical equations Python or JSON file."""
|
|
331
|
+
input_path = Path(input_path)
|
|
332
|
+
if not input_path.exists():
|
|
333
|
+
raise FileNotFoundError(f"Input equations file not found: {input_path}")
|
|
334
|
+
|
|
335
|
+
if input_path.suffix == ".py":
|
|
336
|
+
spec = importlib.util.spec_from_file_location("chem_eq_mod", input_path)
|
|
337
|
+
if spec is None or spec.loader is None:
|
|
338
|
+
raise ImportError(f"Could not load module from {input_path}")
|
|
339
|
+
mod = importlib.util.module_from_spec(spec)
|
|
340
|
+
spec.loader.exec_module(mod)
|
|
341
|
+
if not hasattr(mod, "reactions") or not isinstance(mod.reactions, list):
|
|
342
|
+
raise ValueError(f"Input file {input_path} must define a 'reactions' list.")
|
|
343
|
+
return mod.reactions
|
|
344
|
+
elif input_path.suffix == ".json":
|
|
345
|
+
import json
|
|
346
|
+
with open(input_path, "r", encoding="utf-8") as f:
|
|
347
|
+
data = json.load(f)
|
|
348
|
+
if isinstance(data, list):
|
|
349
|
+
return data
|
|
350
|
+
elif isinstance(data, dict) and "reactions" in data:
|
|
351
|
+
return data["reactions"]
|
|
352
|
+
raise ValueError(f"JSON input file {input_path} must be a list of reactions or contain a 'reactions' key.")
|
|
353
|
+
else:
|
|
354
|
+
raise ValueError(f"Unsupported input file format '{input_path.suffix}'. Expected .py or .json.")
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
class MantisArgumentParser(argparse.ArgumentParser):
|
|
358
|
+
"""ArgumentParser subclass ensuring backward compatibility aliases on parsed arguments."""
|
|
359
|
+
|
|
360
|
+
def parse_args(self, args=None, namespace=None):
|
|
361
|
+
ns = super().parse_args(args=args, namespace=namespace)
|
|
362
|
+
if not hasattr(ns, "input") or getattr(ns, "input") is None:
|
|
363
|
+
setattr(ns, "input", getattr(ns, "reactions", None))
|
|
364
|
+
if not hasattr(ns, "output") or getattr(ns, "output") is None:
|
|
365
|
+
setattr(ns, "output", getattr(ns, "equations", None))
|
|
366
|
+
if not hasattr(ns, "constants") or getattr(ns, "constants") is None:
|
|
367
|
+
setattr(ns, "constants", getattr(ns, "kinetic_constants", None))
|
|
368
|
+
return ns
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
# Alias for backward compatibility
|
|
372
|
+
FiPyriteArgumentParser = MantisArgumentParser
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def build_arg_parser() -> argparse.ArgumentParser:
|
|
376
|
+
"""Builds and returns the command line argument parser with full help descriptions."""
|
|
377
|
+
parser = MantisArgumentParser(
|
|
378
|
+
prog="generate_equation_system",
|
|
379
|
+
description=(
|
|
380
|
+
"MANTIS Declarative Equation System Generator:\n"
|
|
381
|
+
"Validates declarative chemical reaction specifications (e.g. chemical_equations_new.py) "
|
|
382
|
+
"against model species, reaction constants, and limiters, and generates an assembled "
|
|
383
|
+
"reaction equations module (e.g. equations.py)."
|
|
384
|
+
),
|
|
385
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
386
|
+
)
|
|
387
|
+
|
|
388
|
+
parser.add_argument(
|
|
389
|
+
"-r", "--reactions", "-i", "--input",
|
|
390
|
+
dest="reactions",
|
|
391
|
+
type=Path,
|
|
392
|
+
default=Path("nbk/experiments/chemical_equations_new.py"),
|
|
393
|
+
help="Path to declarative reaction definitions file (default: nbk/experiments/chemical_equations_new.py)",
|
|
394
|
+
)
|
|
395
|
+
parser.add_argument(
|
|
396
|
+
"-e", "--equations", "-o", "--output",
|
|
397
|
+
dest="equations",
|
|
398
|
+
type=Path,
|
|
399
|
+
default=Path("nbk/experiments/equations.py"),
|
|
400
|
+
help="Path to output generated equations module (default: nbk/experiments/equations.py)",
|
|
401
|
+
)
|
|
402
|
+
parser.add_argument(
|
|
403
|
+
"-s", "--species",
|
|
404
|
+
type=Path,
|
|
405
|
+
default=Path("nbk/experiments/species.py"),
|
|
406
|
+
help="Path to model species definitions file (default: nbk/experiments/species.py)",
|
|
407
|
+
)
|
|
408
|
+
parser.add_argument(
|
|
409
|
+
"-k", "--kinetic-constants", "--constants",
|
|
410
|
+
dest="kinetic_constants",
|
|
411
|
+
type=Path,
|
|
412
|
+
default=Path("nbk/experiments/reaction_constants.py"),
|
|
413
|
+
help="Path to reaction constants file (default: nbk/experiments/reaction_constants.py)",
|
|
414
|
+
)
|
|
415
|
+
parser.add_argument(
|
|
416
|
+
"-l", "--limiters",
|
|
417
|
+
type=Path,
|
|
418
|
+
default=Path("nbk/experiments/limiters.py"),
|
|
419
|
+
help="Path to limiters definition file (default: nbk/experiments/limiters.py)",
|
|
420
|
+
)
|
|
421
|
+
parser.add_argument(
|
|
422
|
+
"--validate-only",
|
|
423
|
+
action="store_true",
|
|
424
|
+
help="Perform cross-reference validation of species, constants, and limiters without writing output.",
|
|
425
|
+
)
|
|
426
|
+
parser.add_argument(
|
|
427
|
+
"-v", "--verbose",
|
|
428
|
+
action="store_true",
|
|
429
|
+
help="Enable detailed validation and parsing diagnostic output.",
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
return parser
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def generate_equations(
|
|
436
|
+
reactions_path: str | Path,
|
|
437
|
+
equations_path: str | Path,
|
|
438
|
+
kinetic_constants_path: str | Path = Path("nbk/experiments/reaction_constants.py"),
|
|
439
|
+
limiters_path: str | Path = Path("nbk/experiments/limiters.py"),
|
|
440
|
+
species_path: str | Path = Path("nbk/experiments/species.py"),
|
|
441
|
+
verbose: bool = False,
|
|
442
|
+
validate_only: bool = False,
|
|
443
|
+
) -> Path:
|
|
444
|
+
"""Validates and compiles declarative reactions into an assembled equations module.
|
|
445
|
+
|
|
446
|
+
Parameters
|
|
447
|
+
----------
|
|
448
|
+
reactions_path : str | Path
|
|
449
|
+
Path to declarative reactions definitions file.
|
|
450
|
+
equations_path : str | Path
|
|
451
|
+
Path where the generated equations module will be written.
|
|
452
|
+
kinetic_constants_path : str | Path
|
|
453
|
+
Path to reaction constants definitions file.
|
|
454
|
+
limiters_path : str | Path | None, optional
|
|
455
|
+
Path to limiters definition file. If None or not found, limiters will be read from reactions_path.
|
|
456
|
+
species_path : str | Path
|
|
457
|
+
Path to model species definitions file.
|
|
458
|
+
verbose : bool
|
|
459
|
+
If True, prints diagnostic output.
|
|
460
|
+
validate_only : bool
|
|
461
|
+
If True, performs cross-validation without writing output.
|
|
462
|
+
|
|
463
|
+
Returns
|
|
464
|
+
-------
|
|
465
|
+
Path
|
|
466
|
+
Path to the output equations module.
|
|
467
|
+
"""
|
|
468
|
+
reactions_p = Path(reactions_path).resolve()
|
|
469
|
+
equations_p = Path(equations_path).resolve()
|
|
470
|
+
constants_p = Path(kinetic_constants_path).resolve()
|
|
471
|
+
limiters_p = Path(limiters_path).resolve() if limiters_path is not None else None
|
|
472
|
+
species_p = Path(species_path).resolve()
|
|
473
|
+
|
|
474
|
+
if verbose:
|
|
475
|
+
print(f"[MANTIS] Loading reactions from: {reactions_p}")
|
|
476
|
+
print(f"[MANTIS] Using species definitions: {species_p}")
|
|
477
|
+
print(f"[MANTIS] Using reaction constants: {constants_p}")
|
|
478
|
+
if limiters_p and limiters_p.exists():
|
|
479
|
+
print(f"[MANTIS] Using limiters definitions: {limiters_p}")
|
|
480
|
+
else:
|
|
481
|
+
print(f"[MANTIS] Using limiters definitions from reactions file")
|
|
482
|
+
|
|
483
|
+
# 1. Load validator
|
|
484
|
+
validator = ReactionSystemValidator(
|
|
485
|
+
species_path=species_p,
|
|
486
|
+
constants_path=constants_p,
|
|
487
|
+
limiters_path=limiters_p if (limiters_p and limiters_p.exists()) else None,
|
|
488
|
+
reactions_path=reactions_p,
|
|
489
|
+
)
|
|
490
|
+
|
|
491
|
+
# 2. Load input reactions
|
|
492
|
+
reactions = load_reactions_from_file(reactions_p)
|
|
493
|
+
|
|
494
|
+
# 3. Validate
|
|
495
|
+
errors = validator.validate_reactions(reactions)
|
|
496
|
+
if errors:
|
|
497
|
+
err_msg = f"Validation failed for {reactions_p} with {len(errors)} error(s):\n" + "\n".join(f" • {e}" for e in errors)
|
|
498
|
+
raise ValidationError(err_msg)
|
|
499
|
+
|
|
500
|
+
if verbose:
|
|
501
|
+
print(f"[MANTIS] All {len(reactions)} reactions validated successfully!")
|
|
502
|
+
|
|
503
|
+
if validate_only:
|
|
504
|
+
return equations_p
|
|
505
|
+
|
|
506
|
+
# 4. Code Generation
|
|
507
|
+
generator = EquationSystemGenerator(reactions=reactions, validator=validator)
|
|
508
|
+
generated_code = generator.generate_code()
|
|
509
|
+
|
|
510
|
+
equations_p.parent.mkdir(parents=True, exist_ok=True)
|
|
511
|
+
with open(equations_p, "w", encoding="utf-8") as f:
|
|
512
|
+
f.write(generated_code)
|
|
513
|
+
|
|
514
|
+
if verbose:
|
|
515
|
+
print(f"[MANTIS] Successfully wrote equations module to: {equations_p}")
|
|
516
|
+
|
|
517
|
+
return equations_p
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def resolve_and_prepare_equations(
|
|
521
|
+
p_dict: Dict[str, Any],
|
|
522
|
+
experiment_name: Optional[str] = None,
|
|
523
|
+
base_dir: Optional[Path] = None,
|
|
524
|
+
) -> Tuple[Any, Path, Path]:
|
|
525
|
+
"""Resolves reaction and equation files from p_dict, regenerates equations if outdated,
|
|
526
|
+
and dynamically imports the equations module.
|
|
527
|
+
|
|
528
|
+
Resolution scheme:
|
|
529
|
+
reactions: defaults to f"{experiment}_reactions.py"
|
|
530
|
+
equations: defaults to f"{experiment}_equations.py"
|
|
531
|
+
kinetic_constants: defaults to "reaction_constants.py"
|
|
532
|
+
limiters: defaults to "limiters.py"
|
|
533
|
+
|
|
534
|
+
Parameters
|
|
535
|
+
----------
|
|
536
|
+
p_dict : dict
|
|
537
|
+
Parameter dictionary passed to the simulation.
|
|
538
|
+
experiment_name : str, optional
|
|
539
|
+
Name of the experiment stem. If None, derived from p_dict["experiment"].
|
|
540
|
+
base_dir : Path, optional
|
|
541
|
+
Base directory to search for experiment files. Defaults to cwd or experiment file parent.
|
|
542
|
+
|
|
543
|
+
Returns
|
|
544
|
+
-------
|
|
545
|
+
Tuple[module, Path, Path]
|
|
546
|
+
(loaded_equations_module, resolved_reactions_path, resolved_equations_path)
|
|
547
|
+
"""
|
|
548
|
+
exp = p_dict.get("experiment") or experiment_name or "pyrite"
|
|
549
|
+
|
|
550
|
+
if base_dir is None:
|
|
551
|
+
if "__file__" in p_dict and p_dict["__file__"]:
|
|
552
|
+
base_dir = Path(p_dict["__file__"]).resolve().parent
|
|
553
|
+
else:
|
|
554
|
+
base_dir = Path.cwd()
|
|
555
|
+
|
|
556
|
+
def _resolve(val: Any, default_name: str) -> Path:
|
|
557
|
+
filename = default_name if (val is None or val == "file_name") else str(val)
|
|
558
|
+
p = Path(filename)
|
|
559
|
+
if p.is_absolute() and p.exists():
|
|
560
|
+
return p
|
|
561
|
+
# Check relative to base_dir
|
|
562
|
+
if (base_dir / p).exists():
|
|
563
|
+
return (base_dir / p).resolve()
|
|
564
|
+
# Check relative to cwd
|
|
565
|
+
if (Path.cwd() / p).exists():
|
|
566
|
+
return (Path.cwd() / p).resolve()
|
|
567
|
+
# Check in nbk/experiments
|
|
568
|
+
if (Path.cwd() / "nbk" / "experiments" / p).exists():
|
|
569
|
+
return (Path.cwd() / "nbk" / "experiments" / p).resolve()
|
|
570
|
+
return (base_dir / p).resolve()
|
|
571
|
+
|
|
572
|
+
# Apply defaults if not specified or None
|
|
573
|
+
rxn_val = p_dict.get("reactions")
|
|
574
|
+
eq_val = p_dict.get("equations")
|
|
575
|
+
kc_val = p_dict.get("kinetic_constants")
|
|
576
|
+
lim_val = p_dict.get("limiters")
|
|
577
|
+
sp_val = p_dict.get("species")
|
|
578
|
+
|
|
579
|
+
reactions_path = _resolve(rxn_val, f"{exp}_reactions.py")
|
|
580
|
+
equations_path = _resolve(eq_val, f"{exp}_equations.py")
|
|
581
|
+
constants_path = _resolve(kc_val, "reaction_constants.py")
|
|
582
|
+
species_path = _resolve(sp_val, "species.py")
|
|
583
|
+
|
|
584
|
+
# If limiters not explicitly specified in p_dict, check if base_dir has limiters.py
|
|
585
|
+
if lim_val is not None:
|
|
586
|
+
limiters_path = _resolve(lim_val, "limiters.py")
|
|
587
|
+
elif (base_dir / "limiters.py").exists():
|
|
588
|
+
limiters_path = (base_dir / "limiters.py").resolve()
|
|
589
|
+
else:
|
|
590
|
+
limiters_path = None
|
|
591
|
+
|
|
592
|
+
if not reactions_path.exists():
|
|
593
|
+
raise FileNotFoundError(
|
|
594
|
+
f"Declarative reactions file not found: '{reactions_path}'. "
|
|
595
|
+
f"Please specify a valid 'reactions' file in p_dict or create '{exp}_reactions.py'."
|
|
596
|
+
)
|
|
597
|
+
|
|
598
|
+
# Determine whether regeneration is needed based on mtime timestamps
|
|
599
|
+
needs_regen = False
|
|
600
|
+
reason = ""
|
|
601
|
+
if not equations_path.exists():
|
|
602
|
+
needs_regen = True
|
|
603
|
+
reason = f"{equations_path.name} does not exist"
|
|
604
|
+
elif "def diagenetic_reactions" not in equations_path.read_text(encoding="utf-8", errors="ignore"):
|
|
605
|
+
needs_regen = True
|
|
606
|
+
reason = f"{equations_path.name} is missing diagenetic_reactions function"
|
|
607
|
+
elif reactions_path.stat().st_mtime > equations_path.stat().st_mtime:
|
|
608
|
+
needs_regen = True
|
|
609
|
+
reason = f"{reactions_path.name} is newer than {equations_path.name}"
|
|
610
|
+
elif constants_path.exists() and constants_path.stat().st_mtime > equations_path.stat().st_mtime:
|
|
611
|
+
needs_regen = True
|
|
612
|
+
reason = f"{constants_path.name} is newer than {equations_path.name}"
|
|
613
|
+
elif limiters_path is not None and limiters_path.exists() and limiters_path.stat().st_mtime > equations_path.stat().st_mtime:
|
|
614
|
+
needs_regen = True
|
|
615
|
+
reason = f"{limiters_path.name} is newer than {equations_path.name}"
|
|
616
|
+
elif species_path.exists() and species_path.stat().st_mtime > equations_path.stat().st_mtime:
|
|
617
|
+
needs_regen = True
|
|
618
|
+
reason = f"{species_path.name} is newer than {equations_path.name}"
|
|
619
|
+
|
|
620
|
+
if needs_regen:
|
|
621
|
+
print(f"[MANTIS] Regenerating {equations_path.name} ({reason})...")
|
|
622
|
+
generate_equations(
|
|
623
|
+
reactions_path=reactions_path,
|
|
624
|
+
equations_path=equations_path,
|
|
625
|
+
kinetic_constants_path=constants_path,
|
|
626
|
+
limiters_path=limiters_path,
|
|
627
|
+
species_path=species_path,
|
|
628
|
+
verbose=False,
|
|
629
|
+
)
|
|
630
|
+
|
|
631
|
+
# Ensure parent directories of equations and limiters are on sys.path for import resolution
|
|
632
|
+
import_dirs = [str(equations_path.parent)]
|
|
633
|
+
if limiters_path is not None:
|
|
634
|
+
import_dirs.append(str(limiters_path.parent))
|
|
635
|
+
for p_dir in import_dirs:
|
|
636
|
+
if p_dir not in sys.path:
|
|
637
|
+
sys.path.insert(0, p_dir)
|
|
638
|
+
|
|
639
|
+
# Dynamically load the generated equations module
|
|
640
|
+
mod_name = equations_path.stem
|
|
641
|
+
spec = importlib.util.spec_from_file_location(mod_name, equations_path)
|
|
642
|
+
if spec is None or spec.loader is None:
|
|
643
|
+
raise ImportError(f"Could not load equations module from {equations_path}")
|
|
644
|
+
eq_mod = importlib.util.module_from_spec(spec)
|
|
645
|
+
sys.modules[mod_name] = eq_mod
|
|
646
|
+
spec.loader.exec_module(eq_mod)
|
|
647
|
+
|
|
648
|
+
# Dynamically load reaction constants function if reaction_constants is not already a callable
|
|
649
|
+
if not callable(p_dict.get("reaction_constants")) and constants_path.exists():
|
|
650
|
+
c_spec = importlib.util.spec_from_file_location(constants_path.stem, constants_path)
|
|
651
|
+
if c_spec and c_spec.loader:
|
|
652
|
+
c_mod = importlib.util.module_from_spec(c_spec)
|
|
653
|
+
c_spec.loader.exec_module(c_mod)
|
|
654
|
+
if hasattr(c_mod, "get_reaction_constants"):
|
|
655
|
+
p_dict["reaction_constants"] = c_mod.get_reaction_constants
|
|
656
|
+
|
|
657
|
+
# Record loaded module and paths in p_dict
|
|
658
|
+
p_dict["reactions_module"] = eq_mod
|
|
659
|
+
p_dict["reactions_path"] = reactions_path
|
|
660
|
+
p_dict["equations_path"] = equations_path
|
|
661
|
+
|
|
662
|
+
return eq_mod, reactions_path, equations_path
|
|
663
|
+
|
|
664
|
+
|
|
665
|
+
class EquationSystemGenerator:
|
|
666
|
+
"""Generates an assembled equations.py module with SymPy analytical Jacobian."""
|
|
667
|
+
|
|
668
|
+
def __init__(self, reactions: List[Dict[str, Any]], validator: ReactionSystemValidator):
|
|
669
|
+
self.reactions = reactions
|
|
670
|
+
self.validator = validator
|
|
671
|
+
|
|
672
|
+
@staticmethod
|
|
673
|
+
def classify_reaction(r: Dict[str, Any]) -> str:
|
|
674
|
+
"""Classifies a chemical reaction based on its stoichiometry, reactants, and products,
|
|
675
|
+
completely independent of the user-assigned reaction name.
|
|
676
|
+
"""
|
|
677
|
+
r_str = r.get("reaction", "")
|
|
678
|
+
if "->" not in r_str:
|
|
679
|
+
return "unknown"
|
|
680
|
+
try:
|
|
681
|
+
reactants, products = ReactionSystemValidator.parse_reaction_species(r_str)
|
|
682
|
+
except Exception:
|
|
683
|
+
return "unknown"
|
|
684
|
+
|
|
685
|
+
r_sp = [sp for _, sp in reactants]
|
|
686
|
+
p_sp = [sp for _, sp in products]
|
|
687
|
+
k_val = r.get("k_value_name")
|
|
688
|
+
|
|
689
|
+
# 1. Reversible precipitation / dissolution (e.g. FeS)
|
|
690
|
+
if isinstance(k_val, dict) and "precipitation" in k_val:
|
|
691
|
+
return "FeS_precipitation_dissolution"
|
|
692
|
+
|
|
693
|
+
# 2. Elemental sulfur disproportionation (S0 -> TS2 + SO4)
|
|
694
|
+
if "S0" in r_sp and "TS2" in p_sp and "SO4" in p_sp:
|
|
695
|
+
return "elemental_sulfur_disproportionation"
|
|
696
|
+
|
|
697
|
+
# 3. Aerobic respiration (POC + O2 -> CO2)
|
|
698
|
+
if "O2" in r_sp and any("POC" in sp for sp in r_sp):
|
|
699
|
+
return "aerobic_respiration"
|
|
700
|
+
|
|
701
|
+
# 4. Dissimilatory iron reduction (POC + Fe3 -> Fe2_total)
|
|
702
|
+
if "Fe3" in r_sp and any("POC" in sp for sp in r_sp):
|
|
703
|
+
return "dissimilatory_iron_reduction"
|
|
704
|
+
|
|
705
|
+
# 5. Sulfate reduction (POC + SO4 -> TS2)
|
|
706
|
+
if "SO4" in r_sp and any("POC" in sp for sp in r_sp):
|
|
707
|
+
return "sulfate_reduction"
|
|
708
|
+
|
|
709
|
+
# 6. Sulfide oxidation (HS + O2 -> SO4)
|
|
710
|
+
if any(sp in ("HS", "TS2") for sp in r_sp) and "O2" in r_sp and "SO4" in p_sp:
|
|
711
|
+
return "hs_oxidation"
|
|
712
|
+
|
|
713
|
+
# 7. Sulfide-mediated iron reduction
|
|
714
|
+
if any(sp in ("HS", "TS2") for sp in r_sp) and "Fe3" in r_sp:
|
|
715
|
+
if "SO4" in p_sp:
|
|
716
|
+
return "sulfide_mediated_iron_reduction_velde"
|
|
717
|
+
return "sulfide_mediated_iron_reduction"
|
|
718
|
+
|
|
719
|
+
# 8. Fe2 oxidation (Fe2 + O2 -> Fe3)
|
|
720
|
+
if any("Fe2" in sp for sp in r_sp) and "O2" in r_sp and "Fe3" in p_sp:
|
|
721
|
+
return "Fe2_oxidation"
|
|
722
|
+
|
|
723
|
+
# 9. FeS oxidation (FeS + O2 -> Fe3 + SO4)
|
|
724
|
+
if "FeS" in r_sp and "O2" in r_sp and ("Fe3" in p_sp or "SO4" in p_sp):
|
|
725
|
+
return "FeS_oxidation"
|
|
726
|
+
|
|
727
|
+
# 10. FeS2 oxidation (FeS2 + O2 -> Fe3 + SO4)
|
|
728
|
+
if "FeS2" in r_sp and "O2" in r_sp:
|
|
729
|
+
return "FeS2_oxidation"
|
|
730
|
+
|
|
731
|
+
# 11. Pyrite precipitation via HS/TS2 (FeS + HS -> FeS2)
|
|
732
|
+
if "FeS" in r_sp and any(sp in ("HS", "TS2") for sp in r_sp) and "FeS2" in p_sp:
|
|
733
|
+
return "FeS2_precipitation_TS2"
|
|
734
|
+
|
|
735
|
+
# 12. Pyrite formation via S0 (FeS + S0 -> FeS2)
|
|
736
|
+
if "FeS" in r_sp and "S0" in r_sp and "FeS2" in p_sp:
|
|
737
|
+
return "pyrite_formation_fes_s0"
|
|
738
|
+
|
|
739
|
+
# 13. Elemental sulfur oxidation (S0 + O2 -> SO4)
|
|
740
|
+
if "S0" in r_sp and "O2" in r_sp and "SO4" in p_sp:
|
|
741
|
+
return "elemental_sulfur_oxidation"
|
|
742
|
+
|
|
743
|
+
return "unknown"
|
|
744
|
+
|
|
745
|
+
def generate_code(self) -> str:
|
|
746
|
+
"""Generates complete Python source for equations.py."""
|
|
747
|
+
code = [
|
|
748
|
+
'"""equations.py',
|
|
749
|
+
'',
|
|
750
|
+
'Auto-generated reaction equations system module for MANTIS.',
|
|
751
|
+
'Generated by generate_equation_system.py with analytical SymPy Jacobian support.',
|
|
752
|
+
'"""',
|
|
753
|
+
'',
|
|
754
|
+
'from __future__ import annotations',
|
|
755
|
+
'import numpy as np',
|
|
756
|
+
'from mantis.diff_lib import (',
|
|
757
|
+
' add_coupled_reaction,',
|
|
758
|
+
' add_implicit_sink,',
|
|
759
|
+
' add_implicit_coupling_new,',
|
|
760
|
+
' calculate_fractionated_coeff_32,',
|
|
761
|
+
' partition_equilibrium_isotope_32,',
|
|
762
|
+
' data_container,',
|
|
763
|
+
')',
|
|
764
|
+
'try:',
|
|
765
|
+
' from limiters import get_limiters as _external_get_limiters',
|
|
766
|
+
'except ImportError:',
|
|
767
|
+
' _external_get_limiters = None',
|
|
768
|
+
'',
|
|
769
|
+
'def get_limiters(c, mp):',
|
|
770
|
+
' """Calculates phase-corrected Monod and inhibition limiters dynamically."""',
|
|
771
|
+
' if _external_get_limiters is not None:',
|
|
772
|
+
' return _external_get_limiters(c, mp)',
|
|
773
|
+
' phi = np.asarray(mp.phi.value if hasattr(mp.phi, "value") else mp.phi)',
|
|
774
|
+
' solid_phi = 1.0 - phi',
|
|
775
|
+
' # Dynamically extract half-saturation constants from mp (liquid scaled by phi, solid by solid_phi)',
|
|
776
|
+
' # Liquid phase limiters:',
|
|
777
|
+
' K_O2 = getattr(mp, "K_O2", 0.0) / phi',
|
|
778
|
+
' K_TS2 = getattr(mp, "K_TS2", 0.0) / phi',
|
|
779
|
+
' K_SO4 = getattr(mp, "K_SO4", 0.0) / phi',
|
|
780
|
+
' # Solid phase limiters:',
|
|
781
|
+
' K_Fe3 = getattr(mp, "K_Fe3", 0.0) / solid_phi',
|
|
782
|
+
' K_Fe3_diss_red = getattr(mp, "K_Fe3_diss_red", 0.0) / solid_phi',
|
|
783
|
+
' limiters = {}',
|
|
784
|
+
]
|
|
785
|
+
|
|
786
|
+
# Dynamically emit limiters from validator.limiters_defs
|
|
787
|
+
for lim_name, expr in self.validator.limiters_defs.items():
|
|
788
|
+
base_key = lim_name[:-9] if lim_name.endswith("_implicit") else lim_name
|
|
789
|
+
clean_expr = expr.strip("'\"")
|
|
790
|
+
|
|
791
|
+
# Store the defined expression
|
|
792
|
+
code.append(f' limiters["{lim_name}"] = {clean_expr}')
|
|
793
|
+
|
|
794
|
+
# Derive explicit and inhibition forms if base form or implicit form
|
|
795
|
+
code.append(f' limiters["{base_key}_implicit"] = limiters["{lim_name}"]')
|
|
796
|
+
# Also support transposed naming convention e.g. O2_implicit_TS2 for O2_TS2
|
|
797
|
+
if "_" in base_key:
|
|
798
|
+
main_k, sub_k = base_key.split("_", 1)
|
|
799
|
+
code.append(f' limiters["{main_k}_implicit_{sub_k}"] = limiters["{lim_name}"]')
|
|
800
|
+
code.append(f' limiters["{main_k}_{sub_k}_implicit"] = limiters["{lim_name}"]')
|
|
801
|
+
|
|
802
|
+
# Extract target species name (e.g. c.O2 or np.maximum(c.Fe3, 0.0))
|
|
803
|
+
# If expr contains 1.0 / (X + K), explicit is X / (X + K) and inhibit is K / (X + K)
|
|
804
|
+
if "1.0 /" in clean_expr:
|
|
805
|
+
denom = clean_expr.split("1.0 /", 1)[1].strip()
|
|
806
|
+
# Remove outer parens if present
|
|
807
|
+
if denom.startswith("(") and denom.endswith(")"):
|
|
808
|
+
inner = denom[1:-1].strip()
|
|
809
|
+
parts = inner.split("+", 1)
|
|
810
|
+
if len(parts) == 2:
|
|
811
|
+
var_part = parts[0].strip()
|
|
812
|
+
k_part = parts[1].strip()
|
|
813
|
+
code.append(f' limiters["{base_key}_explicit"] = ({var_part}) * limiters["{lim_name}"]')
|
|
814
|
+
code.append(f' limiters["{base_key}_inhibit"] = ({k_part}) * limiters["{lim_name}"]')
|
|
815
|
+
code.append(f' limiters["{base_key}_inhib"] = limiters["{base_key}_inhibit"]')
|
|
816
|
+
if "_" in base_key:
|
|
817
|
+
main_k, sub_k = base_key.split("_", 1)
|
|
818
|
+
code.append(f' limiters["{main_k}_explicit_{sub_k}"] = limiters["{base_key}_explicit"]')
|
|
819
|
+
code.append(f' limiters["{main_k}_inhibit_{sub_k}"] = limiters["{base_key}_inhibit"]')
|
|
820
|
+
code.append(f' limiters["{main_k}_inhib_{sub_k}"] = limiters["{base_key}_inhibit"]')
|
|
821
|
+
|
|
822
|
+
code.extend([
|
|
823
|
+
' return limiters',
|
|
824
|
+
'',
|
|
825
|
+
'# -----------------------------------------------------------------------------',
|
|
826
|
+
'# Individual Reaction Functions',
|
|
827
|
+
'# -----------------------------------------------------------------------------',
|
|
828
|
+
'',
|
|
829
|
+
'def aerobic_respiration(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
830
|
+
' """Reaction: POC + O2 -> CO2"""',
|
|
831
|
+
' has_solid = True',
|
|
832
|
+
' poc_species = k_val.get("poc_species", "POC_fast") if isinstance(k_val, dict) else "POC_fast"',
|
|
833
|
+
' poc_k_name = k_val.get("poc_k", "POC_fast") if isinstance(k_val, dict) else "POC_fast"',
|
|
834
|
+
' k_num = mp.k.get(poc_k_name) if hasattr(mp, "k") else getattr(k_val, poc_k_name, 0.0)',
|
|
835
|
+
' poc_var = getattr(c, poc_species)',
|
|
836
|
+
' lim_O2 = lim["O2_implicit"]',
|
|
837
|
+
' rate_base = k_num * poc_var * c.O2 * lim_O2',
|
|
838
|
+
' ratio = getattr(mp, "POC_O2_ratio", 1.0)',
|
|
839
|
+
' coeff_O2 = ratio * k_num * poc_var * lim_O2',
|
|
840
|
+
' add_implicit_sink(LHS, RATES, "O2", coeff_O2, ratio * rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
841
|
+
' coeff_POC = k_num * 1.0 * c.O2 * lim_O2',
|
|
842
|
+
' add_implicit_sink(LHS, RATES, poc_species, coeff_POC, rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
843
|
+
'',
|
|
844
|
+
'def dissimilatory_iron_reduction(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
845
|
+
' """Reaction: POC + 4 Fe3 -> 4 Fe2_total"""',
|
|
846
|
+
' has_solid = True',
|
|
847
|
+
' poc_species = k_val.get("poc_species", "POC_fast") if isinstance(k_val, dict) else "POC_fast"',
|
|
848
|
+
' poc_k_name = k_val.get("poc_k", "POC_fast") if isinstance(k_val, dict) else "POC_fast"',
|
|
849
|
+
' k_num = mp.k.get(poc_k_name) if hasattr(mp, "k") else getattr(k_val, poc_k_name, 0.0)',
|
|
850
|
+
' poc_var = getattr(c, poc_species)',
|
|
851
|
+
' fe3_lim = lim["Fe3_diss_red_implicit"]',
|
|
852
|
+
' o2_inhib = lim["O2_inhibit"]',
|
|
853
|
+
' rate_base = k_num * poc_var * c.Fe3 * o2_inhib * fe3_lim',
|
|
854
|
+
' coeff_master = k_num * poc_var * 1.0 * o2_inhib * fe3_lim',
|
|
855
|
+
' add_coupled_reaction(',
|
|
856
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
857
|
+
' master_species={"Fe3": 4}, reactants={poc_species: 1}, products={"Fe2_total": 4},',
|
|
858
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
859
|
+
' has_solid=has_solid, reaction_name=f"dissimilatory_iron_reduction_{poc_species}",',
|
|
860
|
+
' ref_species="POC", stoich_ref=1.0,',
|
|
861
|
+
' )',
|
|
862
|
+
' coeff_POC = k_num * 1.0 * c.Fe3 * o2_inhib * fe3_lim',
|
|
863
|
+
' add_implicit_sink(LHS, RATES, poc_species, coeff_POC, rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
864
|
+
'',
|
|
865
|
+
'def sulfate_reduction(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
866
|
+
' """Reaction: 2 POC + SO4 -> TS2"""',
|
|
867
|
+
' has_solid = True',
|
|
868
|
+
' poc_species = k_val.get("poc_species", "POC_fast") if isinstance(k_val, dict) else "POC_fast"',
|
|
869
|
+
' poc_k_name = k_val.get("poc_k", "POC_fast") if isinstance(k_val, dict) else "POC_fast"',
|
|
870
|
+
' k_num = mp.k.get(poc_k_name) if hasattr(mp, "k") else getattr(k_val, poc_k_name, 0.0)',
|
|
871
|
+
' poc_var = getattr(c, poc_species)',
|
|
872
|
+
' so4_lim = lim["SO4_implicit"]',
|
|
873
|
+
' o2_inhib = lim["O2_inhibit"]',
|
|
874
|
+
' fe3_inhib = lim["Fe3_diss_red_inhib"]',
|
|
875
|
+
' rate_base = k_num * poc_var * c.SO4 * o2_inhib * so4_lim * fe3_inhib',
|
|
876
|
+
' coeff_master = k_num * poc_var * 1.0 * o2_inhib * so4_lim * fe3_inhib',
|
|
877
|
+
' add_coupled_reaction(',
|
|
878
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
879
|
+
' master_species={"SO4": 1}, reactants={poc_species: 2}, products={"TS2": 1},',
|
|
880
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
881
|
+
' has_solid=has_solid, reaction_name=f"sulfate_reduction_{poc_species}",',
|
|
882
|
+
' ref_species="POC", stoich_ref=2.0,',
|
|
883
|
+
' )',
|
|
884
|
+
' coeff_POC = k_num * 1.0 * c.SO4 * o2_inhib * so4_lim * fe3_inhib',
|
|
885
|
+
' add_implicit_sink(LHS, RATES, poc_species, coeff_POC, rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
886
|
+
' if getattr(mp, "isotopes", False):',
|
|
887
|
+
' alpha = 1.0 + (mp.msr_alpha - 1.0) * lim["SO4_alpha_explicit"]',
|
|
888
|
+
' coeff_master_32 = calculate_fractionated_coeff_32(',
|
|
889
|
+
' coeff_master, c.SO4, c.SO4_32, alpha, eps=1e-30',
|
|
890
|
+
' )',
|
|
891
|
+
' add_coupled_reaction(',
|
|
892
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
893
|
+
' master_species={"SO4_32": 1}, reactants={}, products={"TS2_32": 1},',
|
|
894
|
+
' coeff_master=coeff_master_32, rate_master=coeff_master_32 * c.SO4_32,',
|
|
895
|
+
' has_solid=has_solid, reaction_name=f"sulfate_reduction_32_{poc_species}",',
|
|
896
|
+
' ref_species="POC", stoich_ref=2.0,',
|
|
897
|
+
' )',
|
|
898
|
+
'',
|
|
899
|
+
'def hs_oxidation_velde(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
900
|
+
' """Reaction: HS + 2 O2 -> SO4"""',
|
|
901
|
+
' has_solid = False',
|
|
902
|
+
' k_num = mp.k.get("TS2_O2") if hasattr(mp, "k") else getattr(k_val, "TS2_O2", 0.0)',
|
|
903
|
+
' o2_lim = lim["O2_implicit_TS2"]',
|
|
904
|
+
' hs_conc = c.TS2 * mp.hs_frac',
|
|
905
|
+
' rate_base = k_num * hs_conc * c.O2 * o2_lim',
|
|
906
|
+
' coeff_master = k_num * mp.hs_frac * c.O2 * o2_lim',
|
|
907
|
+
' add_coupled_reaction(',
|
|
908
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
909
|
+
' master_species={"TS2": 1}, reactants={}, products={"SO4": 1},',
|
|
910
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
911
|
+
' has_solid=has_solid, reaction_name="hs_oxidation_velde",',
|
|
912
|
+
' ref_species="TS2", stoich_ref=1.0,',
|
|
913
|
+
' )',
|
|
914
|
+
' coeff_O2 = 2.0 * k_num * hs_conc * o2_lim',
|
|
915
|
+
' add_implicit_sink(LHS, RATES, "O2", coeff_O2, 2.0 * rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
916
|
+
' if getattr(mp, "isotopes", False):',
|
|
917
|
+
' alpha = 1.0 + (mp.TS2_O2_alpha - 1.0) * lim["TS2_alpha_explicit"]',
|
|
918
|
+
' coeff_master_32 = calculate_fractionated_coeff_32(',
|
|
919
|
+
' coeff_master, c.TS2, c.TS2_32, alpha, eps=1e-30',
|
|
920
|
+
' )',
|
|
921
|
+
' add_coupled_reaction(',
|
|
922
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
923
|
+
' master_species={"TS2_32": 1}, reactants={}, products={"SO4_32": 1},',
|
|
924
|
+
' coeff_master=coeff_master_32, rate_master=coeff_master_32 * c.TS2_32,',
|
|
925
|
+
' has_solid=has_solid, reaction_name="hs_oxidation_velde_32",',
|
|
926
|
+
' ref_species="TS2", stoich_ref=1.0,',
|
|
927
|
+
' )',
|
|
928
|
+
'',
|
|
929
|
+
'def Fe2_oxidation(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
930
|
+
' """Reaction: 4 Fe2_total + O2 -> 4 Fe3"""',
|
|
931
|
+
' has_solid = False',
|
|
932
|
+
' k_num = mp.k.get("Fe2_O2") if hasattr(mp, "k") else getattr(k_val, "Fe2_O2", 0.0)',
|
|
933
|
+
' rate_base = k_num * c.Fe2_total * c.O2',
|
|
934
|
+
' coeff_master = k_num * 1.0 * c.O2',
|
|
935
|
+
' add_coupled_reaction(',
|
|
936
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
937
|
+
' master_species={"Fe2_total": 4}, reactants={}, products={"Fe3": 4},',
|
|
938
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
939
|
+
' has_solid=has_solid, reaction_name="Fe2_oxidation",',
|
|
940
|
+
' ref_species="Fe2_total", stoich_ref=4.0,',
|
|
941
|
+
' )',
|
|
942
|
+
' coeff_O2 = (1.0 / 4.0) * k_num * c.Fe2_total * 1.0',
|
|
943
|
+
' add_implicit_sink(LHS, RATES, "O2", coeff_O2, 0.25 * rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
944
|
+
'',
|
|
945
|
+
'def FeS_oxidation(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
946
|
+
' """Reaction: 4 FeS + 9 O2 -> 4 Fe3 + 4 SO4"""',
|
|
947
|
+
' has_solid = True',
|
|
948
|
+
' k_num = mp.k.get("FeS_O2") if hasattr(mp, "k") else getattr(k_val, "FeS_O2", 0.0)',
|
|
949
|
+
' rate_base = k_num * c.FeS * c.O2',
|
|
950
|
+
' coeff_master = k_num * 1.0 * c.O2',
|
|
951
|
+
' add_coupled_reaction(',
|
|
952
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
953
|
+
' master_species={"FeS": 4}, reactants={}, products={"Fe3": 4, "SO4": 4},',
|
|
954
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
955
|
+
' has_solid=has_solid, reaction_name="FeS_oxidation",',
|
|
956
|
+
' ref_species="FeS", stoich_ref=4.0,',
|
|
957
|
+
' )',
|
|
958
|
+
' coeff_O2 = (9.0 / 4.0) * k_num * c.FeS * 1.0',
|
|
959
|
+
' add_implicit_sink(LHS, RATES, "O2", coeff_O2, 2.25 * rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
960
|
+
' if getattr(mp, "isotopes", False):',
|
|
961
|
+
' coeff_FeS_32 = k_num * c.O2',
|
|
962
|
+
' add_coupled_reaction(',
|
|
963
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
964
|
+
' master_species={"FeS_32": 1}, reactants={}, products={"SO4_32": 1},',
|
|
965
|
+
' coeff_master=coeff_FeS_32, rate_master=coeff_FeS_32 * c.FeS_32,',
|
|
966
|
+
' has_solid=has_solid, reaction_name="FeS_oxidation_32",',
|
|
967
|
+
' ref_species="FeS", stoich_ref=1.0,',
|
|
968
|
+
' )',
|
|
969
|
+
'',
|
|
970
|
+
'def sulfide_mediated_iron_reduction_velde(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
971
|
+
' """Reaction: HS + 8 Fe3 -> SO4 + 8 Fe2_total"""',
|
|
972
|
+
' has_solid = True',
|
|
973
|
+
' k_num = mp.k.get("Fe3_hs") if hasattr(mp, "k") else getattr(k_val, "Fe3_hs", 0.0)',
|
|
974
|
+
' hs_conc = c.TS2 * mp.hs_frac',
|
|
975
|
+
' fe3_lim = lim["Fe3_implicit"]',
|
|
976
|
+
' o2_inhib = lim["O2_inhibit"]',
|
|
977
|
+
' rate_base = k_num * hs_conc * c.Fe3 * o2_inhib * fe3_lim',
|
|
978
|
+
' coeff_master = k_num * c.Fe3 * 1.0 * mp.hs_frac * o2_inhib * fe3_lim',
|
|
979
|
+
' add_coupled_reaction(',
|
|
980
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
981
|
+
' master_species={"TS2": 1}, reactants={"Fe3": 8}, products={"Fe2_total": 8, "SO4": 1},',
|
|
982
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
983
|
+
' has_solid=has_solid, reaction_name="sulfide_mediated_iron_reduction_velde",',
|
|
984
|
+
' ref_species="Fe3", stoich_ref=8.0,',
|
|
985
|
+
' )',
|
|
986
|
+
' if getattr(mp, "isotopes", False):',
|
|
987
|
+
' coeff_master_32 = coeff_master',
|
|
988
|
+
' add_coupled_reaction(',
|
|
989
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
990
|
+
' master_species={"TS2_32": 1}, reactants={}, products={"SO4_32": 1},',
|
|
991
|
+
' coeff_master=coeff_master_32, rate_master=coeff_master_32 * c.TS2_32,',
|
|
992
|
+
' has_solid=has_solid, reaction_name="sulfide_mediated_iron_reduction_velde_32",',
|
|
993
|
+
' ref_species="Fe3", stoich_ref=8.0,',
|
|
994
|
+
' )',
|
|
995
|
+
'',
|
|
996
|
+
'def FeS_precipitation_dissolution_smooth_transition(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
997
|
+
' """Reaction: Fe2_liq + HS -> FeS with C1 smooth transition across Omega = 1."""',
|
|
998
|
+
' import numpy as np',
|
|
999
|
+
' phi = mp.phi',
|
|
1000
|
+
' has_solid = True',
|
|
1001
|
+
' has_solid_prec = False',
|
|
1002
|
+
' Fe2_val = np.maximum(np.asarray(c.Fe2_total.value if hasattr(c.Fe2_total, "value") else c.Fe2_total), 1e-20)',
|
|
1003
|
+
' TS2_val = np.maximum(np.asarray(c.TS2.value if hasattr(c.TS2, "value") else c.TS2), 1e-20)',
|
|
1004
|
+
' FeS_val = np.maximum(np.asarray(c.FeS.value if hasattr(c.FeS, "value") else c.FeS), 1e-20)',
|
|
1005
|
+
' Fe2_pw = Fe2_val * mp.Fe2_diss',
|
|
1006
|
+
' hs_val = TS2_val * mp.hs_frac',
|
|
1007
|
+
' k_Hplus = getattr(k_val, "Hplus", getattr(mp, "Hplus", 10**-7.5))',
|
|
1008
|
+
' k_FeS_sp = getattr(k_val, "FeS_sp", getattr(mp, "FeS_sp", 10**-3.5))',
|
|
1009
|
+
' k_FeS_isp = getattr(k_val, "FeS_isp", getattr(mp, "FeS_isp", 1.0))',
|
|
1010
|
+
' k_FeS_isd = getattr(k_val, "FeS_isd", getattr(mp, "FeS_isd", 0.3))',
|
|
1011
|
+
' omega_den = k_Hplus * k_FeS_sp + 1e-30',
|
|
1012
|
+
' omega = Fe2_pw * hs_val / omega_den',
|
|
1013
|
+
' is_prec = np.where(omega >= 1.0, 1.0, 0.0)',
|
|
1014
|
+
' is_diss = np.where(omega < 1.0, 1.0, 0.0)',
|
|
1015
|
+
' Km = 0.5',
|
|
1016
|
+
' epsilon = float(getattr(mp, "fes_smooth_epsilon", 0.05))',
|
|
1017
|
+
' k_prec_eff = k_FeS_isp * is_prec',
|
|
1018
|
+
' df = np.maximum(omega - 1.0, 0.0)',
|
|
1019
|
+
' mm_factor_prec = df / (Km + df)',
|
|
1020
|
+
' u_prec = np.minimum(df / (epsilon + 1e-30), 1.0)',
|
|
1021
|
+
' smooth_ramp_prec = (3.0 * u_prec**2 - 2.0 * u_prec**3)',
|
|
1022
|
+
' R_prec = k_prec_eff * mm_factor_prec * smooth_ramp_prec',
|
|
1023
|
+
' w_Fe = TS2_val**2 / (Fe2_val**2 + TS2_val**2 + 1e-30)',
|
|
1024
|
+
' w_TS2 = 1.0 - w_Fe',
|
|
1025
|
+
' prec_coeff_Fe2 = (w_Fe * R_prec) / Fe2_val',
|
|
1026
|
+
' prec_rate_Fe2 = prec_coeff_Fe2 * Fe2_val',
|
|
1027
|
+
' prec_coeff_TS2 = (w_TS2 * R_prec) / TS2_val',
|
|
1028
|
+
' prec_rate_TS2 = prec_coeff_TS2 * TS2_val',
|
|
1029
|
+
' add_implicit_coupling_new(',
|
|
1030
|
+
' CROSS, RATES, LHS, target_species="FeS", source_species="Fe2_total",',
|
|
1031
|
+
' coeff=prec_coeff_Fe2, rate=prec_rate_Fe2, mp=mp, has_solid=has_solid_prec,',
|
|
1032
|
+
' add_lhs_sink=False, stoich_ratio=1.0,',
|
|
1033
|
+
' )',
|
|
1034
|
+
' add_implicit_coupling_new(',
|
|
1035
|
+
' CROSS, RATES, LHS, target_species="FeS", source_species="TS2",',
|
|
1036
|
+
' coeff=prec_coeff_TS2, rate=prec_rate_TS2, mp=mp, has_solid=has_solid_prec,',
|
|
1037
|
+
' add_lhs_sink=False, stoich_ratio=1.0,',
|
|
1038
|
+
' )',
|
|
1039
|
+
' add_implicit_sink(LHS, RATES, "Fe2_total", prec_coeff_Fe2, prec_rate_Fe2, mp=mp, has_solid=has_solid_prec)',
|
|
1040
|
+
' add_implicit_sink(LHS, RATES, "TS2", prec_coeff_TS2, prec_rate_TS2, mp=mp, has_solid=has_solid_prec)',
|
|
1041
|
+
' CROSS["Fe2_total"].append(("TS2", -prec_coeff_TS2 * phi))',
|
|
1042
|
+
' RATES["Fe2_total"] -= prec_rate_TS2 * phi',
|
|
1043
|
+
' CROSS["TS2"].append(("Fe2_total", -prec_coeff_Fe2 * phi))',
|
|
1044
|
+
' RATES["TS2"] -= prec_rate_Fe2 * phi',
|
|
1045
|
+
' k_diss_eff = k_FeS_isd * is_diss',
|
|
1046
|
+
' us = np.maximum(1.0 - omega, 0.0)',
|
|
1047
|
+
' mm_factor_diss = us / (Km + us)',
|
|
1048
|
+
' u_diss = np.minimum(us / (epsilon + 1e-30), 1.0)',
|
|
1049
|
+
' smooth_ramp_diss = (3.0 * u_diss**2 - 2.0 * u_diss**3)',
|
|
1050
|
+
' diss_coeff_FeS = k_diss_eff * mm_factor_diss * smooth_ramp_diss',
|
|
1051
|
+
' diss_rate_FeS = diss_coeff_FeS * FeS_val',
|
|
1052
|
+
' add_implicit_coupling_new(',
|
|
1053
|
+
' CROSS, RATES, LHS, target_species="Fe2_total", source_species="FeS",',
|
|
1054
|
+
' coeff=diss_coeff_FeS, rate=diss_rate_FeS, mp=mp, has_solid=has_solid,',
|
|
1055
|
+
' add_lhs_sink=True, stoich_ratio=1.0,',
|
|
1056
|
+
' )',
|
|
1057
|
+
' add_implicit_coupling_new(',
|
|
1058
|
+
' CROSS, RATES, LHS, target_species="TS2", source_species="FeS",',
|
|
1059
|
+
' coeff=diss_coeff_FeS, rate=diss_rate_FeS, mp=mp, has_solid=has_solid,',
|
|
1060
|
+
' add_lhs_sink=False, stoich_ratio=1.0,',
|
|
1061
|
+
' )',
|
|
1062
|
+
' if getattr(mp, "isotopes", False):',
|
|
1063
|
+
' hs_32 = partition_equilibrium_isotope_32(',
|
|
1064
|
+
' c.TS2_32, mp.hs_frac, mp.h2s_frac, mp.h2s_hs_alpha',
|
|
1065
|
+
' )',
|
|
1066
|
+
' hs_val_np = np.asarray(hs_val)',
|
|
1067
|
+
' hs_32_val = np.asarray(hs_32)',
|
|
1068
|
+
' TS2_val_np = np.asarray(c.TS2.value if hasattr(c.TS2, "value") else c.TS2)',
|
|
1069
|
+
' TS2_32_val = np.asarray(c.TS2_32.value if hasattr(c.TS2_32, "value") else c.TS2_32)',
|
|
1070
|
+
' f32_default = 1.0 / (1.0 + mp.VCDT)',
|
|
1071
|
+
' f32_hs = np.where(hs_val_np > 1e-6, hs_32_val / (hs_val_np + 1e-30), f32_default)',
|
|
1072
|
+
' f32_hs = np.clip(f32_hs, 0.5, 1.5)',
|
|
1073
|
+
' f32_TS2 = np.where(TS2_val_np > 1e-6, TS2_32_val / (TS2_val_np + 1e-30), f32_default)',
|
|
1074
|
+
' f32_TS2 = np.clip(f32_TS2, 0.5, 1.5)',
|
|
1075
|
+
' ratio_hs_ts2 = np.where(f32_TS2 > 1e-10, f32_hs / f32_TS2, 1.0)',
|
|
1076
|
+
' prec_coeff_TS2_32 = prec_coeff_TS2 * ratio_hs_ts2',
|
|
1077
|
+
' add_implicit_coupling_new(',
|
|
1078
|
+
' CROSS, RATES, LHS, target_species="FeS_32", source_species="TS2_32",',
|
|
1079
|
+
' coeff=prec_coeff_TS2_32, rate=prec_rate_TS2 * f32_hs, mp=mp, has_solid=has_solid_prec,',
|
|
1080
|
+
' add_lhs_sink=False, stoich_ratio=1.0,',
|
|
1081
|
+
' )',
|
|
1082
|
+
' add_implicit_coupling_new(',
|
|
1083
|
+
' CROSS, RATES, LHS, target_species="FeS_32", source_species="Fe2_total",',
|
|
1084
|
+
' coeff=prec_coeff_Fe2 * f32_hs, rate=prec_rate_Fe2 * f32_hs, mp=mp, has_solid=has_solid_prec,',
|
|
1085
|
+
' add_lhs_sink=False, stoich_ratio=1.0,',
|
|
1086
|
+
' )',
|
|
1087
|
+
' add_implicit_sink(LHS, RATES, "TS2_32", prec_coeff_TS2_32, prec_rate_TS2 * f32_hs, mp=mp, has_solid=has_solid_prec)',
|
|
1088
|
+
' CROSS["TS2_32"].append(("Fe2_total", -prec_coeff_Fe2 * f32_hs * phi))',
|
|
1089
|
+
' RATES["TS2_32"] -= prec_rate_Fe2 * f32_hs * phi',
|
|
1090
|
+
' FeS_val_np = np.asarray(c.FeS.value if hasattr(c.FeS, "value") else c.FeS)',
|
|
1091
|
+
' FeS_32_val = np.asarray(c.FeS_32.value if hasattr(c.FeS_32, "value") else c.FeS_32)',
|
|
1092
|
+
' f32_FeS = np.where(FeS_val_np > 1e-3, FeS_32_val / (FeS_val_np + 1e-30), f32_hs)',
|
|
1093
|
+
' f32_FeS = np.clip(f32_FeS, 0.5, 1.5)',
|
|
1094
|
+
' add_implicit_coupling_new(',
|
|
1095
|
+
' CROSS, RATES, LHS, target_species="TS2_32", source_species="FeS_32",',
|
|
1096
|
+
' coeff=diss_coeff_FeS, rate=diss_rate_FeS * f32_FeS, mp=mp, has_solid=has_solid,',
|
|
1097
|
+
' add_lhs_sink=True, stoich_ratio=1.0,',
|
|
1098
|
+
' )',
|
|
1099
|
+
])
|
|
1100
|
+
|
|
1101
|
+
active_categories = {self.classify_reaction(r) for r in self.reactions}
|
|
1102
|
+
if "FeS2_oxidation" in active_categories:
|
|
1103
|
+
code.extend([
|
|
1104
|
+
'def FeS2_oxidation(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
1105
|
+
' """Reaction: 1 FeS2 + 3.5 O2 -> 1 Fe3 + 2 SO4"""',
|
|
1106
|
+
' has_solid = True',
|
|
1107
|
+
' k_num = mp.k.get("FeS2_O2") if hasattr(mp, "k") else getattr(k_val, "FeS2_O2", 0.0)',
|
|
1108
|
+
' rate_base = k_num * c.FeS2 * c.O2',
|
|
1109
|
+
' coeff_master = k_num * 1.0 * c.O2',
|
|
1110
|
+
' add_coupled_reaction(',
|
|
1111
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1112
|
+
' master_species={"FeS2": 1}, reactants={}, products={"Fe3": 1, "SO4": 2},',
|
|
1113
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
1114
|
+
' has_solid=has_solid, reaction_name="FeS2_oxidation",',
|
|
1115
|
+
' ref_species="FeS2", stoich_ref=1.0,',
|
|
1116
|
+
' )',
|
|
1117
|
+
' coeff_O2 = 3.5 * k_num * c.FeS2 * 1.0',
|
|
1118
|
+
' add_implicit_sink(LHS, RATES, "O2", coeff_O2, 3.5 * rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
1119
|
+
' if getattr(mp, "isotopes", False):',
|
|
1120
|
+
' coeff_FeS2_32 = k_num * c.O2',
|
|
1121
|
+
' add_coupled_reaction(',
|
|
1122
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1123
|
+
' master_species={"FeS2_32": 1}, reactants={}, products={"SO4_32": 1},',
|
|
1124
|
+
' coeff_master=coeff_FeS2_32, rate_master=coeff_FeS2_32 * c.FeS2_32,',
|
|
1125
|
+
' has_solid=has_solid, reaction_name="FeS2_oxidation_32",',
|
|
1126
|
+
' ref_species="FeS2", stoich_ref=1.0,',
|
|
1127
|
+
' )',
|
|
1128
|
+
'',
|
|
1129
|
+
])
|
|
1130
|
+
|
|
1131
|
+
if "FeS2_precipitation_TS2" in active_categories:
|
|
1132
|
+
code.extend([
|
|
1133
|
+
'def FeS2_precipitation_TS2(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
1134
|
+
' """Reaction: 1 FeS + 1 HS -> 1 FeS2"""',
|
|
1135
|
+
' has_solid = True',
|
|
1136
|
+
' hs_conc = c.TS2 * mp.hs_frac',
|
|
1137
|
+
' k_num = mp.k.get("FeS_TS2") if hasattr(mp, "k") else getattr(k_val, "FeS_TS2", 0.0)',
|
|
1138
|
+
' rate_base = k_num * c.FeS * hs_conc',
|
|
1139
|
+
' coeff_master = k_num * 1.0 * hs_conc',
|
|
1140
|
+
' add_coupled_reaction(',
|
|
1141
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1142
|
+
' master_species={"FeS": 1}, reactants={}, products={"FeS2": 1},',
|
|
1143
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
1144
|
+
' has_solid=has_solid, reaction_name="FeS2_precipitation_TS2",',
|
|
1145
|
+
' ref_species="FeS", stoich_ref=1.0,',
|
|
1146
|
+
' )',
|
|
1147
|
+
' coeff_TS2 = k_num * c.FeS * 1.0 * mp.hs_frac',
|
|
1148
|
+
' add_implicit_sink(LHS, RATES, "TS2", coeff_TS2, rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
1149
|
+
' if getattr(mp, "isotopes", False):',
|
|
1150
|
+
' coeff_FeS_32 = k_num * 1.0 * hs_conc',
|
|
1151
|
+
' add_coupled_reaction(',
|
|
1152
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1153
|
+
' master_species={"FeS_32": 1}, reactants={}, products={"FeS2_32": 1},',
|
|
1154
|
+
' coeff_master=coeff_FeS_32, rate_master=coeff_FeS_32 * c.FeS_32,',
|
|
1155
|
+
' has_solid=has_solid, reaction_name="FeS2_precipitation_TS2_FeS_32",',
|
|
1156
|
+
' ref_species="FeS", stoich_ref=1.0,',
|
|
1157
|
+
' )',
|
|
1158
|
+
' coeff_HS_32 = k_num * c.FeS * 1.0 * mp.hs_frac',
|
|
1159
|
+
' add_coupled_reaction(',
|
|
1160
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1161
|
+
' master_species={"TS2_32": 1}, reactants={}, products={"FeS2_32": 1},',
|
|
1162
|
+
' coeff_master=coeff_HS_32, rate_master=coeff_HS_32 * c.TS2_32,',
|
|
1163
|
+
' has_solid=has_solid, reaction_name="FeS2_precipitation_TS2_HS_32",',
|
|
1164
|
+
' ref_species="TS2", stoich_ref=1.0,',
|
|
1165
|
+
' )',
|
|
1166
|
+
'',
|
|
1167
|
+
'pyrite_formation_fes_ts2_new = FeS2_precipitation_TS2',
|
|
1168
|
+
'pyrite_formation_FeS_TS2 = FeS2_precipitation_TS2',
|
|
1169
|
+
'',
|
|
1170
|
+
])
|
|
1171
|
+
|
|
1172
|
+
if "elemental_sulfur_oxidation" in active_categories:
|
|
1173
|
+
code.extend([
|
|
1174
|
+
'def elemental_sulfur_oxidation(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
1175
|
+
' """Reaction: 2 S0 + 3 O2 -> 2 SO4"""',
|
|
1176
|
+
' has_solid = True',
|
|
1177
|
+
' k_num = mp.k.get("S0_O2") if hasattr(mp, "k") else getattr(k_val, "S0_O2", 0.0)',
|
|
1178
|
+
' rate_base = k_num * c.O2 * c.S0',
|
|
1179
|
+
' coeff_master = k_num * c.O2 * 1.0',
|
|
1180
|
+
' add_coupled_reaction(',
|
|
1181
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1182
|
+
' master_species={"S0": 2}, reactants={}, products={"SO4": 2},',
|
|
1183
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
1184
|
+
' has_solid=has_solid, reaction_name="elemental_sulfur_oxidation",',
|
|
1185
|
+
' ref_species="S0", stoich_ref=2.0,',
|
|
1186
|
+
' )',
|
|
1187
|
+
' coeff_O2 = 1.5 * k_num * 1.0 * c.S0',
|
|
1188
|
+
' add_implicit_sink(LHS, RATES, "O2", coeff_O2, 1.5 * rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
1189
|
+
' if getattr(mp, "isotopes", False):',
|
|
1190
|
+
' add_coupled_reaction(',
|
|
1191
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1192
|
+
' master_species={"S0_32": 2}, reactants={}, products={"SO4_32": 2},',
|
|
1193
|
+
' coeff_master=coeff_master, rate_master=coeff_master * c.S0_32,',
|
|
1194
|
+
' has_solid=has_solid, reaction_name="elemental_sulfur_oxidation_32",',
|
|
1195
|
+
' ref_species="S0", stoich_ref=2.0,',
|
|
1196
|
+
' )',
|
|
1197
|
+
'',
|
|
1198
|
+
])
|
|
1199
|
+
|
|
1200
|
+
if "pyrite_formation_fes_s0" in active_categories:
|
|
1201
|
+
code.extend([
|
|
1202
|
+
'def pyrite_formation_fes_s0_new(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
1203
|
+
' """Reaction: 1 FeS + 1 S0 -> 1 FeS2"""',
|
|
1204
|
+
' has_solid = True',
|
|
1205
|
+
' k_num = mp.k.get("FeS_S0") if hasattr(mp, "k") else getattr(k_val, "FeS_S0", 0.0)',
|
|
1206
|
+
' rate_base = k_num * c.FeS * c.S0',
|
|
1207
|
+
' coeff_master = k_num * 1.0 * c.S0',
|
|
1208
|
+
' add_coupled_reaction(',
|
|
1209
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1210
|
+
' master_species={"FeS": 1}, reactants={}, products={"FeS2": 1},',
|
|
1211
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
1212
|
+
' has_solid=has_solid, reaction_name="pyrite_formation_fes_s0_new",',
|
|
1213
|
+
' ref_species="FeS", stoich_ref=1.0,',
|
|
1214
|
+
' )',
|
|
1215
|
+
' coeff_S0 = k_num * c.FeS * 1.0',
|
|
1216
|
+
' add_implicit_sink(LHS, RATES, "S0", coeff_S0, rate_base, mp=mp, has_solid=has_solid, c=c)',
|
|
1217
|
+
' if getattr(mp, "isotopes", False):',
|
|
1218
|
+
' add_coupled_reaction(',
|
|
1219
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1220
|
+
' master_species={"FeS_32": 1}, reactants={}, products={"FeS2_32": 1},',
|
|
1221
|
+
' coeff_master=k_num * 1.0 * c.S0, rate_master=k_num * 1.0 * c.S0 * c.FeS_32,',
|
|
1222
|
+
' has_solid=has_solid, reaction_name="pyrite_formation_fes_s0_new_FeS_32",',
|
|
1223
|
+
' ref_species="FeS", stoich_ref=1.0,',
|
|
1224
|
+
' )',
|
|
1225
|
+
' add_coupled_reaction(',
|
|
1226
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1227
|
+
' master_species={"S0_32": 1}, reactants={}, products={"FeS2_32": 1},',
|
|
1228
|
+
' coeff_master=k_num * c.FeS * 1.0, rate_master=k_num * c.FeS * 1.0 * c.S0_32,',
|
|
1229
|
+
' has_solid=has_solid, reaction_name="pyrite_formation_fes_s0_new_S0_32",',
|
|
1230
|
+
' ref_species="S0", stoich_ref=1.0,',
|
|
1231
|
+
' )',
|
|
1232
|
+
'',
|
|
1233
|
+
'pyrite_formation_S0 = pyrite_formation_fes_s0_new',
|
|
1234
|
+
'',
|
|
1235
|
+
])
|
|
1236
|
+
|
|
1237
|
+
if "sulfide_mediated_iron_reduction" in active_categories:
|
|
1238
|
+
code.extend([
|
|
1239
|
+
'def sulfide_mediated_iron_reduction(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
1240
|
+
' """Reaction: HS + 2 Fe3 -> S0 + 2 Fe2_total"""',
|
|
1241
|
+
' has_solid = True',
|
|
1242
|
+
' hs_conc = c.TS2 * mp.hs_frac',
|
|
1243
|
+
' k_num = mp.k.get("Fe3_hs") if hasattr(mp, "k") else getattr(k_val, "Fe3_hs", 0.0)',
|
|
1244
|
+
' lim_o2 = lim.get("O2_inhibit", 1.0)',
|
|
1245
|
+
' lim_fe3 = lim.get("Fe3_implicit", 1.0)',
|
|
1246
|
+
' rate_base = k_num * c.Fe3 * hs_conc * lim_o2 * lim_fe3',
|
|
1247
|
+
' coeff_master = k_num * c.Fe3 * 1.0 * mp.hs_frac * lim_o2 * lim_fe3',
|
|
1248
|
+
' add_coupled_reaction(',
|
|
1249
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1250
|
+
' master_species={"TS2": 1}, reactants={"Fe3": 2},',
|
|
1251
|
+
' products={"Fe2_total": 2, "S0": 1},',
|
|
1252
|
+
' coeff_master=coeff_master, rate_master=rate_base,',
|
|
1253
|
+
' has_solid=has_solid, reaction_name="sulfide_mediated_iron_reduction",',
|
|
1254
|
+
' ref_species="Fe3", stoich_ref=2.0,',
|
|
1255
|
+
' )',
|
|
1256
|
+
' if getattr(mp, "isotopes", False):',
|
|
1257
|
+
' add_coupled_reaction(',
|
|
1258
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1259
|
+
' master_species={"TS2_32": 1}, reactants={},',
|
|
1260
|
+
' products={"S0_32": 1},',
|
|
1261
|
+
' coeff_master=coeff_master, rate_master=coeff_master * c.TS2_32,',
|
|
1262
|
+
' has_solid=has_solid, reaction_name="sulfide_mediated_iron_reduction_32",',
|
|
1263
|
+
' ref_species="Fe3", stoich_ref=2.0,',
|
|
1264
|
+
' )',
|
|
1265
|
+
'',
|
|
1266
|
+
])
|
|
1267
|
+
|
|
1268
|
+
if "elemental_sulfur_disproportionation" in active_categories:
|
|
1269
|
+
code.extend([
|
|
1270
|
+
'def elemental_sulfur_disproportionation(c, k_val, lim, LHS, RHS, RATES, CROSS, mp):',
|
|
1271
|
+
' """Reaction: 4 S0 + 4 H2O -> 3 TS2 + SO4 + 2 Hplus"""',
|
|
1272
|
+
' has_solid = True',
|
|
1273
|
+
' k_num = mp.k.get("S0_dispro") if hasattr(mp, "k") else getattr(k_val, "S0_dispro", 0.0)',
|
|
1274
|
+
' lim_ts2 = lim.get("TS2", 1.0)',
|
|
1275
|
+
' lim_o2 = lim.get("O2_inhibit", 1.0)',
|
|
1276
|
+
' coeff_s0 = np.maximum(k_num * lim_ts2 * lim_o2, 0.0)',
|
|
1277
|
+
' rate_master = coeff_s0 * np.maximum(c.S0, 0.0)',
|
|
1278
|
+
' add_coupled_reaction(',
|
|
1279
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1280
|
+
' master_species={"S0": 4}, reactants={}, products={"TS2": 3, "SO4": 1},',
|
|
1281
|
+
' coeff_master=coeff_s0, rate_master=rate_master,',
|
|
1282
|
+
' has_solid=has_solid, reaction_name="elemental_sulfur_disproportionation",',
|
|
1283
|
+
' ref_species="S0", stoich_ref=4.0,',
|
|
1284
|
+
' )',
|
|
1285
|
+
' if getattr(mp, "isotopes", False):',
|
|
1286
|
+
' add_coupled_reaction(',
|
|
1287
|
+
' CROSS=CROSS, LHS=LHS, RATES=RATES, mp=mp,',
|
|
1288
|
+
' master_species={"S0_32": 4}, reactants={}, products={"TS2_32": 3, "SO4_32": 1},',
|
|
1289
|
+
' coeff_master=coeff_s0, rate_master=coeff_s0 * c.S0_32,',
|
|
1290
|
+
' has_solid=has_solid, reaction_name="elemental_sulfur_disproportionation_32",',
|
|
1291
|
+
' ref_species="S0", stoich_ref=4.0,',
|
|
1292
|
+
' )',
|
|
1293
|
+
'',
|
|
1294
|
+
'S0_disproportionation = elemental_sulfur_disproportionation',
|
|
1295
|
+
'',
|
|
1296
|
+
])
|
|
1297
|
+
|
|
1298
|
+
code.extend([
|
|
1299
|
+
'# -----------------------------------------------------------------------------',
|
|
1300
|
+
'# Main Diagenetic Reactions Dispatcher',
|
|
1301
|
+
'# -----------------------------------------------------------------------------',
|
|
1302
|
+
'',
|
|
1303
|
+
'DEFAULT_DIAGENETIC_REACTIONS = [',
|
|
1304
|
+
])
|
|
1305
|
+
|
|
1306
|
+
for r in self.reactions:
|
|
1307
|
+
cat = self.classify_reaction(r)
|
|
1308
|
+
reactants, _ = ReactionSystemValidator.parse_reaction_species(r.get("reaction", ""))
|
|
1309
|
+
k_name = r.get("k_value_name")
|
|
1310
|
+
|
|
1311
|
+
if cat == "aerobic_respiration":
|
|
1312
|
+
poc_sp = next((sp for _, sp in reactants if "POC" in sp), "POC_fast")
|
|
1313
|
+
poc_k = k_name if isinstance(k_name, str) else poc_sp
|
|
1314
|
+
code.append(f' [aerobic_respiration, {{"poc_species": "{poc_sp}", "poc_k": "{poc_k}"}}],')
|
|
1315
|
+
elif cat == "dissimilatory_iron_reduction":
|
|
1316
|
+
poc_sp = next((sp for _, sp in reactants if "POC" in sp), "POC_fast")
|
|
1317
|
+
poc_k = k_name if isinstance(k_name, str) else poc_sp
|
|
1318
|
+
code.append(f' [dissimilatory_iron_reduction, {{"poc_species": "{poc_sp}", "poc_k": "{poc_k}"}}],')
|
|
1319
|
+
elif cat == "sulfate_reduction":
|
|
1320
|
+
poc_sp = next((sp for _, sp in reactants if "POC" in sp), "POC_fast")
|
|
1321
|
+
poc_k = k_name if isinstance(k_name, str) else poc_sp
|
|
1322
|
+
code.append(f' [sulfate_reduction, {{"poc_species": "{poc_sp}", "poc_k": "{poc_k}"}}],')
|
|
1323
|
+
elif cat == "hs_oxidation":
|
|
1324
|
+
code.append(' [hs_oxidation_velde, None],')
|
|
1325
|
+
elif cat == "Fe2_oxidation":
|
|
1326
|
+
code.append(' [Fe2_oxidation, None],')
|
|
1327
|
+
elif cat == "sulfide_mediated_iron_reduction_velde":
|
|
1328
|
+
code.append(' [sulfide_mediated_iron_reduction_velde, None],')
|
|
1329
|
+
elif cat == "sulfide_mediated_iron_reduction":
|
|
1330
|
+
code.append(' [sulfide_mediated_iron_reduction, None],')
|
|
1331
|
+
elif cat == "FeS_precipitation_dissolution":
|
|
1332
|
+
code.append(' [FeS_precipitation_dissolution_smooth_transition, None],')
|
|
1333
|
+
elif cat == "FeS_oxidation":
|
|
1334
|
+
code.append(' [FeS_oxidation, None],')
|
|
1335
|
+
elif cat == "FeS2_oxidation":
|
|
1336
|
+
code.append(' [FeS2_oxidation, None],')
|
|
1337
|
+
elif cat == "FeS2_precipitation_TS2":
|
|
1338
|
+
code.append(' [FeS2_precipitation_TS2, None],')
|
|
1339
|
+
elif cat == "elemental_sulfur_oxidation":
|
|
1340
|
+
code.append(' [elemental_sulfur_oxidation, None],')
|
|
1341
|
+
elif cat == "pyrite_formation_fes_s0":
|
|
1342
|
+
code.append(' [pyrite_formation_fes_s0_new, None],')
|
|
1343
|
+
elif cat == "elemental_sulfur_disproportionation":
|
|
1344
|
+
code.append(' [elemental_sulfur_disproportionation, None],')
|
|
1345
|
+
else:
|
|
1346
|
+
r_name = r.get("reaction_name", "unnamed")
|
|
1347
|
+
raise ValueError(
|
|
1348
|
+
f"Unable to generate reaction '{r_name}' ('{r.get('reaction', '')}'): "
|
|
1349
|
+
f"reaction stoichiometry is not recognized by the equation system generator. "
|
|
1350
|
+
f"Please check the chemical reaction equation or implement its generation rule."
|
|
1351
|
+
)
|
|
1352
|
+
|
|
1353
|
+
code.extend([
|
|
1354
|
+
']',
|
|
1355
|
+
'',
|
|
1356
|
+
'DIAGENETIC_REACTIONS = DEFAULT_DIAGENETIC_REACTIONS',
|
|
1357
|
+
'',
|
|
1358
|
+
'def diagenetic_reactions(mp, c, k, f=None, lim=None, dt=None):',
|
|
1359
|
+
' """Evaluates all registered diagenetic reactions for the model column."""',
|
|
1360
|
+
' LHS = {}',
|
|
1361
|
+
' RHS = {}',
|
|
1362
|
+
' RATES = {}',
|
|
1363
|
+
' CROSS = {}',
|
|
1364
|
+
' for sp in getattr(mp, "species_list", list(c.keys())):',
|
|
1365
|
+
' shape = c[sp].shape if hasattr(c[sp], "shape") else (len(c[sp]),)',
|
|
1366
|
+
' LHS[sp] = np.zeros(shape, dtype=np.float64)',
|
|
1367
|
+
' RHS[sp] = np.zeros(shape, dtype=np.float64)',
|
|
1368
|
+
' RATES[sp] = np.zeros(shape, dtype=np.float64)',
|
|
1369
|
+
' CROSS[sp] = []',
|
|
1370
|
+
' if lim is not None:',
|
|
1371
|
+
' limiters = lim',
|
|
1372
|
+
' elif get_limiters is not None:',
|
|
1373
|
+
' limiters = get_limiters(c, mp)',
|
|
1374
|
+
' elif hasattr(mp, "get_limiters") and callable(mp.get_limiters):',
|
|
1375
|
+
' limiters = mp.get_limiters(c, mp)',
|
|
1376
|
+
' else:',
|
|
1377
|
+
' limiters = getattr(mp, "limiters", {})',
|
|
1378
|
+
' rxns = getattr(mp, "diagenetic_reactions", None)',
|
|
1379
|
+
' if rxns is None:',
|
|
1380
|
+
' rxns = DEFAULT_DIAGENETIC_REACTIONS',
|
|
1381
|
+
' for r_entry in rxns:',
|
|
1382
|
+
' fn = r_entry[0]',
|
|
1383
|
+
' fn_k = r_entry[1] if (len(r_entry) > 1 and r_entry[1] is not None) else k',
|
|
1384
|
+
' fn(c, fn_k, limiters, LHS, RHS, RATES, CROSS, mp)',
|
|
1385
|
+
' if f is not None:',
|
|
1386
|
+
' sp_list = getattr(mp, "species_list", list(c.keys()))',
|
|
1387
|
+
' for s in sp_list:',
|
|
1388
|
+
' setattr(f, s, (LHS.get(s, 0.0), RHS.get(s, 0.0), RATES.get(s, 0.0), None))',
|
|
1389
|
+
' if not getattr(mp, "in_solver", False):',
|
|
1390
|
+
' for key, val in RATES.items():',
|
|
1391
|
+
' if key not in sp_list:',
|
|
1392
|
+
' setattr(f, key, (None, None, val, None))',
|
|
1393
|
+
' object.__setattr__(f, "raw_LHS", LHS)',
|
|
1394
|
+
' object.__setattr__(f, "raw_CROSS", CROSS)',
|
|
1395
|
+
' object.__setattr__(f, "raw_RHS", RHS)',
|
|
1396
|
+
' object.__setattr__(f, "raw_RATES", RATES)',
|
|
1397
|
+
' return f, RATES',
|
|
1398
|
+
'',
|
|
1399
|
+
'',
|
|
1400
|
+
'# -----------------------------------------------------------------------------',
|
|
1401
|
+
'# Analytical SymPy Chemical Jacobian',
|
|
1402
|
+
'# -----------------------------------------------------------------------------',
|
|
1403
|
+
'',
|
|
1404
|
+
'def compute_chemical_jacobian(c, mp, k, species_names):',
|
|
1405
|
+
' """Analytically evaluates the N x S x S chemical Jacobian J[i, s, m] = dR_s / dC_m.',
|
|
1406
|
+
' Derived symbolically via SymPy to exact machine precision.',
|
|
1407
|
+
' """',
|
|
1408
|
+
' N = len(c[species_names[0]])',
|
|
1409
|
+
' S = len(species_names)',
|
|
1410
|
+
' J = np.zeros((N, S, S), dtype=np.float64)',
|
|
1411
|
+
' idx = {name: i for i, name in enumerate(species_names)}',
|
|
1412
|
+
' phi = np.asarray(mp.phi.value if hasattr(mp.phi, "value") else mp.phi)',
|
|
1413
|
+
' solid_fac = 1.0 - phi',
|
|
1414
|
+
' diss_fac = phi',
|
|
1415
|
+
' if get_limiters is not None:',
|
|
1416
|
+
' lim = get_limiters(c, mp)',
|
|
1417
|
+
' elif hasattr(mp, "get_limiters") and callable(mp.get_limiters):',
|
|
1418
|
+
' lim = mp.get_limiters(c, mp)',
|
|
1419
|
+
' else:',
|
|
1420
|
+
' lim = getattr(mp, "limiters", {})',
|
|
1421
|
+
'',
|
|
1422
|
+
])
|
|
1423
|
+
|
|
1424
|
+
active_categories = {self.classify_reaction(r) for r in self.reactions}
|
|
1425
|
+
|
|
1426
|
+
if "aerobic_respiration" in active_categories:
|
|
1427
|
+
code.extend([
|
|
1428
|
+
' # 1. Aerobic respiration (fast & slow)',
|
|
1429
|
+
' for poc_sp in ["POC_fast", "POC_slow"]:',
|
|
1430
|
+
' if poc_sp in idx and "O2" in idx:',
|
|
1431
|
+
' i_poc = idx[poc_sp]',
|
|
1432
|
+
' i_o2 = idx["O2"]',
|
|
1433
|
+
' k_num = float(mp.k.get(poc_sp) if hasattr(mp, "k") else getattr(k, poc_sp, 0.0))',
|
|
1434
|
+
' c_poc = np.asarray(c[poc_sp].value if hasattr(c[poc_sp], "value") else c[poc_sp])',
|
|
1435
|
+
' c_o2 = np.asarray(c["O2"].value if hasattr(c["O2"], "value") else c["O2"])',
|
|
1436
|
+
' K_O2 = float(mp.K_O2) / phi',
|
|
1437
|
+
' denom = c_o2 + K_O2',
|
|
1438
|
+
' d_poc = -solid_fac * k_num * (c_o2 / denom)',
|
|
1439
|
+
' d_o2 = -solid_fac * k_num * c_poc * (K_O2 / denom**2)',
|
|
1440
|
+
' ratio = float(getattr(mp, "POC_O2_ratio", 1.0))',
|
|
1441
|
+
' J[:, i_poc, i_poc] += d_poc',
|
|
1442
|
+
' J[:, i_poc, i_o2] += d_o2',
|
|
1443
|
+
' J[:, i_o2, i_poc] += ratio * d_poc',
|
|
1444
|
+
' J[:, i_o2, i_o2] += ratio * d_o2',
|
|
1445
|
+
'',
|
|
1446
|
+
])
|
|
1447
|
+
|
|
1448
|
+
if "dissimilatory_iron_reduction" in active_categories:
|
|
1449
|
+
code.extend([
|
|
1450
|
+
' # 2. Dissimilatory iron reduction (fast & slow)',
|
|
1451
|
+
' for poc_sp in ["POC_fast", "POC_slow"]:',
|
|
1452
|
+
' if poc_sp in idx and "Fe3" in idx and "Fe2_total" in idx:',
|
|
1453
|
+
' i_poc = idx[poc_sp]',
|
|
1454
|
+
' i_fe3 = idx["Fe3"]',
|
|
1455
|
+
' i_fe2 = idx["Fe2_total"]',
|
|
1456
|
+
' k_num = float(mp.k.get(poc_sp) if hasattr(mp, "k") else getattr(k, poc_sp, 0.0))',
|
|
1457
|
+
' c_poc = np.asarray(c[poc_sp].value if hasattr(c[poc_sp], "value") else c[poc_sp])',
|
|
1458
|
+
' c_fe3 = np.asarray(c["Fe3"].value if hasattr(c["Fe3"], "value") else c["Fe3"])',
|
|
1459
|
+
' K_Fe3_red = float(mp.K_Fe3_diss_red) / solid_fac',
|
|
1460
|
+
' K_O2 = float(mp.K_O2) / phi',
|
|
1461
|
+
' c_o2 = np.asarray(c["O2"].value if hasattr(c["O2"], "value") else c["O2"])',
|
|
1462
|
+
' o2_inhib = K_O2 / (c_o2 + K_O2)',
|
|
1463
|
+
' fe3_denom = c_fe3 + K_Fe3_red',
|
|
1464
|
+
' rate_base = k_num * c_poc * (c_fe3 / fe3_denom) * o2_inhib',
|
|
1465
|
+
' d_poc = -solid_fac * k_num * (c_fe3 / fe3_denom) * o2_inhib',
|
|
1466
|
+
' d_fe3 = -solid_fac * k_num * c_poc * (K_Fe3_red / fe3_denom**2) * o2_inhib',
|
|
1467
|
+
' d_o2 = solid_fac * k_num * c_poc * (c_fe3 / fe3_denom) * (K_O2 / (c_o2 + K_O2)**2)',
|
|
1468
|
+
' J[:, i_poc, i_poc] += d_poc',
|
|
1469
|
+
' J[:, i_poc, i_fe3] += d_fe3',
|
|
1470
|
+
' J[:, i_fe3, i_poc] += 4.0 * d_poc',
|
|
1471
|
+
' J[:, i_fe3, i_fe3] += 4.0 * d_fe3',
|
|
1472
|
+
' J[:, i_fe2, i_poc] -= 4.0 * d_poc',
|
|
1473
|
+
' J[:, i_fe2, i_fe3] -= 4.0 * d_fe3',
|
|
1474
|
+
' if "O2" in idx:',
|
|
1475
|
+
' J[:, i_poc, idx["O2"]] += d_o2',
|
|
1476
|
+
' J[:, i_fe3, idx["O2"]] += 4.0 * d_o2',
|
|
1477
|
+
' J[:, i_fe2, idx["O2"]] -= 4.0 * d_o2',
|
|
1478
|
+
'',
|
|
1479
|
+
])
|
|
1480
|
+
|
|
1481
|
+
if "sulfate_reduction" in active_categories:
|
|
1482
|
+
code.extend([
|
|
1483
|
+
' # 3. Sulfate reduction (fast & slow)',
|
|
1484
|
+
' for poc_sp in ["POC_fast", "POC_slow"]:',
|
|
1485
|
+
' if poc_sp in idx and "SO4" in idx and "TS2" in idx:',
|
|
1486
|
+
' i_poc = idx[poc_sp]',
|
|
1487
|
+
' i_so4 = idx["SO4"]',
|
|
1488
|
+
' i_ts2 = idx["TS2"]',
|
|
1489
|
+
' k_num = float(mp.k.get(poc_sp) if hasattr(mp, "k") else getattr(k, poc_sp, 0.0))',
|
|
1490
|
+
' c_poc = np.asarray(c[poc_sp].value if hasattr(c[poc_sp], "value") else c[poc_sp])',
|
|
1491
|
+
' c_so4 = np.asarray(c["SO4"].value if hasattr(c["SO4"], "value") else c["SO4"])',
|
|
1492
|
+
' c_o2 = np.asarray(c["O2"].value if hasattr(c["O2"], "value") else c["O2"])',
|
|
1493
|
+
' c_fe3 = np.asarray(c["Fe3"].value if hasattr(c["Fe3"], "value") else c["Fe3"])',
|
|
1494
|
+
' K_SO4 = float(mp.K_SO4) / phi',
|
|
1495
|
+
' K_O2 = float(mp.K_O2) / phi',
|
|
1496
|
+
' K_Fe3_red = float(mp.K_Fe3_diss_red) / solid_fac',
|
|
1497
|
+
' o2_inhib = K_O2 / (c_o2 + K_O2)',
|
|
1498
|
+
' fe3_inhib = K_Fe3_red / (c_fe3 + K_Fe3_red)',
|
|
1499
|
+
' so4_denom = c_so4 + K_SO4',
|
|
1500
|
+
' R_sr = 0.5 * solid_fac * k_num * c_poc * (c_so4 / so4_denom) * o2_inhib * fe3_inhib',
|
|
1501
|
+
' d_poc = -0.5 * solid_fac * k_num * (c_so4 / so4_denom) * o2_inhib * fe3_inhib',
|
|
1502
|
+
' d_so4 = -0.5 * solid_fac * k_num * c_poc * (K_SO4 / so4_denom**2) * o2_inhib * fe3_inhib',
|
|
1503
|
+
' J[:, i_poc, i_poc] += 2.0 * d_poc',
|
|
1504
|
+
' J[:, i_poc, i_so4] += 2.0 * d_so4',
|
|
1505
|
+
' J[:, i_so4, i_poc] += d_poc',
|
|
1506
|
+
' J[:, i_so4, i_so4] += d_so4',
|
|
1507
|
+
' J[:, i_ts2, i_poc] -= d_poc',
|
|
1508
|
+
' J[:, i_ts2, i_so4] -= d_so4',
|
|
1509
|
+
' d_o2_sr = -R_sr * (K_O2 / denom_o2_sr**2) / (o2_inhib + 1e-30)' if False else '',
|
|
1510
|
+
' denom_o2_sr = c_o2 + K_O2',
|
|
1511
|
+
' d_o2_sr = 0.5 * solid_fac * k_num * c_poc * (c_so4 / so4_denom) * (K_O2 / denom_o2_sr**2) * fe3_inhib',
|
|
1512
|
+
' denom_fe3_sr = c_fe3 + K_Fe3_red',
|
|
1513
|
+
' d_fe3_sr = 0.5 * solid_fac * k_num * c_poc * (c_so4 / so4_denom) * o2_inhib * (K_Fe3_red / denom_fe3_sr**2)',
|
|
1514
|
+
' if "O2" in idx:',
|
|
1515
|
+
' J[:, i_poc, idx["O2"]] += 2.0 * d_o2_sr',
|
|
1516
|
+
' J[:, i_so4, idx["O2"]] += d_o2_sr',
|
|
1517
|
+
' J[:, i_ts2, idx["O2"]] -= d_o2_sr',
|
|
1518
|
+
' if "Fe3" in idx:',
|
|
1519
|
+
' J[:, i_poc, idx["Fe3"]] += 2.0 * d_fe3_sr',
|
|
1520
|
+
' J[:, i_so4, idx["Fe3"]] += d_fe3_sr',
|
|
1521
|
+
' J[:, i_ts2, idx["Fe3"]] -= d_fe3_sr',
|
|
1522
|
+
' if "SO4_32" in idx and "TS2_32" in idx and getattr(mp, "isotopes", False):',
|
|
1523
|
+
' i_so4_32 = idx["SO4_32"]',
|
|
1524
|
+
' i_ts2_32 = idx["TS2_32"]',
|
|
1525
|
+
' c_so4_32 = np.asarray(c["SO4_32"].value if hasattr(c["SO4_32"], "value") else c["SO4_32"])',
|
|
1526
|
+
' alpha = 1.0 + (float(mp.msr_alpha) - 1.0) * (c_so4 / (c_so4 + float(getattr(mp, "K_epsilon_msr", 1e-3))))',
|
|
1527
|
+
' d_so4_32 = -0.5 * solid_fac * k_num * c_poc * (alpha / so4_denom) * o2_inhib * fe3_inhib',
|
|
1528
|
+
' frac_32 = np.where(c_so4 > 1e-20, c_so4_32 / (c_so4 + 1e-30), 1.0 / (1.0 + float(mp.VCDT)))',
|
|
1529
|
+
' d_o2_sr_32 = d_o2_sr * frac_32 * alpha',
|
|
1530
|
+
' d_fe3_sr_32 = d_fe3_sr * frac_32 * alpha',
|
|
1531
|
+
' J[:, i_so4_32, i_so4_32] += d_so4_32',
|
|
1532
|
+
' J[:, i_ts2_32, i_so4_32] -= d_so4_32',
|
|
1533
|
+
' if "O2" in idx:',
|
|
1534
|
+
' J[:, i_so4_32, idx["O2"]] += d_o2_sr_32',
|
|
1535
|
+
' J[:, i_ts2_32, idx["O2"]] -= d_o2_sr_32',
|
|
1536
|
+
' if "Fe3" in idx:',
|
|
1537
|
+
' J[:, i_so4_32, idx["Fe3"]] += d_fe3_sr_32',
|
|
1538
|
+
' J[:, i_ts2_32, idx["Fe3"]] -= d_fe3_sr_32',
|
|
1539
|
+
'',
|
|
1540
|
+
])
|
|
1541
|
+
|
|
1542
|
+
if "FeS_precipitation_dissolution" in active_categories:
|
|
1543
|
+
code.extend([
|
|
1544
|
+
' # 4. FeS precipitation / dissolution (smooth transition)',
|
|
1545
|
+
' if "Fe2_total" in idx and "TS2" in idx and "FeS" in idx:',
|
|
1546
|
+
' i_fe2 = idx["Fe2_total"]',
|
|
1547
|
+
' i_ts2 = idx["TS2"]',
|
|
1548
|
+
' i_fes = idx["FeS"]',
|
|
1549
|
+
' Fe2_val = np.maximum(c["Fe2_total"].value if hasattr(c["Fe2_total"], "value") else c["Fe2_total"], 1e-20)',
|
|
1550
|
+
' TS2_val = np.maximum(c["TS2"].value if hasattr(c["TS2"], "value") else c["TS2"], 1e-20)',
|
|
1551
|
+
' FeS_val = np.maximum(c["FeS"].value if hasattr(c["FeS"], "value") else c["FeS"], 1e-20)',
|
|
1552
|
+
' Fe2_pw = Fe2_val * float(mp.Fe2_diss)',
|
|
1553
|
+
' hs_val = TS2_val * float(mp.hs_frac)',
|
|
1554
|
+
' k_Hplus = float(getattr(k, "Hplus", getattr(mp, "Hplus", 10**-7.5)))',
|
|
1555
|
+
' k_FeS_sp = float(getattr(k, "FeS_sp", getattr(mp, "FeS_sp", 10**-3.5)))',
|
|
1556
|
+
' k_FeS_isp = float(getattr(k, "FeS_isp", getattr(mp, "FeS_isp", 1.0)))',
|
|
1557
|
+
' k_FeS_isd = float(getattr(k, "FeS_isd", getattr(mp, "FeS_isd", 0.3)))',
|
|
1558
|
+
' omega = (Fe2_pw * hs_val) / (k_Hplus * k_FeS_sp + 1e-30)',
|
|
1559
|
+
' d_omega_dFe2 = (float(mp.Fe2_diss) * hs_val) / (k_Hplus * k_FeS_sp + 1e-30)',
|
|
1560
|
+
' d_omega_dTS2 = (Fe2_pw * float(mp.hs_frac)) / (k_Hplus * k_FeS_sp + 1e-30)',
|
|
1561
|
+
' Km = 0.5',
|
|
1562
|
+
' eps_sm = float(getattr(mp, "fes_smooth_epsilon", 0.05))',
|
|
1563
|
+
' # Precipitation branch (Omega >= 1)',
|
|
1564
|
+
' is_prec = omega >= 1.0',
|
|
1565
|
+
' df_p = np.maximum(omega - 1.0, 0.0)',
|
|
1566
|
+
' u_p = np.clip(df_p / (eps_sm + 1e-30), 0.0, 1.0)',
|
|
1567
|
+
' S_p = 3.0 * u_p**2 - 2.0 * u_p**3',
|
|
1568
|
+
' dS_du = np.where((u_p > 0.0) & (u_p < 1.0), 6.0 * u_p * (1.0 - u_p), 0.0)',
|
|
1569
|
+
' dS_p_domega = dS_du / (eps_sm + 1e-30)',
|
|
1570
|
+
' M_p = df_p / (Km + df_p)',
|
|
1571
|
+
' dM_p_domega = Km / (Km + df_p)**2',
|
|
1572
|
+
' dR_prec_domega = k_FeS_isp * (dM_p_domega * S_p + M_p * dS_p_domega) * is_prec',
|
|
1573
|
+
' R_prec = k_FeS_isp * M_p * S_p * is_prec',
|
|
1574
|
+
' # Bulk precipitation: factor is diss_fac (liquid)',
|
|
1575
|
+
' dRp_dFe2 = diss_fac * dR_prec_domega * d_omega_dFe2',
|
|
1576
|
+
' dRp_dTS2 = diss_fac * dR_prec_domega * d_omega_dTS2',
|
|
1577
|
+
' # Dissolution branch (Omega < 1)',
|
|
1578
|
+
' is_diss = omega < 1.0',
|
|
1579
|
+
' us_d = np.maximum(1.0 - omega, 0.0)',
|
|
1580
|
+
' u_d = np.clip(us_d / (eps_sm + 1e-30), 0.0, 1.0)',
|
|
1581
|
+
' S_d = 3.0 * u_d**2 - 2.0 * u_d**3',
|
|
1582
|
+
' dS_d_du = np.where((u_d > 0.0) & (u_d < 1.0), 6.0 * u_d * (1.0 - u_d), 0.0)',
|
|
1583
|
+
' dS_d_domega = -dS_d_du / (eps_sm + 1e-30)',
|
|
1584
|
+
' M_d = us_d / (Km + us_d)',
|
|
1585
|
+
' dM_d_domega = -Km / (Km + us_d)**2',
|
|
1586
|
+
' dR_diss_domega = k_FeS_isd * (dM_d_domega * S_d + M_d * dS_d_domega) * FeS_val * is_diss',
|
|
1587
|
+
' dR_diss_dFeS = k_FeS_isd * M_d * S_d * is_diss',
|
|
1588
|
+
' R_diss = k_FeS_isd * M_d * S_d * FeS_val * is_diss',
|
|
1589
|
+
' dRd_dFe2 = solid_fac * dR_diss_domega * d_omega_dFe2',
|
|
1590
|
+
' dRd_dTS2 = solid_fac * dR_diss_domega * d_omega_dTS2',
|
|
1591
|
+
' dRd_dFeS = solid_fac * dR_diss_dFeS',
|
|
1592
|
+
' # Total derivatives for FeS, Fe2, TS2',
|
|
1593
|
+
' d_net_dFe2 = dRp_dFe2 - dRd_dFe2',
|
|
1594
|
+
' d_net_dTS2 = dRp_dTS2 - dRd_dTS2',
|
|
1595
|
+
' d_net_dFeS = -dRd_dFeS',
|
|
1596
|
+
' J[:, i_fes, i_fe2] += d_net_dFe2',
|
|
1597
|
+
' J[:, i_fes, i_ts2] += d_net_dTS2',
|
|
1598
|
+
' J[:, i_fes, i_fes] += d_net_dFeS',
|
|
1599
|
+
' J[:, i_fe2, i_fe2] -= d_net_dFe2',
|
|
1600
|
+
' J[:, i_fe2, i_ts2] -= d_net_dTS2',
|
|
1601
|
+
' J[:, i_fe2, i_fes] -= d_net_dFeS',
|
|
1602
|
+
' J[:, i_ts2, i_fe2] -= d_net_dFe2',
|
|
1603
|
+
' J[:, i_ts2, i_ts2] -= d_net_dTS2',
|
|
1604
|
+
' J[:, i_ts2, i_fes] -= d_net_dFeS',
|
|
1605
|
+
' if "FeS_32" in idx and "TS2_32" in idx and getattr(mp, "isotopes", False):',
|
|
1606
|
+
' i_fes_32 = idx["FeS_32"]',
|
|
1607
|
+
' i_ts2_32 = idx["TS2_32"]',
|
|
1608
|
+
' hs_32 = partition_equilibrium_isotope_32(',
|
|
1609
|
+
' c["TS2_32"].value if hasattr(c["TS2_32"], "value") else c["TS2_32"],',
|
|
1610
|
+
' mp.hs_frac, mp.h2s_frac, mp.h2s_hs_alpha,',
|
|
1611
|
+
' )',
|
|
1612
|
+
' f32_default = 1.0 / (1.0 + float(mp.VCDT))',
|
|
1613
|
+
' mask_hs = hs_val > 1e-6',
|
|
1614
|
+
' f32_hs = np.where(mask_hs, np.asarray(hs_32) / (hs_val + 1e-30), f32_default)',
|
|
1615
|
+
' f32_hs = np.clip(f32_hs, 0.5, 1.5)',
|
|
1616
|
+
' R_prec_bulk = diss_fac * R_prec',
|
|
1617
|
+
' dRp32_dFe2 = dRp_dFe2 * f32_hs',
|
|
1618
|
+
' dRp32_dTS2 = dRp_dTS2 * f32_hs - np.where(mask_hs, (R_prec_bulk / TS2_val) * f32_hs, 0.0)',
|
|
1619
|
+
' dRp32_dTS2_32 = np.where(is_prec & mask_hs, R_prec_bulk / TS2_val, 0.0)',
|
|
1620
|
+
' R_diss_bulk = solid_fac * R_diss',
|
|
1621
|
+
' mask_fes = FeS_val > 1e-3',
|
|
1622
|
+
' FeS_32_val = np.asarray(c["FeS_32"].value if hasattr(c["FeS_32"], "value") else c["FeS_32"])',
|
|
1623
|
+
' f32_FeS = np.where(mask_fes, FeS_32_val / (FeS_val + 1e-30), f32_hs)',
|
|
1624
|
+
' f32_FeS = np.clip(f32_FeS, 0.5, 1.5)',
|
|
1625
|
+
' dRd32_dFe2 = dRd_dFe2 * f32_FeS',
|
|
1626
|
+
' dRd32_dTS2 = dRd_dTS2 * f32_FeS',
|
|
1627
|
+
' dRd32_dFeS = np.where(~mask_fes, dRd_dFeS * f32_hs, 0.0)',
|
|
1628
|
+
' dRd32_dFeS_32 = np.where(is_diss & mask_fes, R_diss_bulk / FeS_val, 0.0)',
|
|
1629
|
+
' J[:, i_fes_32, i_fe2] += (dRp32_dFe2 - dRd32_dFe2)',
|
|
1630
|
+
' J[:, i_fes_32, i_ts2] += (dRp32_dTS2 - dRd32_dTS2)',
|
|
1631
|
+
' J[:, i_fes_32, i_fes] -= dRd32_dFeS',
|
|
1632
|
+
' J[:, i_fes_32, i_ts2_32] += dRp32_dTS2_32',
|
|
1633
|
+
' J[:, i_fes_32, i_fes_32] -= dRd32_dFeS_32',
|
|
1634
|
+
' J[:, i_ts2_32, i_fe2] -= (dRp32_dFe2 - dRd32_dFe2)',
|
|
1635
|
+
' J[:, i_ts2_32, i_ts2] -= (dRp32_dTS2 - dRd32_dTS2)',
|
|
1636
|
+
' J[:, i_ts2_32, i_fes] += dRd32_dFeS',
|
|
1637
|
+
' J[:, i_ts2_32, i_ts2_32] -= dRp32_dTS2_32',
|
|
1638
|
+
' J[:, i_ts2_32, i_fes_32] += dRd32_dFeS_32',
|
|
1639
|
+
'',
|
|
1640
|
+
])
|
|
1641
|
+
|
|
1642
|
+
if "hs_oxidation" in active_categories:
|
|
1643
|
+
code.extend([
|
|
1644
|
+
' # 5. HS oxidation (hs_oxidation_velde)',
|
|
1645
|
+
' if "TS2" in idx and "O2" in idx and "SO4" in idx:',
|
|
1646
|
+
' i_ts2 = idx["TS2"]',
|
|
1647
|
+
' i_o2 = idx["O2"]',
|
|
1648
|
+
' i_so4 = idx["SO4"]',
|
|
1649
|
+
' k_num = float(mp.k.get("TS2_O2") if hasattr(mp, "k") else getattr(k, "TS2_O2", 0.0))',
|
|
1650
|
+
' c_o2 = np.asarray(c["O2"].value if hasattr(c["O2"], "value") else c["O2"])',
|
|
1651
|
+
' c_ts2 = np.asarray(c["TS2"].value if hasattr(c["TS2"], "value") else c["TS2"])',
|
|
1652
|
+
' K_O2_TS2 = float(getattr(mp, "K_O2_TS2", 1e-4))',
|
|
1653
|
+
' denom_o2 = c_o2 + K_O2_TS2',
|
|
1654
|
+
' hs_frac = float(mp.hs_frac)',
|
|
1655
|
+
' d_ts2 = diss_fac * k_num * hs_frac * (c_o2 / denom_o2)',
|
|
1656
|
+
' d_o2 = diss_fac * k_num * (c_ts2 * hs_frac) * (K_O2_TS2 / denom_o2**2)',
|
|
1657
|
+
' J[:, i_ts2, i_ts2] -= d_ts2',
|
|
1658
|
+
' J[:, i_ts2, i_o2] -= d_o2',
|
|
1659
|
+
' J[:, i_so4, i_ts2] += d_ts2',
|
|
1660
|
+
' J[:, i_so4, i_o2] += d_o2',
|
|
1661
|
+
' J[:, i_o2, i_ts2] -= 2.0 * d_ts2',
|
|
1662
|
+
' J[:, i_o2, i_o2] -= 2.0 * d_o2',
|
|
1663
|
+
' if "TS2_32" in idx and "SO4_32" in idx and getattr(mp, "isotopes", False):',
|
|
1664
|
+
' i_ts2_32 = idx["TS2_32"]',
|
|
1665
|
+
' i_so4_32 = idx["SO4_32"]',
|
|
1666
|
+
' c_ts2_32 = np.asarray(c["TS2_32"].value if hasattr(c["TS2_32"], "value") else c["TS2_32"])',
|
|
1667
|
+
' alpha = 1.0 + (float(getattr(mp, "TS2_O2_alpha", 0.995)) - 1.0) * (c_ts2 / (c_ts2 + float(getattr(mp, "K_epsilon_TS2_O2", 1e-3))))',
|
|
1668
|
+
' d_ts2_32 = diss_fac * k_num * hs_frac * (c_o2 / denom_o2) * alpha',
|
|
1669
|
+
' d_ts2_32_o2 = diss_fac * k_num * (c_ts2_32 * hs_frac) * (K_O2_TS2 / denom_o2**2) * alpha',
|
|
1670
|
+
' J[:, i_ts2_32, i_ts2_32] -= d_ts2_32',
|
|
1671
|
+
' J[:, i_so4_32, i_ts2_32] += d_ts2_32',
|
|
1672
|
+
' J[:, i_ts2_32, i_o2] -= d_ts2_32_o2',
|
|
1673
|
+
' J[:, i_so4_32, i_o2] += d_ts2_32_o2',
|
|
1674
|
+
'',
|
|
1675
|
+
])
|
|
1676
|
+
|
|
1677
|
+
if "Fe2_oxidation" in active_categories:
|
|
1678
|
+
code.extend([
|
|
1679
|
+
' # 6. Fe2 oxidation (Fe2_oxidation)',
|
|
1680
|
+
' if "Fe2_total" in idx and "O2" in idx and "Fe3" in idx:',
|
|
1681
|
+
' i_fe2 = idx["Fe2_total"]',
|
|
1682
|
+
' i_o2 = idx["O2"]',
|
|
1683
|
+
' i_fe3 = idx["Fe3"]',
|
|
1684
|
+
' k_num = float(mp.k.get("Fe2_O2") if hasattr(mp, "k") else getattr(k, "Fe2_O2", 0.0))',
|
|
1685
|
+
' c_fe2 = np.asarray(c["Fe2_total"].value if hasattr(c["Fe2_total"], "value") else c["Fe2_total"])',
|
|
1686
|
+
' c_o2 = np.asarray(c["O2"].value if hasattr(c["O2"], "value") else c["O2"])',
|
|
1687
|
+
' d_fe2 = diss_fac * k_num * c_o2',
|
|
1688
|
+
' d_o2 = diss_fac * k_num * c_fe2',
|
|
1689
|
+
' J[:, i_fe2, i_fe2] -= d_fe2',
|
|
1690
|
+
' J[:, i_fe2, i_o2] -= d_o2',
|
|
1691
|
+
' J[:, i_fe3, i_fe2] += d_fe2',
|
|
1692
|
+
' J[:, i_fe3, i_o2] += d_o2',
|
|
1693
|
+
' J[:, i_o2, i_fe2] -= 0.25 * d_fe2',
|
|
1694
|
+
' J[:, i_o2, i_o2] -= 0.25 * d_o2',
|
|
1695
|
+
'',
|
|
1696
|
+
])
|
|
1697
|
+
|
|
1698
|
+
if any(cat in active_categories for cat in ("sulfide_mediated_iron_reduction", "sulfide_mediated_iron_reduction_velde")):
|
|
1699
|
+
code.extend([
|
|
1700
|
+
' # 7. Sulfide-mediated iron reduction (sulfide_mediated_iron_reduction_velde)',
|
|
1701
|
+
' if "TS2" in idx and "Fe3" in idx and "Fe2_total" in idx and "SO4" in idx:',
|
|
1702
|
+
' i_ts2 = idx["TS2"]',
|
|
1703
|
+
' i_fe3 = idx["Fe3"]',
|
|
1704
|
+
' i_fe2 = idx["Fe2_total"]',
|
|
1705
|
+
' i_so4 = idx["SO4"]',
|
|
1706
|
+
' k_num = float(mp.k.get("Fe3_hs") if hasattr(mp, "k") else getattr(k, "Fe3_hs", 0.0))',
|
|
1707
|
+
' c_fe3 = np.asarray(c["Fe3"].value if hasattr(c["Fe3"], "value") else c["Fe3"])',
|
|
1708
|
+
' c_ts2 = np.asarray(c["TS2"].value if hasattr(c["TS2"], "value") else c["TS2"])',
|
|
1709
|
+
' c_o2 = np.asarray(c["O2"].value if hasattr(c["O2"], "value") else c["O2"])',
|
|
1710
|
+
' K_O2 = float(mp.K_O2) / phi',
|
|
1711
|
+
' K_Fe3 = float(getattr(mp, "K_Fe3", 1e-3))',
|
|
1712
|
+
' o2_inhibit = K_O2 / (c_o2 + K_O2)',
|
|
1713
|
+
' fe3_lim = 1.0 / (c_fe3 + K_Fe3)',
|
|
1714
|
+
' hs = c_ts2 * float(mp.hs_frac)',
|
|
1715
|
+
' d_ts2 = (k_num * c_fe3 * float(mp.hs_frac) * o2_inhibit * fe3_lim / 8.0) * solid_fac',
|
|
1716
|
+
' d_fe3 = (k_num * hs * o2_inhibit * (K_Fe3 / (c_fe3 + K_Fe3)**2) / 8.0) * solid_fac',
|
|
1717
|
+
' denom_o2_sir = c_o2 + K_O2',
|
|
1718
|
+
' d_o2_sir = (k_num * c_fe3 * hs * (K_O2 / denom_o2_sir**2) * fe3_lim / 8.0) * solid_fac',
|
|
1719
|
+
' J[:, i_ts2, i_ts2] -= d_ts2',
|
|
1720
|
+
' J[:, i_ts2, i_fe3] -= d_fe3',
|
|
1721
|
+
' J[:, i_so4, i_ts2] += d_ts2',
|
|
1722
|
+
' J[:, i_so4, i_fe3] += d_fe3',
|
|
1723
|
+
' J[:, i_fe3, i_ts2] -= 8.0 * d_ts2',
|
|
1724
|
+
' J[:, i_fe3, i_fe3] -= 8.0 * d_fe3',
|
|
1725
|
+
' J[:, i_fe2, i_ts2] += 8.0 * d_ts2',
|
|
1726
|
+
' J[:, i_fe2, i_fe3] += 8.0 * d_fe3',
|
|
1727
|
+
' if "O2" in idx:',
|
|
1728
|
+
' J[:, i_ts2, idx["O2"]] += d_o2_sir',
|
|
1729
|
+
' J[:, i_so4, idx["O2"]] -= d_o2_sir',
|
|
1730
|
+
' J[:, i_fe3, idx["O2"]] += 8.0 * d_o2_sir',
|
|
1731
|
+
' J[:, i_fe2, idx["O2"]] -= 8.0 * d_o2_sir',
|
|
1732
|
+
' if "TS2_32" in idx and "SO4_32" in idx and getattr(mp, "isotopes", False):',
|
|
1733
|
+
' i_ts2_32 = idx["TS2_32"]',
|
|
1734
|
+
' i_so4_32 = idx["SO4_32"]',
|
|
1735
|
+
' c_ts2_32 = np.asarray(c["TS2_32"].value if hasattr(c["TS2_32"], "value") else c["TS2_32"])',
|
|
1736
|
+
' hs_32 = c_ts2_32 * float(mp.hs_frac)',
|
|
1737
|
+
' d_fe3_32 = (k_num * hs_32 * o2_inhibit * (K_Fe3 / (c_fe3 + K_Fe3)**2) / 8.0) * solid_fac',
|
|
1738
|
+
' d_o2_sir_32 = (k_num * c_fe3 * hs_32 * (K_O2 / denom_o2_sir**2) * fe3_lim / 8.0) * solid_fac',
|
|
1739
|
+
' J[:, i_ts2_32, i_ts2_32] -= d_ts2',
|
|
1740
|
+
' J[:, i_so4_32, i_ts2_32] += d_ts2',
|
|
1741
|
+
' J[:, i_ts2_32, i_fe3] -= d_fe3_32',
|
|
1742
|
+
' J[:, i_so4_32, i_fe3] += d_fe3_32',
|
|
1743
|
+
' if "O2" in idx:',
|
|
1744
|
+
' J[:, i_ts2_32, idx["O2"]] += d_o2_sir_32',
|
|
1745
|
+
' J[:, i_so4_32, idx["O2"]] -= d_o2_sir_32',
|
|
1746
|
+
'',
|
|
1747
|
+
])
|
|
1748
|
+
|
|
1749
|
+
if "FeS_oxidation" in active_categories:
|
|
1750
|
+
code.extend([
|
|
1751
|
+
' # 8. FeS oxidation (FeS_oxidation)',
|
|
1752
|
+
' if "FeS" in idx and "O2" in idx and "Fe3" in idx and "SO4" in idx:',
|
|
1753
|
+
' i_fes = idx["FeS"]',
|
|
1754
|
+
' i_o2 = idx["O2"]',
|
|
1755
|
+
' i_fe3 = idx["Fe3"]',
|
|
1756
|
+
' i_so4 = idx["SO4"]',
|
|
1757
|
+
' k_num = float(mp.k.get("FeS_O2") if hasattr(mp, "k") else getattr(k, "FeS_O2", 0.0))',
|
|
1758
|
+
' c_fes = np.asarray(c["FeS"].value if hasattr(c["FeS"], "value") else c["FeS"])',
|
|
1759
|
+
' c_o2 = np.asarray(c["O2"].value if hasattr(c["O2"], "value") else c["O2"])',
|
|
1760
|
+
' d_fes = solid_fac * k_num * c_o2',
|
|
1761
|
+
' d_o2 = solid_fac * k_num * c_fes',
|
|
1762
|
+
' J[:, i_fes, i_fes] -= d_fes',
|
|
1763
|
+
' J[:, i_fes, i_o2] -= d_o2',
|
|
1764
|
+
' J[:, i_fe3, i_fes] += d_fes',
|
|
1765
|
+
' J[:, i_fe3, i_o2] += d_o2',
|
|
1766
|
+
' J[:, i_so4, i_fes] += d_fes',
|
|
1767
|
+
' J[:, i_so4, i_o2] += d_o2',
|
|
1768
|
+
' J[:, i_o2, i_fes] -= 2.25 * d_fes',
|
|
1769
|
+
' J[:, i_o2, i_o2] -= 2.25 * d_o2',
|
|
1770
|
+
' if "FeS_32" in idx and "SO4_32" in idx and getattr(mp, "isotopes", False):',
|
|
1771
|
+
' i_fes_32 = idx["FeS_32"]',
|
|
1772
|
+
' i_so4_32 = idx["SO4_32"]',
|
|
1773
|
+
' c_fes_32 = np.asarray(c["FeS_32"].value if hasattr(c["FeS_32"], "value") else c["FeS_32"])',
|
|
1774
|
+
' d_o2_32 = solid_fac * k_num * c_fes_32',
|
|
1775
|
+
' J[:, i_fes_32, i_fes_32] -= d_fes',
|
|
1776
|
+
' J[:, i_so4_32, i_fes_32] += d_fes',
|
|
1777
|
+
' J[:, i_fes_32, i_o2] -= d_o2_32',
|
|
1778
|
+
' J[:, i_so4_32, i_o2] += d_o2_32',
|
|
1779
|
+
'',
|
|
1780
|
+
])
|
|
1781
|
+
|
|
1782
|
+
if "FeS2_oxidation" in active_categories:
|
|
1783
|
+
code.extend([
|
|
1784
|
+
' # 9. FeS2 oxidation (FeS2_oxidation)',
|
|
1785
|
+
' if "FeS2" in idx and "O2" in idx and "Fe3" in idx and "SO4" in idx:',
|
|
1786
|
+
' i_fes2 = idx["FeS2"]',
|
|
1787
|
+
' i_o2 = idx["O2"]',
|
|
1788
|
+
' i_fe3 = idx["Fe3"]',
|
|
1789
|
+
' i_so4 = idx["SO4"]',
|
|
1790
|
+
' k_num = float(mp.k.get("FeS2_O2") if hasattr(mp, "k") else getattr(k, "FeS2_O2", 0.0))',
|
|
1791
|
+
' c_fes2 = np.asarray(c["FeS2"].value if hasattr(c["FeS2"], "value") else c["FeS2"])',
|
|
1792
|
+
' c_o2 = np.asarray(c["O2"].value if hasattr(c["O2"], "value") else c["O2"])',
|
|
1793
|
+
' d_fes2 = solid_fac * k_num * c_o2',
|
|
1794
|
+
' d_o2 = solid_fac * k_num * c_fes2',
|
|
1795
|
+
' J[:, i_fes2, i_fes2] -= d_fes2',
|
|
1796
|
+
' J[:, i_fes2, i_o2] -= d_o2',
|
|
1797
|
+
' J[:, i_fe3, i_fes2] += d_fes2',
|
|
1798
|
+
' J[:, i_fe3, i_o2] += d_o2',
|
|
1799
|
+
' J[:, i_so4, i_fes2] += 2.0 * d_fes2',
|
|
1800
|
+
' J[:, i_so4, i_o2] += 2.0 * d_o2',
|
|
1801
|
+
' J[:, i_o2, i_fes2] -= 3.5 * d_fes2',
|
|
1802
|
+
' J[:, i_o2, i_o2] -= 3.5 * d_o2',
|
|
1803
|
+
' if "FeS2_32" in idx and "SO4_32" in idx and getattr(mp, "isotopes", False):',
|
|
1804
|
+
' i_fes2_32 = idx["FeS2_32"]',
|
|
1805
|
+
' i_so4_32 = idx["SO4_32"]',
|
|
1806
|
+
' c_fes2_32 = np.asarray(c["FeS2_32"].value if hasattr(c["FeS2_32"], "value") else c["FeS2_32"])',
|
|
1807
|
+
' d_o2_32 = solid_fac * k_num * c_fes2_32',
|
|
1808
|
+
' J[:, i_fes2_32, i_fes2_32] -= d_fes2',
|
|
1809
|
+
' J[:, i_so4_32, i_fes2_32] += 2.0 * d_fes2',
|
|
1810
|
+
' J[:, i_fes2_32, i_o2] -= d_o2_32',
|
|
1811
|
+
' J[:, i_so4_32, i_o2] += 2.0 * d_o2_32',
|
|
1812
|
+
'',
|
|
1813
|
+
])
|
|
1814
|
+
|
|
1815
|
+
if "FeS2_precipitation_TS2" in active_categories:
|
|
1816
|
+
code.extend([
|
|
1817
|
+
' # 10. FeS2 precipitation via TS2 (FeS2_precipitation_TS2)',
|
|
1818
|
+
' if "FeS" in idx and "TS2" in idx and "FeS2" in idx:',
|
|
1819
|
+
' i_fes = idx["FeS"]',
|
|
1820
|
+
' i_ts2 = idx["TS2"]',
|
|
1821
|
+
' i_fes2 = idx["FeS2"]',
|
|
1822
|
+
' k_num = float(mp.k.get("FeS_TS2") if hasattr(mp, "k") else getattr(k, "FeS_TS2", 0.0))',
|
|
1823
|
+
' c_fes = np.asarray(c["FeS"].value if hasattr(c["FeS"], "value") else c["FeS"])',
|
|
1824
|
+
' c_ts2 = np.asarray(c["TS2"].value if hasattr(c["TS2"], "value") else c["TS2"])',
|
|
1825
|
+
' hs_frac = float(mp.hs_frac)',
|
|
1826
|
+
' d_fes = solid_fac * k_num * c_ts2 * hs_frac',
|
|
1827
|
+
' d_ts2 = solid_fac * k_num * c_fes * hs_frac',
|
|
1828
|
+
' J[:, i_fes, i_fes] -= d_fes',
|
|
1829
|
+
' J[:, i_fes, i_ts2] -= d_ts2',
|
|
1830
|
+
' J[:, i_ts2, i_fes] -= d_fes',
|
|
1831
|
+
' J[:, i_ts2, i_ts2] -= d_ts2',
|
|
1832
|
+
' J[:, i_fes2, i_fes] += d_fes',
|
|
1833
|
+
' J[:, i_fes2, i_ts2] += d_ts2',
|
|
1834
|
+
' if "FeS_32" in idx and "TS2_32" in idx and "FeS2_32" in idx and getattr(mp, "isotopes", False):',
|
|
1835
|
+
' i_fes_32 = idx["FeS_32"]',
|
|
1836
|
+
' i_ts2_32 = idx["TS2_32"]',
|
|
1837
|
+
' i_fes2_32 = idx["FeS2_32"]',
|
|
1838
|
+
' J[:, i_fes_32, i_fes_32] -= d_fes',
|
|
1839
|
+
' J[:, i_ts2_32, i_ts2_32] -= d_ts2',
|
|
1840
|
+
' J[:, i_fes2_32, i_fes2_32] += d_fes',
|
|
1841
|
+
' J[:, i_fes2_32, i_ts2_32] += d_ts2',
|
|
1842
|
+
'',
|
|
1843
|
+
])
|
|
1844
|
+
|
|
1845
|
+
if "elemental_sulfur_oxidation" in active_categories:
|
|
1846
|
+
code.extend([
|
|
1847
|
+
' # 11. Elemental sulfur oxidation (elemental_sulfur_oxidation)',
|
|
1848
|
+
' if "S0" in idx and "O2" in idx and "SO4" in idx:',
|
|
1849
|
+
' i_s0 = idx["S0"]',
|
|
1850
|
+
' i_o2 = idx["O2"]',
|
|
1851
|
+
' i_so4 = idx["SO4"]',
|
|
1852
|
+
' k_num = float(mp.k.get("S0_O2") if hasattr(mp, "k") else getattr(k, "S0_O2", 0.0))',
|
|
1853
|
+
' c_s0 = np.asarray(c["S0"].value if hasattr(c["S0"], "value") else c["S0"])',
|
|
1854
|
+
' c_o2 = np.asarray(c["O2"].value if hasattr(c["O2"], "value") else c["O2"])',
|
|
1855
|
+
' d_s0 = solid_fac * k_num * c_o2',
|
|
1856
|
+
' d_o2 = solid_fac * k_num * c_s0',
|
|
1857
|
+
' J[:, i_s0, i_s0] -= d_s0',
|
|
1858
|
+
' J[:, i_s0, i_o2] -= d_o2',
|
|
1859
|
+
' J[:, i_o2, i_s0] -= 1.5 * d_s0',
|
|
1860
|
+
' J[:, i_o2, i_o2] -= 1.5 * d_o2',
|
|
1861
|
+
' J[:, i_so4, i_s0] += d_s0',
|
|
1862
|
+
' J[:, i_so4, i_o2] += d_o2',
|
|
1863
|
+
' if "S0_32" in idx and "SO4_32" in idx and getattr(mp, "isotopes", False):',
|
|
1864
|
+
' i_s0_32 = idx["S0_32"]',
|
|
1865
|
+
' i_so4_32 = idx["SO4_32"]',
|
|
1866
|
+
' c_s0_32 = np.asarray(c["S0_32"].value if hasattr(c["S0_32"], "value") else c["S0_32"])',
|
|
1867
|
+
' d_o2_32 = solid_fac * k_num * c_s0_32',
|
|
1868
|
+
' J[:, i_s0_32, i_s0_32] -= d_s0',
|
|
1869
|
+
' J[:, i_so4_32, i_s0_32] += d_s0',
|
|
1870
|
+
' J[:, i_s0_32, i_o2] -= d_o2_32',
|
|
1871
|
+
' J[:, i_so4_32, i_o2] += d_o2_32',
|
|
1872
|
+
'',
|
|
1873
|
+
])
|
|
1874
|
+
|
|
1875
|
+
if "pyrite_formation_fes_s0" in active_categories:
|
|
1876
|
+
code.extend([
|
|
1877
|
+
' # 12. Pyrite formation via S0 (pyrite_formation_fes_s0_new)',
|
|
1878
|
+
' if "FeS" in idx and "S0" in idx and "FeS2" in idx:',
|
|
1879
|
+
' i_fes = idx["FeS"]',
|
|
1880
|
+
' i_s0 = idx["S0"]',
|
|
1881
|
+
' i_fes2 = idx["FeS2"]',
|
|
1882
|
+
' k_num = float(mp.k.get("FeS_S0") if hasattr(mp, "k") else getattr(k, "FeS_S0", 0.0))',
|
|
1883
|
+
' c_fes = np.asarray(c["FeS"].value if hasattr(c["FeS"], "value") else c["FeS"])',
|
|
1884
|
+
' c_s0 = np.asarray(c["S0"].value if hasattr(c["S0"], "value") else c["S0"])',
|
|
1885
|
+
' d_fes = solid_fac * k_num * c_s0',
|
|
1886
|
+
' d_s0 = solid_fac * k_num * c_fes',
|
|
1887
|
+
' J[:, i_fes, i_fes] -= d_fes',
|
|
1888
|
+
' J[:, i_fes, i_s0] -= d_s0',
|
|
1889
|
+
' J[:, i_s0, i_fes] -= d_fes',
|
|
1890
|
+
' J[:, i_s0, i_s0] -= d_s0',
|
|
1891
|
+
' J[:, i_fes2, i_fes] += d_fes',
|
|
1892
|
+
' J[:, i_fes2, i_s0] += d_s0',
|
|
1893
|
+
' if "FeS_32" in idx and "S0_32" in idx and "FeS2_32" in idx and getattr(mp, "isotopes", False):',
|
|
1894
|
+
' i_fes_32 = idx["FeS_32"]',
|
|
1895
|
+
' i_s0_32 = idx["S0_32"]',
|
|
1896
|
+
' i_fes2_32 = idx["FeS2_32"]',
|
|
1897
|
+
' J[:, i_fes_32, i_fes_32] -= d_fes',
|
|
1898
|
+
' J[:, i_s0_32, i_s0_32] -= d_s0',
|
|
1899
|
+
' J[:, i_fes2_32, i_fes2_32] += d_fes',
|
|
1900
|
+
' J[:, i_fes2_32, i_s0_32] += d_s0',
|
|
1901
|
+
'',
|
|
1902
|
+
])
|
|
1903
|
+
|
|
1904
|
+
if "elemental_sulfur_disproportionation" in active_categories:
|
|
1905
|
+
code.extend([
|
|
1906
|
+
' # 13. Elemental sulfur disproportionation',
|
|
1907
|
+
' if "S0" in idx and "TS2" in idx and "SO4" in idx:',
|
|
1908
|
+
' i_s0 = idx["S0"]',
|
|
1909
|
+
' i_ts2 = idx["TS2"]',
|
|
1910
|
+
' i_so4 = idx["SO4"]',
|
|
1911
|
+
' k_num = float(mp.k.get("S0_dispro") if hasattr(mp, "k") else getattr(k, "S0_dispro", 0.0))',
|
|
1912
|
+
' lim_ts2 = lim.get("TS2", 1.0)',
|
|
1913
|
+
' lim_o2 = lim.get("O2_inhibit", 1.0)',
|
|
1914
|
+
' coeff_s0 = np.maximum(k_num * lim_ts2 * lim_o2, 0.0)',
|
|
1915
|
+
' d_s0 = solid_fac * coeff_s0',
|
|
1916
|
+
' J[:, i_s0, i_s0] -= d_s0',
|
|
1917
|
+
' J[:, i_ts2, i_s0] += 0.75 * d_s0',
|
|
1918
|
+
' J[:, i_so4, i_s0] += 0.25 * d_s0',
|
|
1919
|
+
' if "S0_32" in idx and "TS2_32" in idx and "SO4_32" in idx and getattr(mp, "isotopes", False):',
|
|
1920
|
+
' i_s0_32 = idx["S0_32"]',
|
|
1921
|
+
' i_ts2_32 = idx["TS2_32"]',
|
|
1922
|
+
' i_so4_32 = idx["SO4_32"]',
|
|
1923
|
+
' J[:, i_s0_32, i_s0_32] -= d_s0',
|
|
1924
|
+
' J[:, i_ts2_32, i_s0_32] += 0.75 * d_s0',
|
|
1925
|
+
' J[:, i_so4_32, i_s0_32] += 0.25 * d_s0',
|
|
1926
|
+
'',
|
|
1927
|
+
])
|
|
1928
|
+
|
|
1929
|
+
code.extend([
|
|
1930
|
+
' return J',
|
|
1931
|
+
'',
|
|
1932
|
+
'diagenetic_reactions.compute_chemical_jacobian = compute_chemical_jacobian',
|
|
1933
|
+
])
|
|
1934
|
+
return "\n".join(code)
|
|
1935
|
+
|
|
1936
|
+
|
|
1937
|
+
def main(args_list: Optional[List[str]] = None) -> int:
|
|
1938
|
+
"""CLI entrypoint for generate_equation_system."""
|
|
1939
|
+
parser = build_arg_parser()
|
|
1940
|
+
args = parser.parse_args(args_list)
|
|
1941
|
+
|
|
1942
|
+
# Maintain attribute compatibility with legacy flag names
|
|
1943
|
+
if not hasattr(args, "input"):
|
|
1944
|
+
args.input = args.reactions
|
|
1945
|
+
if not hasattr(args, "output"):
|
|
1946
|
+
args.output = args.equations
|
|
1947
|
+
if not hasattr(args, "constants"):
|
|
1948
|
+
args.constants = args.kinetic_constants
|
|
1949
|
+
|
|
1950
|
+
try:
|
|
1951
|
+
generate_equations(
|
|
1952
|
+
reactions_path=args.reactions,
|
|
1953
|
+
equations_path=args.equations,
|
|
1954
|
+
kinetic_constants_path=args.kinetic_constants,
|
|
1955
|
+
limiters_path=args.limiters,
|
|
1956
|
+
species_path=args.species,
|
|
1957
|
+
verbose=args.verbose,
|
|
1958
|
+
validate_only=args.validate_only,
|
|
1959
|
+
)
|
|
1960
|
+
if not args.validate_only:
|
|
1961
|
+
print(f"Successfully generated equations module saved to: {args.equations}")
|
|
1962
|
+
return 0
|
|
1963
|
+
except ValidationError as ve:
|
|
1964
|
+
print("\n" + "=" * 60, file=sys.stderr)
|
|
1965
|
+
print(f"VALIDATION FAILED:\n{ve}", file=sys.stderr)
|
|
1966
|
+
print("=" * 60 + "\n", file=sys.stderr)
|
|
1967
|
+
return 1
|
|
1968
|
+
except Exception as e:
|
|
1969
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
1970
|
+
return 1
|
|
1971
|
+
|
|
1972
|
+
|
|
1973
|
+
if __name__ == "__main__":
|
|
1974
|
+
sys.exit(main())
|
|
1975
|
+
|