|
1 | 1 | from pathsim.blocks import Block, ODE |
| 2 | +import pathsim.blocks |
| 3 | +from pathsim import Subsystem, Interface, Connection |
2 | 4 | import numpy as np |
3 | 5 |
|
4 | 6 |
|
@@ -35,3 +37,136 @@ def update(self, t): |
35 | 37 | u = self.inputs[0] |
36 | 38 | # mult by fractions and update outputs |
37 | 39 | self.outputs.update_from_array(self.fractions * u) |
| 40 | + |
| 41 | + |
| 42 | +# BUBBLER SYSTEM |
| 43 | + |
| 44 | + |
| 45 | +class Bubbler(Subsystem): |
| 46 | + """Subsystem representing a tritium bubbling system with 4 vials.""" |
| 47 | + |
| 48 | + vial_efficiency: float |
| 49 | + conversion_efficiency: float |
| 50 | + n_soluble_vials: float |
| 51 | + n_insoluble_vials: float |
| 52 | + |
| 53 | + def __init__( |
| 54 | + self, |
| 55 | + conversion_efficiency=0.9, |
| 56 | + vial_efficiency=0.9, |
| 57 | + replacement_times=None, |
| 58 | + ): |
| 59 | + """ |
| 60 | + Args: |
| 61 | + conversion_efficiency: Conversion efficiency from insoluble to soluble (between 0 and 1). |
| 62 | + vial_efficiency: collection efficiency of each vial (between 0 and 1). |
| 63 | + replacement_times: List of times at which each vial is replaced. If None, no replacement |
| 64 | + events are created. If a single value is provided, it is used for all vials. |
| 65 | + If a single list of floats is provided, it will be used for all vials. |
| 66 | + If a list of lists is provided, each sublist corresponds to the replacement times for each vial. |
| 67 | + """ |
| 68 | + self.reset_times = replacement_times |
| 69 | + self.n_soluble_vials = 2 |
| 70 | + self.n_insoluble_vials = 2 |
| 71 | + self.vial_efficiency = vial_efficiency |
| 72 | + col_eff1 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency]) |
| 73 | + vial_1 = pathsim.blocks.Integrator() |
| 74 | + col_eff2 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency]) |
| 75 | + vial_2 = pathsim.blocks.Integrator() |
| 76 | + |
| 77 | + conversion_eff = Splitter( |
| 78 | + n=2, fractions=[conversion_efficiency, 1 - conversion_efficiency] |
| 79 | + ) |
| 80 | + |
| 81 | + col_eff3 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency]) |
| 82 | + vial_3 = pathsim.blocks.Integrator() |
| 83 | + col_eff4 = Splitter(n=2, fractions=[vial_efficiency, 1 - vial_efficiency]) |
| 84 | + vial_4 = pathsim.blocks.Integrator() |
| 85 | + |
| 86 | + add1 = pathsim.blocks.Adder() |
| 87 | + add2 = pathsim.blocks.Adder() |
| 88 | + |
| 89 | + interface = Interface() |
| 90 | + |
| 91 | + self.vials = [vial_1, vial_2, vial_3, vial_4] |
| 92 | + |
| 93 | + blocks = [ |
| 94 | + vial_1, |
| 95 | + col_eff1, |
| 96 | + vial_2, |
| 97 | + col_eff2, |
| 98 | + conversion_eff, |
| 99 | + vial_3, |
| 100 | + col_eff3, |
| 101 | + vial_4, |
| 102 | + col_eff4, |
| 103 | + add1, |
| 104 | + add2, |
| 105 | + interface, |
| 106 | + ] |
| 107 | + connections = [ |
| 108 | + Connection(interface[0], col_eff1), |
| 109 | + Connection(col_eff1[0], vial_1), |
| 110 | + Connection(col_eff1[1], col_eff2), |
| 111 | + Connection(col_eff2[0], vial_2), |
| 112 | + Connection(col_eff2[1], conversion_eff), |
| 113 | + Connection(conversion_eff[0], add1[0]), |
| 114 | + Connection(conversion_eff[1], add2[0]), |
| 115 | + Connection(interface[1], add1[1]), |
| 116 | + Connection(add1, col_eff3), |
| 117 | + Connection(col_eff3[0], vial_3), |
| 118 | + Connection(col_eff3[1], col_eff4), |
| 119 | + Connection(col_eff4[0], vial_4), |
| 120 | + Connection(col_eff4[1], add2[1]), |
| 121 | + Connection(vial_1, interface[0]), |
| 122 | + Connection(vial_2, interface[1]), |
| 123 | + Connection(vial_3, interface[2]), |
| 124 | + Connection(vial_4, interface[3]), |
| 125 | + Connection(add2, interface[4]), |
| 126 | + ] |
| 127 | + super().__init__(blocks, connections) |
| 128 | + |
| 129 | + def _create_reset_events_one_vial( |
| 130 | + self, block, reset_times |
| 131 | + ) -> list[pathsim.blocks.Schedule]: |
| 132 | + events = [] |
| 133 | + |
| 134 | + def reset_itg(_): |
| 135 | + block.reset() |
| 136 | + |
| 137 | + for t in reset_times: |
| 138 | + events.append( |
| 139 | + pathsim.blocks.Schedule(t_start=t, t_end=t, func_act=reset_itg) |
| 140 | + ) |
| 141 | + return events |
| 142 | + |
| 143 | + def create_reset_events(self) -> list[pathsim.blocks.Schedule]: |
| 144 | + """Create reset events for all vials based on the replacement times. |
| 145 | +
|
| 146 | + Raises: |
| 147 | + ValueError: If reset_times is not valid. |
| 148 | +
|
| 149 | + Returns: |
| 150 | + list of reset events. |
| 151 | + """ |
| 152 | + reset_times = self.reset_times |
| 153 | + events = [] |
| 154 | + # if reset_times is a single list use it for all vials |
| 155 | + if reset_times is None: |
| 156 | + return events |
| 157 | + if isinstance(reset_times, (int, float)): |
| 158 | + reset_times = [reset_times] |
| 159 | + # if it's a flat list use it for all vials |
| 160 | + elif isinstance(reset_times, list) and all( |
| 161 | + isinstance(t, (int, float)) for t in reset_times |
| 162 | + ): |
| 163 | + reset_times = [reset_times] * len(self.vials) |
| 164 | + elif isinstance(reset_times, np.ndarray) and reset_times.ndim == 1: |
| 165 | + reset_times = [reset_times.tolist()] * len(self.vials) |
| 166 | + elif isinstance(reset_times, list) and len(reset_times) != len(self.vials): |
| 167 | + raise ValueError( |
| 168 | + "reset_times must be a single value or a list with the same length as the number of vials" |
| 169 | + ) |
| 170 | + for i, vial in enumerate(self.vials): |
| 171 | + events.extend(self._create_reset_events_one_vial(vial, reset_times[i])) |
| 172 | + return events |
0 commit comments