Skip to content

Commit 2825b17

Browse files
MilanMilan
authored andcommitted
adding tritium module, migrated from core pathsim
1 parent d08aa02 commit 2825b17

11 files changed

Lines changed: 428 additions & 0 deletions

File tree

.DS_Store

6 KB
Binary file not shown.

src/.DS_Store

6 KB
Binary file not shown.

src/pathsim_chem/.DS_Store

6 KB
Binary file not shown.

src/pathsim_chem/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,6 @@
1111
__version__ = "unknown"
1212

1313
__all__ = ["__version__"]
14+
15+
#for direct block import from main package
16+
from .tritium import *

src/pathsim_chem/tritium/.DS_Store

6 KB
Binary file not shown.

src/pathsim_chem/tritium/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Tritium Toolbox
2+
3+
This module contains blocks for nuclear fusion modeling. Especially for tritium cycle modeling.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from .residencetime import *
2+
from .splitter import *
3+
from .bubbler import *
4+
# from .tcap import *
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
#########################################################################################
2+
##
3+
## Bubbler Block
4+
## (blocks/fusion/bubbler.py)
5+
##
6+
#########################################################################################
7+
8+
# IMPORTS ===============================================================================
9+
10+
import numpy as np
11+
12+
from ..dynsys import DynamicalSystem
13+
from ...events.schedule import ScheduleList
14+
15+
16+
# BLOCK DEFIINITIONS ====================================================================
17+
18+
class Bubbler4(DynamicalSystem):
19+
"""
20+
Tritium bubbling system with sequential vial collection stages.
21+
22+
This block models a tritium collection system used in fusion reactor blanket
23+
purge gas processing. The system bubbles tritium-containing gas through a series
24+
of liquid-filled vials to capture and concentrate tritium for measurement and
25+
inventory tracking.
26+
27+
28+
Physical Description
29+
--------------------
30+
The bubbler consists of two parallel processing chains:
31+
32+
**Soluble Chain (Vials 1-2):**
33+
Tritium already in soluble forms (HTO, HT) flows sequentially through
34+
vials 1 and 2. Each vial has a collection efficiency :math:`\\eta_{vial}`,
35+
representing the fraction of tritium that dissolves into the liquid phase
36+
and is retained.
37+
38+
**Insoluble Chain (Vials 3-4):**
39+
Tritium in insoluble forms (T₂, organically bound) first undergoes catalytic
40+
conversion to soluble forms with efficiency :math:`\\alpha_{conv}`. The
41+
converted tritium, along with uncaptured soluble tritium from the first chain,
42+
then flows through vials 3 and 4 with the same collection efficiency.
43+
44+
45+
Mathematical Formulation
46+
-------------------------
47+
The system is governed by the following differential equations for the
48+
vial inventories :math:`x_i`:
49+
50+
.. math::
51+
52+
\\frac{dx_1}{dt} &= \\eta_{vial} \\cdot u_{sol}
53+
54+
\\frac{dx_2}{dt} &= \\eta_{vial} \\cdot (1-\\eta_{vial}) \\cdot u_{sol}
55+
56+
\\frac{dx_3}{dt} &= \\eta_{vial} \\cdot [\\alpha_{conv} \\cdot u_{insol} + (1-\\eta_{vial})^2 \\cdot u_{sol}]
57+
58+
\\frac{dx_4}{dt} &= \\eta_{vial} \\cdot (1-\\eta_{vial}) \\cdot [\\alpha_{conv} \\cdot u_{insol} + (1-\\eta_{vial})^2 \\cdot u_{sol}]
59+
60+
61+
The sample output represents uncaptured tritium exiting the system:
62+
63+
.. math::
64+
65+
y_{sample} = (1-\\alpha_{conv}) \\cdot u_{insol} + (1-\\eta_{vial})^2 \\cdot [\\alpha_{conv} \\cdot u_{insol} + (1-\\eta_{vial})^2 \\cdot u_{sol}]
66+
67+
68+
Where:
69+
- :math:`u_{sol}` = soluble tritium input flow rate
70+
- :math:`u_{insol}` = insoluble tritium input flow rate
71+
- :math:`\\eta_{vial}` = vial collection efficiency
72+
- :math:`\\alpha_{conv}` = conversion efficiency from insoluble to soluble
73+
- :math:`x_i` = tritium inventory in vial i
74+
75+
Parameters
76+
----------
77+
conversion_efficiency : float
78+
Conversion efficiency from insoluble to soluble forms (:math:`\\alpha_{conv}`),
79+
between 0 and 1.
80+
vial_efficiency : float
81+
Collection efficiency of each vial (:math:`\\eta_{vial}`), between 0 and 1.
82+
replacement_times : float | list[float] | list[list[float]]
83+
Times at which each vial is replaced with a fresh one. If None, no
84+
replacement events are created. If a single value is provided, it is
85+
used for all vials. If a single list of floats is provided, it will be
86+
used for all vials. If a list of lists is provided, each sublist
87+
corresponds to the replacement times for each vial.
88+
89+
Notes
90+
-----
91+
Vial replacement is modeled as instantaneous reset events that set the
92+
corresponding vial inventory to zero, simulating the physical replacement
93+
of a full vial with an empty one.
94+
"""
95+
96+
_port_map_out = {
97+
"vial1": 0,
98+
"vial2": 1,
99+
"vial3": 2,
100+
"vial4": 3,
101+
"sample_out": 4,
102+
}
103+
_port_map_in = {
104+
"sample_in_soluble": 0,
105+
"sample_in_insoluble": 1,
106+
}
107+
108+
def __init__(
109+
self,
110+
conversion_efficiency=0.9,
111+
vial_efficiency=0.9,
112+
replacement_times=None,
113+
):
114+
115+
#bubbler parameters
116+
self.replacement_times = replacement_times
117+
self.vial_efficiency = vial_efficiency
118+
self.conversion_efficiency = conversion_efficiency
119+
120+
#dynamical component, ode rhs
121+
def _fn_d(x, u, t):
122+
123+
#short
124+
ve = self.vial_efficiency
125+
ce = self.conversion_efficiency
126+
127+
#unpack inputs
128+
sol, ins = u
129+
130+
#compute vial content change rates
131+
dv1 = ve * sol
132+
dv2 = dv1 * (1 - ve)
133+
dv3 = ve * (ce * ins + (1 - ve)**2 * sol)
134+
dv4 = dv3 * (1 - ve)
135+
136+
return np.array([dv1, dv2, dv3, dv4])
137+
138+
#algebraic output component
139+
def _fn_a(x, u, t):
140+
141+
#short
142+
ve = self.vial_efficiency
143+
ce = self.conversion_efficiency
144+
145+
#unpack inputs
146+
sol, ins = u
147+
148+
sample_out = (1 - ce) * ins + (1 - ve)**2 * (ce * ins + (1 - ve)**2 * sol)
149+
150+
return np.hstack([x, sample_out])
151+
152+
#initialization just like `DynamicalSystem` block
153+
super().__init__(func_dyn=_fn_d, func_alg=_fn_a, initial_value=np.zeros(4))
154+
155+
#create internal vial reset events
156+
self._create_reset_events()
157+
158+
159+
def _create_reset_event_vial(self, i, reset_times):
160+
"""Define event action function and return a `ScheduleList` event
161+
per vial `i` that triggers at predefined `reset_times`.
162+
"""
163+
164+
def reset_vial_i(_):
165+
#get the full engine state
166+
x = self.engine.get()
167+
#set index 'i' to zero
168+
x[i] = 0.0
169+
#set the full engine state
170+
self.engine.set(x)
171+
172+
return ScheduleList(
173+
times_evt=reset_times,
174+
func_act=reset_vial_i
175+
)
176+
177+
178+
def _create_reset_events(self):
179+
"""Create reset events for all vials based on the replacement times.
180+
181+
Raises
182+
------
183+
ValueError : If reset_times is not valid.
184+
185+
Returns
186+
-------
187+
events : list[ScheduleList]
188+
list of reset events for vials
189+
"""
190+
191+
replacement_times = self.replacement_times
192+
self.events = []
193+
194+
# if reset_times is a single list use it for all vials
195+
if replacement_times is None:
196+
return
197+
198+
if isinstance(replacement_times, (int, float)):
199+
replacement_times = [replacement_times]
200+
201+
# if it's a flat list use it for all vials
202+
elif isinstance(replacement_times, list) and all(
203+
isinstance(t, (int, float)) for t in replacement_times
204+
):
205+
replacement_times = [replacement_times] * 4
206+
207+
elif isinstance(replacement_times, np.ndarray) and replacement_times.ndim == 1:
208+
replacement_times = [replacement_times.tolist()] * 4
209+
210+
elif isinstance(replacement_times, list) and len(replacement_times) != 4:
211+
raise ValueError(
212+
"replacement_times must be a single value or a list with the same length as the number of vials"
213+
)
214+
215+
#create the internal events
216+
self.events = [
217+
self._create_reset_event_vial(i, ts) for i, ts in enumerate(replacement_times)
218+
]
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
#########################################################################################
2+
##
3+
## Blocks for residence time modeling
4+
## (blocks/fusion/residencetime.py)
5+
##
6+
#########################################################################################
7+
8+
# IMPORTS ===============================================================================
9+
10+
import numpy as np
11+
12+
from ..dynsys import DynamicalSystem
13+
14+
15+
# BLOCKS ================================================================================
16+
17+
class ResidenceTime(DynamicalSystem):
18+
"""Chemical process block with residence time model.
19+
20+
This block implements an internal 1st order linear ode with
21+
multiple inputs, outputs, an internal constant source term
22+
and no direct passthrough.
23+
24+
The internal ODE with inputs :math:`u_i` :
25+
26+
.. math::
27+
28+
\\dot{x} = - x / \\tau + \\mathrm{src} + \\sum_i \\beta_i u_i
29+
30+
31+
And the output equation for every output `i` :
32+
33+
.. math::
34+
35+
y_i = \\gamma_i x
36+
37+
38+
Parameters
39+
----------
40+
tau : float
41+
residence time, inverse natural frequency (eigenvalue)
42+
betas: None | list[float] | np.ndarray[float]
43+
weights of inputs that are accumulated in state, optional
44+
gammas : None | list[float] | np.ndarray[float]
45+
weights of states (fractions) for output, optional
46+
initial_value : float
47+
initial value of state / initial quantity of process
48+
source_term : float
49+
constant source term / generation term of the process
50+
"""
51+
52+
def __init__(self, tau=1, betas=None, gammas=None, initial_value=0, source_term=0):
53+
54+
#input validation
55+
if np.isclose(tau, 0):
56+
raise ValueError(f"'tau' must be nonzero but is {tau}")
57+
58+
#time constant and input/output weights
59+
self.tau = tau
60+
self.betas = 1 if betas is None else np.array(betas)
61+
self.gammas = 1 if gammas is None else np.array(gammas)
62+
self.source_term = source_term
63+
64+
#rhs of residence time ode
65+
def _fn_d(x, u, t):
66+
return -x/self.tau + self.source_term + sum(self.betas*u)
67+
68+
#jacobian of rhs wrt x
69+
def _jc_d(x, u, t):
70+
return -1/self.tau
71+
72+
#output function of residence time ode
73+
def _fn_a(x, u, t):
74+
return self.gammas * x
75+
76+
#initialization just like `DynamicalSystem` block
77+
super().__init__(func_dyn=_fn_d, jac_dyn=_jc_d, func_alg=_fn_a, initial_value=initial_value)
78+
79+
80+
class Process(ResidenceTime):
81+
"""Simplified version of the `ResidenceTime` model block
82+
with all inputs being summed equally and only the state
83+
and the flux being returned to the output
84+
85+
This block implements an internal 1st order linear ode with
86+
multiple inputs, outputs and no direct passthrough.
87+
88+
The internal ODE with inputs :math:`u_i` :
89+
90+
.. math::
91+
92+
\\dot{x} = - x / \\tau + \\mathrm{src} + \\sum_i u_i
93+
94+
95+
And the output equations for output `i=0` and `i=1`:
96+
97+
.. math::
98+
99+
y_0 = x
100+
101+
.. math::
102+
103+
y_1 = x / \\tau
104+
105+
106+
Parameters
107+
----------
108+
tau : float
109+
residence time, inverse natural frequency (eigenvalue)
110+
initial_value : float
111+
initial value of state / initial quantity of process
112+
source_term : float
113+
constant source term / generation term of the process
114+
"""
115+
116+
#max number of ports
117+
_n_out_max = 2
118+
119+
#maps for input and output port labels
120+
_port_map_out = {"x": 0, "x/tau": 1}
121+
122+
def __init__(self, tau=1, initial_value=0, source_term=0):
123+
super().__init__(tau, 1, [1, 1/tau], initial_value, source_term)

0 commit comments

Comments
 (0)