Skip to content

Commit 20d06f2

Browse files
committed
added profile exports
1 parent 7ac1da5 commit 20d06f2

2 files changed

Lines changed: 95 additions & 3 deletions

File tree

examples/sparging_standard.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@
4040
constant_profiles=False,
4141
)
4242
if __name__ == "__main__":
43-
# my_simulation.exports = ["pressure", "J_T2"]
43+
my_simulation.exports = ["P_g", "a", "aJ_T2"]
4444
output = my_simulation.solve(fast_solve=True)
45+
output.exports_to_csv(FOLDER)
4546
# save output to file
4647
output.to_json(FOLDER / "params.json")
4748

4849
# P = output.exported_fields["pressure"]
4950
# plt.plot(output.x_ct, P)
5051
# output.exports_to_csv(FOLDER)
51-
# output.export = ["pressure, "]
5252

5353
output.profiles_to_csv(FOLDER)
5454
output.profiles_to_cdf(FOLDER)

src/sparging/model.py

Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import pint
2424
from collections.abc import Callable
2525
import logging
26+
from dataclasses import dataclass, field
27+
import warnings
2628

2729
logger = logging.getLogger(__name__)
2830

@@ -47,11 +49,15 @@ class SimulationResults:
4749
dt: pint.Quantity = None
4850
dx: pint.Quantity = None
4951
sim_input: SimulationInput = None
52+
exports: dict[str, pint.Quantity] = None
53+
"""name -> 2D Quantity, axis 0: time step, axis 1: position (x_export)"""
54+
x_export: np.ndarray[pint.Quantity] = None
5055

5156
keys_to_ignore_results = [
5257
"sim_input",
5358
"dt",
5459
"dx",
60+
"exports", # dict of Quantities: exported via exports_to_csv, not JSON/YAML
5561
]
5662

5763
# Backward-compatibility: old field names -> new (axis-named) fields.
@@ -138,7 +144,8 @@ def to_pickle(self, output_path: Path):
138144
pickle.dump(output, f)
139145

140146
def profiles_to_csv(self, output_directory: Path):
141-
"""Save c_T2 and y_T2 profiles at all time steps as CSV files."""
147+
"""Save c_T2 and y_T2 profiles at all time steps as CSV files.
148+
replaced by exports_to_csv, but kept for legacy"""
142149
times_s = np.array([t.to("seconds").magnitude for t in self.times])
143150
col_names = [f"t={t:.1f}s" for t in times_s]
144151

@@ -214,6 +221,42 @@ def split(q, target_unit=None):
214221
output_directory.mkdir(parents=True, exist_ok=True)
215222
ds.to_netcdf(output_directory / "profiles.nc")
216223

224+
def exports_to_csv(self, output_directory: Path):
225+
"""Write each requested export to its own CSV file (e.g. 'P_g.csv', 'a.csv').
226+
227+
Format (matches the notebook's load_profile_csv):
228+
column 0 : 'x_metres'
229+
columns 1.. : one per time step, named 't=<seconds>s'
230+
A companion '_export_units.json' records the physical unit of each export,
231+
so the plotting script can build axis labels.
232+
233+
Replaces profiles_to_csv (which was hard-coded to c_T2 / y_T2).
234+
"""
235+
if not self.exports:
236+
warnings.warn(
237+
"No exports to write. Set `simulation.exports = [...]` before solving."
238+
)
239+
return
240+
241+
output_directory.mkdir(parents=True, exist_ok=True)
242+
243+
times_s = self.times.to("seconds").magnitude
244+
col_names = [f"t={t:.1f}s" for t in times_s]
245+
246+
units = {}
247+
for name, data in self.exports.items():
248+
df = pd.DataFrame(
249+
np.column_stack([self.x_export.magnitude, data.magnitude.T]),
250+
columns=["x_metres", *col_names],
251+
)
252+
df.to_csv(
253+
output_directory / f"{name}.csv", index=False, float_format="%.6e"
254+
)
255+
units[name] = f"{data.units:~P}"
256+
257+
with open(output_directory / "_export_units.json", "w") as f:
258+
json.dump(units, f, indent=2)
259+
217260
@classmethod
218261
def deserialize_output(cls, data: dict) -> SimulationResults:
219262
results = data.get("results", {})
@@ -250,6 +293,9 @@ class Simulation:
250293
t_final: pint.Quantity
251294
dispersion_on: bool = True
252295
constant_profiles: bool = False
296+
exports: list[str] = field(default_factory=list)
297+
"""Names of quantities to export (must be keys of the export registry built
298+
in `solve`). Each is written to '<name>.csv' by `SimulationResults.exports_to_csv`."""
253299

