2323import pint
2424from collections .abc import Callable
2525import logging
26+ from dataclasses import dataclass , field
27+ import warnings
2628
2729logger = 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