Skip to content

Commit d1f8bdb

Browse files
hmonroeddCopilot
andcommitted
post processing functions
Co-authored-by: Copilot <copilot@github.com>
1 parent 81c1ae2 commit d1f8bdb

4 files changed

Lines changed: 96 additions & 45 deletions

File tree

sparging101.py

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
from sparging import animation
55
from sparging.model import Simulation
66
from sparging.inputs import (
7-
get_sim_input,
8-
librapi_input_dict,
97
get_sim_input_standard,
108
)
9+
import sparging.postprocess as pp
1110
import logging
1211
from typing import TYPE_CHECKING
1312
import numpy as np
@@ -30,8 +29,8 @@
3029
my_input.get_c_T2_SS().to('molT2/m^3')
3130
}"
3231
)
33-
T_99 = (np.log(100) * my_input.get_tau()).to("seconds")
34-
print(f"T_99% = {T_99.to('hours')}")
32+
tau = my_input.get_tau().to("seconds")
33+
print(f"tau = {tau.to('hours')}")
3534
print(f"Partial pressure number PP = {my_input.get_PP_number()}")
3635

3736

@@ -48,32 +47,30 @@ def profile_source_T(z: pint.Quantity | list[float], height: pint.Quantity = Non
4847
return np.pi / 2 * np.sin(np.pi / height * z) # normalized
4948
else:
5049
raise NotImplementedError("z must be either a float or a pint.Quantity")
51-
# return 0.5 * (1 + np.cos(0.5 * np.pi / (1 * ureg.m) * z))
5250

5351

54-
my_input.profile_source_T = profile_source_T
55-
my_input.signal_irr = lambda t: 1 if t < T_99 else 0
52+
t_final = 50 * ureg.hour
53+
t_irr = 20 * ureg.hour
54+
my_input.signal_irr = lambda t: 1 if t <= t_irr else 0
55+
my_input.signal_sparging = lambda t: 1
5656

5757
my_simulation = Simulation(
5858
my_input,
59-
t_final=2 * T_99,
59+
t_final=t_final,
6060
profile_pressure_hydrostatic=True,
6161
)
6262

6363
if __name__ == "__main__":
6464
# my_simulation.sim_input.E_g *= 1e5
6565
# my_simulation.sim_input.E_l *= 1e-5
6666
output = my_simulation.solve(fast_solve=True)
67-
68-
# # save output to file
69-
# output.profiles_to_csv(f"output_{tank_height}m.csv")
70-
71-
# # plot results
72-
# from sparging import plotting
73-
# plotting.plot_animation(output)
74-
75-
# import matplotlib.pyplot as plt
76-
77-
# plt.plot(output.times, output.inventories_T2_salt)
78-
# plt.show()
67+
output.to_json("temp.json")
68+
popt, pcov = pp.fit_exp(
69+
output.inventories_T2_salt, output.times, 0 * ureg.s, t_irr, phase="rampup"
70+
)
71+
print(f"Fitted parameters: n0 = {popt[1]}, tau = {popt[0].to('h')}")
72+
popt, pcov = pp.fit_exp(
73+
output.inventories_T2_salt, output.times, t_irr, t_final, phase="decay"
74+
)
75+
print(f"Fitted parameters: n0 = {popt[1]}, tau = {popt[0].to('h')}")
7976
animation.create_animation(output, show_activity=False)

src/sparging/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
ureg = UnitRegistry(
66
autoconvert_offset_to_baseunit=True
77
) # to deal with offset units (eg: degree celsius)
8+
ureg.setup_matplotlib(True)
89
ureg.formatter.default_format = ".3e~D"
910
ureg.define("triton = [tritium] = T")
1011
ureg.define(f"molT = {const.N_A} * triton")

src/sparging/model.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def solve(
226226
tank_volume = self.sim_input.volume.to("m**3").magnitude
227227
a = self.sim_input.a.to("1/m").magnitude
228228
h_l = self.sim_input.h_l.to("m/s").magnitude
229-
K_s = self.sim_input.K_s.to("mol/m**3/Pa").magnitude
229+
K_s = self.sim_input.K_s.to("mol/m**3/Pa").magnitude # convert to molT2 ?
230230
P_0 = self.sim_input.P_bottom.to("Pa").magnitude
231231
T = self.sim_input.temperature.to("K").magnitude
232232
eps_g = self.sim_input.eps_g.to("dimensionless").magnitude
@@ -474,6 +474,8 @@ def post_process(t):
474474

475475
# SOLVE
476476
while t < t_final:
477+
# advance time
478+
t += dt
477479
# update time-dependent terms
478480
gen_T2_ave.value = (
479481
Q_T2 / tank_volume * self.sim_input.signal_irr(t * ureg.s)
@@ -487,9 +489,6 @@ def post_process(t):
487489

488490
post_process(t)
489491

490-
# advance time
491-
t += dt
492-
493492
# TODO reattach units using wrapping
494493
# https://pint.readthedocs.io/en/stable/advanced/performance.html#a-safer-method-wrapping
495494
results = SimulationResults(

src/sparging/postprocess.py

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@
22
from sparging.config import ureg
33
from typing import TYPE_CHECKING
44
import numpy as np
5+
import warnings
6+
from scipy.optimize import curve_fit
57

68
if TYPE_CHECKING:
79
import pint
810
from sparging.model import SimulationResults
911
import matplotlib.pyplot as plt
1012

1113

12-
def indices_from_times(sim_output: SimulationResults, times_ref: list[pint.Quantity]):
13-
i_matching = [np.argmin(np.abs(sim_output.times - t)) for t in times_ref]
14-
times_matching = [sim_output.times[i] for i in i_matching]
15-
16-
assert len(i_matching) == len(times_ref)
17-
return i_matching, times_matching
14+
def idx_from_t(times: list[pint.Quantity], timestamp: pint.Quantity) -> pint.Quantity:
15+
idx = np.argmin(np.abs(times - timestamp))
16+
time = times[idx].to(timestamp.units)
17+
if not np.isclose(times[idx], timestamp):
18+
warnings.warn(
19+
f"Requested time {timestamp} does not exactly match discrete times. Using closest time {time} for index calculation."
20+
)
21+
return idx
1822

1923

2024
def plot_profile(
@@ -25,26 +29,76 @@ def plot_profile(
2529
colors: list = None,
2630
**kwargs,
2731
):
28-
i_to_plot, _ = indices_from_times(sim_output, times)
32+
idx_to_plot = [idx_from_t(sim_output.times, t) for t in times]
2933
y_to_plot = getattr(sim_output, var_name)
30-
for j, idx in enumerate(i_to_plot):
34+
for j, idx in enumerate(idx_to_plot):
3135
ax.plot(
3236
sim_output.x_ct,
3337
y_to_plot[idx],
3438
label=f"t = {sim_output.times[idx]:.2f}",
3539
c=colors[j] if colors is not None else None,
3640
**kwargs,
3741
)
38-
ax.legend()
39-
ax.set_xlabel(r"z [m]")
40-
ax.set_ylabel(var_name) # TODO leverage pint for units
41-
ax.set_title(var_name + " profile")
42-
ax.grid()
43-
44-
45-
def plot_signal(sim_output: SimulationResults, ax: plt.Axes, var_name: str, **kwargs):
46-
ax.plot(sim_output.times, getattr(sim_output, var_name), **kwargs)
47-
# ax.set_xlabel(r"t [s]")
48-
# ax.set_ylabel(var_name) # TODO leverage pint for units
49-
# ax.set_title(var_name + " profile")
50-
# ax.grid()
42+
43+
44+
# TODO remove
45+
# def plot_signal(sim_output: SimulationResults, ax: plt.Axes, var_name: str, **kwargs):
46+
# ax.plot(sim_output.times, getattr(sim_output, var_name), **kwargs)
47+
# # ax.set_xlabel(r"t [s]")
48+
# # ax.set_ylabel(var_name) # TODO leverage pint for units
49+
# # ax.set_title(var_name + " profile")
50+
# # ax.grid()
51+
52+
53+
def get_residual_fraction(
54+
T2_inventories: np.ndarray[pint.Quantity],
55+
times: np.ndarray[pint.Quantity],
56+
t_0: pint.Quantity,
57+
t_end: pint.Quantity,
58+
) -> pint.Quantity:
59+
i_0 = idx_from_t(times, t_0)
60+
i_end = idx_from_t(times, t_end)
61+
return T2_inventories[i_end] / T2_inventories[i_0]
62+
63+
64+
def fit_exp(
65+
vec: np.ndarray[pint.Quantity],
66+
times: np.ndarray[pint.Quantity],
67+
t_0: pint.Quantity,
68+
t_end: pint.Quantity,
69+
phase: str,
70+
): # TODO add warning if error or std is too large
71+
"""
72+
- phase = 'decay' or 'rampup'
73+
"""
74+
75+
def fitting_func(t, tau, n0):
76+
match phase:
77+
case "decay":
78+
return n0 * np.exp(-t / tau)
79+
case "rampup":
80+
return n0 * (1 - np.exp(-t / tau))
81+
case _:
82+
raise ValueError("Invalid phase. Must be 'decay' or 'rampup'.")
83+
84+
idx_0 = idx_from_t(times, t_0)
85+
idx_end = idx_from_t(times, t_end)
86+
t_0 = times[idx_0]
87+
t_end = times[idx_end]
88+
print(
89+
f"Fitting from t={t_0.to('hour')} to t={t_end.to('hour')} (indices {idx_0} to {idx_end})"
90+
)
91+
tau_guess = 10000 * ureg.s
92+
n0_guess = vec[idx_0] if phase == "decay" else vec[idx_end]
93+
94+
# wrapped_fitting_func = ureg.wraps(vec.units, (None, vec.units, "s"))(fitting_func)
95+
96+
popt, pcov = curve_fit(
97+
fitting_func,
98+
(times[idx_0 : idx_end + 1] - t_0).to("s").magnitude,
99+
vec[idx_0 : idx_end + 1].magnitude,
100+
p0=[tau_guess.to("s").magnitude, n0_guess.magnitude],
101+
)
102+
print(f"std:{np.sqrt(np.diag(pcov))}")
103+
104+
return (popt[0] * ureg.s, popt[1] * vec.units), pcov

0 commit comments

Comments
 (0)