254300
def normalize_profile(
255301
self, profile: Callable[[float], float] | None, length: float, mesh, func_space
@@ -368,6 +414,8 @@ def solve(
368414
c_T2_init_ufl_expr, V.sub(0).element.interpolation_points
369415
)
370416
)
417+
# make u match u_n at t=0 so interpolated exports are correct at the first step
418+
u.x.array[:] = u_n.x.array[:]
371419

372420
c_T2, y_T2 = ufl.split(u)
373421
c_T2_n, y_T2_n = ufl.split(u_n)
@@ -478,6 +526,41 @@ def solve(
478526
# NOTE currently we don't use x_profile and use another x in the plotting script
479527
x_profile = coords_profile[profile_sort_coords]
480528

529+
# ---- EXPORTS: registry of quantities that can be exported by name ----
530+
# Every entry is a scalar UFL expression on `mesh`; it is interpolated
531+
# into the scalar profile space V_profile and sampled at x_profile.
532+
exportable = {
533+
"c_T2": (c_T2, "molT2/m^3"),
534+
"y_T2": (y_T2, "dimensionless"),
535+
"P_T2": (P_g * y_T2, "Pa"),
536+
"aJ_T2": (aJ_T2, "molT2/m^3/s"),
537+
"P_g": (P_g, "Pa"),
538+
"eps_g": (eps_g, "dimensionless"),
539+
"eps_l": (eps_l, "dimensionless"),
540+
"a": (a, "1/m"),
541+
"u_g": (u_g, "m/s"),
542+
}
543+
544+
unknown = [name for name in self.exports if name not in exportable]
545+
if unknown:
546+
raise ValueError(
547+
f"Cannot export unknown quantities {unknown}. "
548+
f"Available exports: {sorted(exportable)}"
549+
)
550+
551+
export_exprs = {}
552+
export_funcs = {}
553+
export_units = {}
554+
for name in self.exports:
555+
expr_ufl, units = exportable[name]
556+
export_funcs[name] = dolfinx.fem.Function(V_profile)
557+
export_exprs[name] = dolfinx.fem.Expression(
558+
expr_ufl, V_profile.element.interpolation_points
559+
)
560+
export_units[name] = units
561+
562+
export_data = {name: [] for name in self.exports}
563+
481564
# NOTE maybe we could take this function out and it would take a SimulationResults object as input + u + other things...
482565
def post_process(t):
483566
"""
@@ -509,6 +592,10 @@ def post_process(t):
509592
)
510593
n_T2_salt *= tank_area # total amount of T2 in [mol]
511594
n_T2_salt_series.append(n_T2_salt)
595+
for name in self.exports:
596+
export_funcs[name].interpolate(export_exprs[name])
597+
vals = export_funcs[name].x.array[profile_dofs][profile_sort_coords]
598+
export_data[name].append(vals.copy())
512599

513600
t = 0
514601
times = []
@@ -551,5 +638,10 @@ def post_process(t):
551638
sim_input=self.sim_input,
552639
dt=dt * ureg("s"),
553640
dx=dx * ureg("m"),
641+
exports={
642+
name: np.array(export_data[name]) * ureg(export_units[name])
643+
for name in self.exports
644+
},
645+
x_export=x_profile * ureg("m"),
554646
)
555647
return results

0 commit comments

Comments
 (0)