|
| 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 | + ] |
0 commit comments