aquakin 0.1.0__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.
Files changed (165) hide show
  1. aquakin/__init__.py +332 -0
  2. aquakin/core/__init__.py +7 -0
  3. aquakin/core/conditions.py +162 -0
  4. aquakin/core/context.py +31 -0
  5. aquakin/core/hints.py +32 -0
  6. aquakin/core/introspect.py +174 -0
  7. aquakin/core/model.py +1492 -0
  8. aquakin/core/nodes.py +543 -0
  9. aquakin/core/parser.py +340 -0
  10. aquakin/core/ph_solver.py +853 -0
  11. aquakin/core/precipitation.py +244 -0
  12. aquakin/core/precipitation_equilibrium.py +361 -0
  13. aquakin/core/speciation.py +221 -0
  14. aquakin/core/stoich_resolve.py +173 -0
  15. aquakin/core/temperature.py +82 -0
  16. aquakin/core/units.py +69 -0
  17. aquakin/core/vector_kernel.py +368 -0
  18. aquakin/integrate/__init__.py +104 -0
  19. aquakin/integrate/_common.py +1523 -0
  20. aquakin/integrate/_qmc.py +107 -0
  21. aquakin/integrate/_simultaneous_corrector.py +139 -0
  22. aquakin/integrate/_transforms.py +72 -0
  23. aquakin/integrate/batch.py +455 -0
  24. aquakin/integrate/biofilm.py +997 -0
  25. aquakin/integrate/calibrate.py +1673 -0
  26. aquakin/integrate/cfd.py +270 -0
  27. aquakin/integrate/colored_jacobian.py +493 -0
  28. aquakin/integrate/design.py +258 -0
  29. aquakin/integrate/discrete_adjoint.py +713 -0
  30. aquakin/integrate/events.py +398 -0
  31. aquakin/integrate/fit.py +231 -0
  32. aquakin/integrate/forward_sensitivity.py +515 -0
  33. aquakin/integrate/forward_solve.py +319 -0
  34. aquakin/integrate/global_sensitivity.py +404 -0
  35. aquakin/integrate/monte_carlo.py +233 -0
  36. aquakin/integrate/particle.py +272 -0
  37. aquakin/integrate/pfr.py +347 -0
  38. aquakin/integrate/profile.py +390 -0
  39. aquakin/integrate/scenarios.py +250 -0
  40. aquakin/integrate/sensitivity.py +164 -0
  41. aquakin/models/__init__.py +1 -0
  42. aquakin/models/_make_khalil_balanced.py +149 -0
  43. aquakin/models/_make_khalil_balanced_biofilm.py +110 -0
  44. aquakin/models/_make_khalil_balanced_biofilm_biomass.py +177 -0
  45. aquakin/models/_make_khalil_balanced_biofilm_multispecies.py +308 -0
  46. aquakin/models/_make_khalil_paper.py +201 -0
  47. aquakin/models/_make_khalil_thesis.py +135 -0
  48. aquakin/models/_make_khalil_variants.py +214 -0
  49. aquakin/models/adm1.yaml +488 -0
  50. aquakin/models/asm1.yaml +191 -0
  51. aquakin/models/asm1_ammonia_limitation.yaml +37 -0
  52. aquakin/models/asm2d.yaml +402 -0
  53. aquakin/models/asm2d_chemp.yaml +92 -0
  54. aquakin/models/asm2d_tud.yaml +409 -0
  55. aquakin/models/asm3.yaml +219 -0
  56. aquakin/models/asm3_2step.yaml +368 -0
  57. aquakin/models/asm3_2step_anammox.yaml +95 -0
  58. aquakin/models/asm3_2step_comammox.yaml +92 -0
  59. aquakin/models/asm3_2step_n2o.yaml +132 -0
  60. aquakin/models/asm3_biop.yaml +396 -0
  61. aquakin/models/ozone_bromate.yaml +166 -0
  62. aquakin/models/precipitation_metal_phosphate.yaml +98 -0
  63. aquakin/models/precipitation_metal_phosphate_bounded.yaml +109 -0
  64. aquakin/models/precipitation_metal_phosphate_equilibrium.yaml +119 -0
  65. aquakin/models/precipitation_struvite_calcite.yaml +74 -0
  66. aquakin/models/uv_h2o2.yaml +99 -0
  67. aquakin/models/wats_sewer.yaml +387 -0
  68. aquakin/models/wats_sewer_extended.yaml +531 -0
  69. aquakin/models/wats_sewer_extended_combined.yaml +550 -0
  70. aquakin/models/wats_sewer_extended_directsulfate.yaml +544 -0
  71. aquakin/models/wats_sewer_extended_halforder.yaml +545 -0
  72. aquakin/models/wats_sewer_extended_srbsubstrate.yaml +544 -0
  73. aquakin/models/wats_sewer_extended_v0.yaml +492 -0
  74. aquakin/models/wats_sewer_khalil_paper.yaml +321 -0
  75. aquakin/models/wats_sewer_khalil_paper_balanced.yaml +602 -0
  76. aquakin/models/wats_sewer_khalil_paper_balanced_biofilm.yaml +621 -0
  77. aquakin/models/wats_sewer_khalil_paper_balanced_biofilm_biomass.yaml +536 -0
  78. aquakin/models/wats_sewer_khalil_paper_balanced_biofilm_multispecies.yaml +665 -0
  79. aquakin/models/wats_sewer_khalil_paper_balanced_combined.yaml +602 -0
  80. aquakin/models/wats_sewer_khalil_paper_balanced_directsulfate.yaml +599 -0
  81. aquakin/models/wats_sewer_khalil_paper_balanced_halforder.yaml +16 -0
  82. aquakin/models/wats_sewer_khalil_paper_balanced_stopatS0.yaml +593 -0
  83. aquakin/models/wats_sewer_khalil_paper_combined.yaml +545 -0
  84. aquakin/models/wats_sewer_khalil_paper_directsulfate.yaml +543 -0
  85. aquakin/models/wats_sewer_khalil_paper_halforder.yaml +16 -0
  86. aquakin/models/wats_sewer_khalil_paper_srbsubstrate.yaml +542 -0
  87. aquakin/models/wats_sewer_khalil_paper_stopatS0.yaml +537 -0
  88. aquakin/models/wats_sewer_khalil_thesis.yaml +846 -0
  89. aquakin/plant/__init__.py +255 -0
  90. aquakin/plant/_builder_support.py +96 -0
  91. aquakin/plant/_constants.py +122 -0
  92. aquakin/plant/_flow_split.py +62 -0
  93. aquakin/plant/a2o.py +415 -0
  94. aquakin/plant/aeration_system.py +253 -0
  95. aquakin/plant/balance.py +495 -0
  96. aquakin/plant/bsm/__init__.py +59 -0
  97. aquakin/plant/bsm/bsm1.py +254 -0
  98. aquakin/plant/bsm/bsm2.py +880 -0
  99. aquakin/plant/bsm/data/BSM1_dry.csv +1352 -0
  100. aquakin/plant/bsm/data/BSM1_rain.csv +1352 -0
  101. aquakin/plant/bsm/data/BSM1_storm.csv +1352 -0
  102. aquakin/plant/bsm/data/BSM2_dry.csv +1352 -0
  103. aquakin/plant/bsm/data/BSM2_rain.csv +1352 -0
  104. aquakin/plant/bsm/data/BSM2_storm.csv +1352 -0
  105. aquakin/plant/bsm/data/__init__.py +1 -0
  106. aquakin/plant/bsm/evaluation.py +928 -0
  107. aquakin/plant/bsm/warmstart.py +155 -0
  108. aquakin/plant/calibrate.py +783 -0
  109. aquakin/plant/characterize.py +310 -0
  110. aquakin/plant/clarifier.py +171 -0
  111. aquakin/plant/colored.py +466 -0
  112. aquakin/plant/control.py +178 -0
  113. aquakin/plant/cost.py +192 -0
  114. aquakin/plant/coupling.py +115 -0
  115. aquakin/plant/cstr.py +580 -0
  116. aquakin/plant/delay.py +144 -0
  117. aquakin/plant/design.py +507 -0
  118. aquakin/plant/digester.py +248 -0
  119. aquakin/plant/disinfection.py +363 -0
  120. aquakin/plant/dosing.py +278 -0
  121. aquakin/plant/errors.py +37 -0
  122. aquakin/plant/flow_setpoint.py +91 -0
  123. aquakin/plant/ghg.py +278 -0
  124. aquakin/plant/ifas.py +382 -0
  125. aquakin/plant/influent.py +500 -0
  126. aquakin/plant/interfaces.py +463 -0
  127. aquakin/plant/mbr.py +278 -0
  128. aquakin/plant/metrics.py +565 -0
  129. aquakin/plant/mixer.py +339 -0
  130. aquakin/plant/plant.py +4167 -0
  131. aquakin/plant/primary_clarifier.py +173 -0
  132. aquakin/plant/recycle.py +850 -0
  133. aquakin/plant/results.py +384 -0
  134. aquakin/plant/sbr.py +376 -0
  135. aquakin/plant/schedule.py +77 -0
  136. aquakin/plant/sensitivity.py +1296 -0
  137. aquakin/plant/separators.py +181 -0
  138. aquakin/plant/settling.py +227 -0
  139. aquakin/plant/solve_cache.py +72 -0
  140. aquakin/plant/steady.py +811 -0
  141. aquakin/plant/storage.py +203 -0
  142. aquakin/plant/streams.py +323 -0
  143. aquakin/plant/takacs.py +831 -0
  144. aquakin/plant/temperature.py +164 -0
  145. aquakin/plant/translators.py +113 -0
  146. aquakin/plant/units.py +256 -0
  147. aquakin/schema/__init__.py +22 -0
  148. aquakin/schema/inheritance.py +137 -0
  149. aquakin/schema/loader.py +208 -0
  150. aquakin/schema/model_spec.py +747 -0
  151. aquakin/transport/__init__.py +1 -0
  152. aquakin/transport/openfoam/__init__.py +15 -0
  153. aquakin/transport/openfoam/bridge.py +58 -0
  154. aquakin/transport/openfoam/tracks.py +160 -0
  155. aquakin/utils/__init__.py +49 -0
  156. aquakin/utils/balance.py +153 -0
  157. aquakin/utils/composition.py +361 -0
  158. aquakin/utils/latex.py +145 -0
  159. aquakin/utils/rtd.py +172 -0
  160. aquakin/utils/units.py +633 -0
  161. aquakin-0.1.0.dist-info/METADATA +665 -0
  162. aquakin-0.1.0.dist-info/RECORD +165 -0
  163. aquakin-0.1.0.dist-info/WHEEL +5 -0
  164. aquakin-0.1.0.dist-info/licenses/LICENSE +21 -0
  165. aquakin-0.1.0.dist-info/top_level.txt +1 -0
aquakin/__init__.py ADDED
@@ -0,0 +1,332 @@
1
+ """aquakin: reactive scalar transport kinetics for aqueous environmental systems."""
2
+
3
+ import os as _os
4
+ import sys as _sys
5
+ import warnings as _warnings
6
+
7
+ # aquakin enables JAX 64-bit (x64) mode at import: the stiff implicit ODE solves
8
+ # need double precision (repeatedly forming small net differences in a stiff
9
+ # Newton step loses the accurate digits at float32). This mutates GLOBAL,
10
+ # process-wide JAX state -- any other JAX code in the same process will use
11
+ # float64 afterward. The behaviour is deliberate and documented, but to keep it
12
+ # from being a *silent* surprise we warn when we are overriding what looks like
13
+ # an explicit float32 preference: JAX was already imported (so other code may be
14
+ # relying on the default float32), or JAX_ENABLE_X64 is set to a false value.
15
+ _jax_already_imported = "jax" in _sys.modules
16
+
17
+ import jax
18
+
19
+ if not jax.config.jax_enable_x64:
20
+ _env_x64 = _os.environ.get("JAX_ENABLE_X64", "").strip().lower()
21
+ _overrides_explicit_choice = _jax_already_imported or _env_x64 in ("0", "false", "off", "no")
22
+ if _overrides_explicit_choice:
23
+ _warnings.warn(
24
+ "aquakin is enabling JAX 64-bit (x64) mode, which it requires for "
25
+ "stiff ODE integration. This is GLOBAL, process-wide JAX state: "
26
+ "other JAX code in this process will now use float64. It overrides "
27
+ "an apparent float32 preference (JAX was already imported, or "
28
+ "JAX_ENABLE_X64 is set off). Enable x64 yourself before importing "
29
+ "aquakin, or run aquakin in a separate process, to silence this.",
30
+ stacklevel=2,
31
+ )
32
+ jax.config.update("jax_enable_x64", True)
33
+
34
+ import logging as _logging
35
+
36
+ # Library logging hygiene: attach a NullHandler to the package's root logger so
37
+ # aquakin never configures logging on the application's behalf (it emits nothing
38
+ # unless the caller opts in) and there is no "No handlers could be found" noise.
39
+ # Progress output -- e.g. the ``progress=`` sampling loops in
40
+ # ``plant.steady_state_dgsm`` / ``dynamic_dgsm`` -- is logged at INFO on child
41
+ # loggers; a caller sees it by enabling INFO, e.g.
42
+ # ``logging.basicConfig(level=logging.INFO)`` or
43
+ # ``logging.getLogger("aquakin").setLevel(logging.INFO)`` with a handler.
44
+ _logging.getLogger("aquakin").addHandler(_logging.NullHandler())
45
+
46
+ from aquakin.core.conditions import OperatingConditions, SpatialConditions
47
+ from aquakin.core.model import CompiledModel, compile_model
48
+ from aquakin.core.parser import parse_rate_expression
49
+ from aquakin.integrate.batch import BatchReactor, BatchSolution
50
+ from aquakin.integrate.biofilm import BiofilmReactor, BiofilmSolution
51
+ from aquakin.integrate.calibrate import (
52
+ CalibrationResult,
53
+ FreeICConfig,
54
+ LaplaceConfig,
55
+ OptimizerConfig,
56
+ PredictiveBand,
57
+ calibrate,
58
+ )
59
+ from aquakin.integrate.profile import ProfileResult, profile_likelihood
60
+ from aquakin.integrate.cfd import CFDReactor
61
+ from aquakin.integrate.particle import (
62
+ ParticleTrackReactor,
63
+ Track,
64
+ TrackSolution,
65
+ integrate_ensemble,
66
+ )
67
+ from aquakin.integrate.discrete_adjoint import (
68
+ esdirk_adjoint_solve,
69
+ implicit_euler_adjoint_solve,
70
+ )
71
+ from aquakin.integrate._common import (
72
+ DifferentiationConfig,
73
+ IntegratorConfig,
74
+ check_finite_gradient,
75
+ forward_adjoint,
76
+ )
77
+ from aquakin.integrate.forward_sensitivity import (
78
+ ForwardSensitivityResult,
79
+ forward_sensitivity,
80
+ )
81
+ from aquakin.integrate.pfr import PFRSolution, PlugFlowReactor
82
+ from aquakin.integrate.sensitivity import SensitivityResult, sensitivity
83
+ from aquakin.integrate.fit import FitResult, fit
84
+ from aquakin.integrate.global_sensitivity import DGSMResult, dgsm
85
+ from aquakin.integrate.events import Event, EventedResult, solve_with_events
86
+ from aquakin.integrate.design import Constraint, OptimizeResult, optimize_design
87
+ from aquakin.integrate.monte_carlo import MonteCarloResult, monte_carlo
88
+ from aquakin.integrate.scenarios import (
89
+ KPIComparison,
90
+ ScenarioComparison,
91
+ compare_scenarios,
92
+ kpi_comparison,
93
+ )
94
+ from aquakin.plant import (
95
+ ActivatedSludgeSizing,
96
+ Aeration,
97
+ AerationDesignPoint,
98
+ AerationSystem,
99
+ AlgebraicTemperature,
100
+ BSM1Evaluation,
101
+ BSM2Evaluation,
102
+ CSTRUnit,
103
+ CarbonFootprint,
104
+ ChlorineContactUnit,
105
+ ComponentBalance,
106
+ CostFactors,
107
+ IdentityTranslator,
108
+ InfluentFractions,
109
+ InfluentSeries,
110
+ MassBalance,
111
+ NoDigesterError,
112
+ OperatingCost,
113
+ UnknownPortError,
114
+ UnknownUnitError,
115
+ WiringError,
116
+ characterize_influent,
117
+ fractionate,
118
+ mass_balance,
119
+ read_influent_csv,
120
+ MixerUnit,
121
+ Plant,
122
+ PlantCheck,
123
+ PlantSolution,
124
+ HeatBalanceTemperature,
125
+ RatioSplitter,
126
+ SetpointSplitter,
127
+ SludgeMetrics,
128
+ StateTranslator,
129
+ Stream,
130
+ TemperatureModel,
131
+ ThresholdSplitter,
132
+ UVUnit,
133
+ Unit,
134
+ aeration_energy,
135
+ blower_airflow_total,
136
+ blower_energy,
137
+ blower_power_kw,
138
+ carbon_footprint,
139
+ carbon_mass,
140
+ co2e_from_energy,
141
+ ct_log_removal,
142
+ ct_value,
143
+ design_summary,
144
+ derived_BOD,
145
+ derived_COD,
146
+ derived_TKN,
147
+ derived_TSS,
148
+ direct_n2o_emission,
149
+ effluent_averages,
150
+ effluent_quality_index,
151
+ evaluate_bsm1,
152
+ evaluate_bsm2,
153
+ heating_energy,
154
+ methane_to_co2e,
155
+ mixing_energy,
156
+ n2o_n_to_co2e,
157
+ operating_cost,
158
+ operational_cost_index,
159
+ operational_cost_index_bsm2,
160
+ pumping_energy,
161
+ pumping_energy_bsm2,
162
+ required_airflow,
163
+ size_activated_sludge,
164
+ sludge_metrics,
165
+ stripped_n2o,
166
+ t10_from_baffling,
167
+ t10_from_rtd,
168
+ time_average,
169
+ uv_dose,
170
+ uv_log_inactivation,
171
+ )
172
+ from aquakin.schema.loader import (
173
+ clear_model_cache,
174
+ load_model,
175
+ load_model_from_file,
176
+ )
177
+ from aquakin.schema.model_spec import SpeciationUnitsWarning
178
+
179
+ # The utility helpers are aggregated by aquakin.utils (the single utility
180
+ # surface); the top-level namespace re-exports the curated model-analysis subset
181
+ # from there rather than reaching into utils.balance / .composition / .units.
182
+ from aquakin.utils import (
183
+ UnitWarning,
184
+ canonical_content,
185
+ check_conservation,
186
+ check_model_units,
187
+ check_nitrogen,
188
+ composition_table,
189
+ parse_units,
190
+ )
191
+
192
+ __all__ = [
193
+ "ActivatedSludgeSizing",
194
+ "Aeration",
195
+ "AerationDesignPoint",
196
+ "AerationSystem",
197
+ "AlgebraicTemperature",
198
+ "BSM1Evaluation",
199
+ "BSM2Evaluation",
200
+ "BatchReactor",
201
+ "BatchSolution",
202
+ "BiofilmReactor",
203
+ "BiofilmSolution",
204
+ "CFDReactor",
205
+ "CSTRUnit",
206
+ "CalibrationResult",
207
+ "CarbonFootprint",
208
+ "ChlorineContactUnit",
209
+ "CompiledModel",
210
+ "ComponentBalance",
211
+ "Constraint",
212
+ "CostFactors",
213
+ "DGSMResult",
214
+ "DifferentiationConfig",
215
+ "Event",
216
+ "EventedResult",
217
+ "FitResult",
218
+ "ForwardSensitivityResult",
219
+ "FreeICConfig",
220
+ "HeatBalanceTemperature",
221
+ "IdentityTranslator",
222
+ "InfluentFractions",
223
+ "InfluentSeries",
224
+ "IntegratorConfig",
225
+ "KPIComparison",
226
+ "LaplaceConfig",
227
+ "MassBalance",
228
+ "MixerUnit",
229
+ "MonteCarloResult",
230
+ "NoDigesterError",
231
+ "OperatingConditions",
232
+ "OperatingCost",
233
+ "OptimizeResult",
234
+ "OptimizerConfig",
235
+ "PFRSolution",
236
+ "ParticleTrackReactor",
237
+ "Plant",
238
+ "PlantCheck",
239
+ "PlantSolution",
240
+ "PlugFlowReactor",
241
+ "PredictiveBand",
242
+ "ProfileResult",
243
+ "RatioSplitter",
244
+ "ScenarioComparison",
245
+ "SensitivityResult",
246
+ "SetpointSplitter",
247
+ "SludgeMetrics",
248
+ "SpatialConditions",
249
+ "SpeciationUnitsWarning",
250
+ "StateTranslator",
251
+ "Stream",
252
+ "TemperatureModel",
253
+ "ThresholdSplitter",
254
+ "Track",
255
+ "TrackSolution",
256
+ "UVUnit",
257
+ "Unit",
258
+ "UnitWarning",
259
+ "UnknownPortError",
260
+ "UnknownUnitError",
261
+ "WiringError",
262
+ "aeration_energy",
263
+ "blower_airflow_total",
264
+ "blower_energy",
265
+ "blower_power_kw",
266
+ "calibrate",
267
+ "canonical_content",
268
+ "carbon_footprint",
269
+ "carbon_mass",
270
+ "characterize_influent",
271
+ "check_conservation",
272
+ "check_finite_gradient",
273
+ "check_model_units",
274
+ "check_nitrogen",
275
+ "clear_model_cache",
276
+ "co2e_from_energy",
277
+ "compare_scenarios",
278
+ "compile_model",
279
+ "composition_table",
280
+ "ct_log_removal",
281
+ "ct_value",
282
+ "derived_BOD",
283
+ "derived_COD",
284
+ "derived_TKN",
285
+ "derived_TSS",
286
+ "design_summary",
287
+ "dgsm",
288
+ "direct_n2o_emission",
289
+ "effluent_averages",
290
+ "effluent_quality_index",
291
+ "esdirk_adjoint_solve",
292
+ "evaluate_bsm1",
293
+ "evaluate_bsm2",
294
+ "fit",
295
+ "forward_adjoint",
296
+ "forward_sensitivity",
297
+ "fractionate",
298
+ "heating_energy",
299
+ "implicit_euler_adjoint_solve",
300
+ "integrate_ensemble",
301
+ "kpi_comparison",
302
+ "load_model",
303
+ "load_model_from_file",
304
+ "mass_balance",
305
+ "methane_to_co2e",
306
+ "mixing_energy",
307
+ "monte_carlo",
308
+ "n2o_n_to_co2e",
309
+ "operating_cost",
310
+ "operational_cost_index",
311
+ "operational_cost_index_bsm2",
312
+ "optimize_design",
313
+ "parse_rate_expression",
314
+ "parse_units",
315
+ "profile_likelihood",
316
+ "pumping_energy",
317
+ "pumping_energy_bsm2",
318
+ "read_influent_csv",
319
+ "required_airflow",
320
+ "sensitivity",
321
+ "size_activated_sludge",
322
+ "sludge_metrics",
323
+ "solve_with_events",
324
+ "stripped_n2o",
325
+ "t10_from_baffling",
326
+ "t10_from_rtd",
327
+ "time_average",
328
+ "uv_dose",
329
+ "uv_log_inactivation",
330
+ ]
331
+
332
+ __version__ = "0.1.0"
@@ -0,0 +1,7 @@
1
+ """Core data structures and AST machinery (no Pydantic dependency)."""
2
+
3
+ from aquakin.core.conditions import SpatialConditions
4
+ from aquakin.core.context import CompileContext
5
+ from aquakin.core.model import CompiledModel, compile_model
6
+
7
+ __all__ = ["CompileContext", "CompiledModel", "SpatialConditions", "compile_model"]
@@ -0,0 +1,162 @@
1
+ """Spatially varying condition fields (pH, temperature, etc.)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Iterable
6
+ from dataclasses import dataclass, field
7
+
8
+ import jax.numpy as jnp
9
+
10
+
11
+ @dataclass
12
+ class SpatialConditions:
13
+ """
14
+ Container for spatially varying condition fields.
15
+
16
+ Each entry in ``fields`` is a JAX array of shape ``(n_locations,)`` giving
17
+ the value of that field at each spatial location. Reactors index into
18
+ these arrays with ``loc_idx`` at runtime.
19
+
20
+ Attributes
21
+ ----------
22
+ fields : dict[str, jnp.ndarray]
23
+ Mapping from field name (e.g. ``"pH"``) to a 1-D JAX array.
24
+ """
25
+
26
+ fields: dict[str, jnp.ndarray] = field(default_factory=dict)
27
+
28
+ def __post_init__(self) -> None:
29
+ normalised: dict[str, jnp.ndarray] = {}
30
+ n_locations: int | None = None
31
+ for name, value in self.fields.items():
32
+ arr = jnp.asarray(value)
33
+ if arr.ndim == 0:
34
+ arr = arr[None]
35
+ elif arr.ndim != 1:
36
+ raise ValueError(f"Condition field '{name}' must be 1-D, got shape {arr.shape}")
37
+ if n_locations is None:
38
+ n_locations = int(arr.shape[0])
39
+ elif int(arr.shape[0]) != n_locations:
40
+ raise ValueError(
41
+ f"Condition field '{name}' has length {arr.shape[0]}, expected {n_locations}"
42
+ )
43
+ normalised[name] = arr
44
+ self.fields = normalised
45
+
46
+ @property
47
+ def n_locations(self) -> int:
48
+ """Number of spatial locations represented."""
49
+ if not self.fields:
50
+ return 0
51
+ return int(next(iter(self.fields.values())).shape[0])
52
+
53
+ @classmethod
54
+ def uniform(cls, n_locations: int = 1, **kwargs: float) -> SpatialConditions:
55
+ """
56
+ Build a spatially homogeneous ``SpatialConditions`` object.
57
+
58
+ Parameters
59
+ ----------
60
+ n_locations : int, optional
61
+ Number of spatial locations (default 1). Must be at least 1; the
62
+ default suits a 0-D batch reactor, so a batch user writes
63
+ ``SpatialConditions.uniform(pH=7.5, T=293.15)``. For the 0-D case the
64
+ :class:`OperatingConditions` alias reads more naturally still.
65
+ **kwargs : float
66
+ Field name -> scalar value. The scalar is broadcast to
67
+ ``(n_locations,)``.
68
+
69
+ Returns
70
+ -------
71
+ SpatialConditions
72
+ """
73
+ if n_locations < 1:
74
+ raise ValueError(f"n_locations must be >= 1, got {n_locations}")
75
+ # Broadcast (don't ``float()``-coerce) so a *traced* condition value -- a
76
+ # user differentiating a solve w.r.t. pH/T -- flows through instead of
77
+ # raising a ConcretizationTypeError. Identity for a concrete value.
78
+ fields = {
79
+ name: jnp.broadcast_to(jnp.asarray(value), (n_locations,))
80
+ for name, value in kwargs.items()
81
+ }
82
+ return cls(fields=fields)
83
+
84
+ def with_(self, **kwargs: float) -> SpatialConditions:
85
+ """Return a copy with some fields overridden (or added).
86
+
87
+ The common edit-from-defaults pattern: start from
88
+ ``model.default_conditions()`` and change only what differs, e.g.::
89
+
90
+ conditions = model.default_conditions().with_(T=283.15) # cold
91
+
92
+ Scalar overrides are broadcast to this object's location count (so the
93
+ result keeps the same ``n_locations``); a length-``n_locations`` array
94
+ override is used as-is. Fields not named are carried over unchanged. The
95
+ original is not modified.
96
+
97
+ Parameters
98
+ ----------
99
+ **kwargs : float or array-like
100
+ Field name -> new value (scalar, broadcast to ``n_locations``, or a
101
+ length-``n_locations`` array).
102
+
103
+ Returns
104
+ -------
105
+ SpatialConditions
106
+ A new ``SpatialConditions`` (always the base type, so it stays valid
107
+ for every reactor) with the merged fields.
108
+ """
109
+ n = self.n_locations or 1
110
+ merged = dict(self.fields)
111
+ for name, value in kwargs.items():
112
+ arr = jnp.asarray(value)
113
+ # Broadcast a scalar rather than ``float()``-coercing it, so a traced
114
+ # override (a gradient w.r.t. a condition) is not concretized.
115
+ merged[name] = jnp.broadcast_to(arr, (n,)) if arr.ndim == 0 else arr
116
+ return SpatialConditions(fields=merged)
117
+
118
+ def validate_required(self, required: Iterable[str]) -> None:
119
+ """
120
+ Raise ``ValueError`` if any required field is missing.
121
+
122
+ Parameters
123
+ ----------
124
+ required : iterable of str
125
+ Field names that must be present.
126
+ """
127
+ missing = sorted(set(required) - set(self.fields.keys()))
128
+ if missing:
129
+ raise ValueError(
130
+ f"SpatialConditions is missing required fields: {missing}. "
131
+ f"Provided: {sorted(self.fields.keys())}"
132
+ )
133
+
134
+
135
+ class OperatingConditions(SpatialConditions):
136
+ """Operating conditions for the 0-D (single stirred tank) case.
137
+
138
+ A single stirred tank has no spatial extent, so the spatially-varying
139
+ :class:`SpatialConditions` (with its ``n_locations`` array model) reads as
140
+ over-machinery for the most basic setup. ``OperatingConditions`` is the same
141
+ object specialised to one location, constructed directly from scalar field
142
+ values::
143
+
144
+ conditions = aquakin.OperatingConditions(pH=7.5, T=293.15)
145
+
146
+ It **is** a :class:`SpatialConditions` (one location), so it works unchanged
147
+ in every reactor; for a spatially varying PFR/CFD case use
148
+ :class:`SpatialConditions` (or :meth:`SpatialConditions.uniform`) directly.
149
+ To start from a model's declared defaults instead, use
150
+ ``model.default_conditions()`` and :meth:`SpatialConditions.with_` to edit.
151
+
152
+ Parameters
153
+ ----------
154
+ **kwargs : float
155
+ Condition field name (e.g. ``pH``, ``T``) -> scalar value.
156
+ """
157
+
158
+ def __init__(self, **kwargs: float) -> None:
159
+ # ``jnp.asarray(value)`` (not ``float(value)``) so a traced condition --
160
+ # a gradient w.r.t. an operating condition -- flows through rather than
161
+ # raising a ConcretizationTypeError. Identity for a concrete value.
162
+ super().__init__(fields={name: jnp.asarray(value) for name, value in kwargs.items()})
@@ -0,0 +1,31 @@
1
+ """Compile-time context used by ASTNode.compile()."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class CompileContext:
10
+ """
11
+ Index maps used during AST compilation.
12
+
13
+ Attributes
14
+ ----------
15
+ species_index : dict[str, int]
16
+ Maps species name to its position in the concentration vector ``C``.
17
+ param_index : dict[str, int]
18
+ Maps fully-namespaced parameter name (e.g. ``"O3_Br_direct.k1"``) to
19
+ its position in the flat ``params`` vector.
20
+ condition_fields : frozenset[str]
21
+ Set of valid condition field names. Used to validate
22
+ ``ConditionNode`` references at parse/compile time.
23
+ reaction_name : str
24
+ The reaction that owns the expression currently being compiled. Used
25
+ for namespacing local parameter names.
26
+ """
27
+
28
+ species_index: dict[str, int]
29
+ param_index: dict[str, int]
30
+ condition_fields: frozenset[str]
31
+ reaction_name: str = ""
aquakin/core/hints.py ADDED
@@ -0,0 +1,32 @@
1
+ """Close-match suggestion for unknown-name errors.
2
+
3
+ A single home for the "Did you mean: ...?" hint appended to ``KeyError`` /
4
+ ``ValueError`` messages when a species / parameter / unit / stream name is not
5
+ found. Callers keep their own message prefix and exception type; this only
6
+ formats the suffix, so the hint wording (and its similarity threshold) is tuned
7
+ in one place.
8
+ """
9
+
10
+ import difflib
11
+
12
+
13
+ def did_you_mean(name, choices, n: int = 3) -> str:
14
+ """Return a ``" Did you mean: a, b?"`` suffix, or ``""`` if nothing is close.
15
+
16
+ Parameters
17
+ ----------
18
+ name : str
19
+ The unknown name the user supplied.
20
+ choices : iterable of str
21
+ The valid names to match against.
22
+ n : int, optional
23
+ Maximum number of suggestions (default 3).
24
+
25
+ Returns
26
+ -------
27
+ str
28
+ A leading-space suffix ready to append to an error message, empty when
29
+ no close match is found.
30
+ """
31
+ matches = difflib.get_close_matches(name, list(choices), n=n)
32
+ return f" Did you mean: {', '.join(matches)}?" if matches else ""