pyregen 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyregen/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from .core import Regenerator, RegenState, RegenSolver, RegenError
2
+
3
+ __version__ = "0.1.0"
pyregen/core.py ADDED
@@ -0,0 +1,903 @@
1
+ import numpy as np
2
+ import CoolProp.CoolProp as CP
3
+ import scipy.integrate as integrate
4
+ import scipy.linalg as la
5
+
6
+ class RegenError(Exception):
7
+ "Custom Exception for if this library screws up"
8
+ pass
9
+
10
+ def helium_properties(Pressure: float | np.ndarray,
11
+ Temperature: float | np.ndarray,
12
+ Hydraulic_Diameter: float | np.ndarray,
13
+ Ideal_Gas: bool = False,
14
+ Isotope: int = 4) -> dict:
15
+ """
16
+ Pressure (Pa)
17
+ Temperature (K)
18
+ Hydraulic Diameter (m)
19
+ """
20
+
21
+ if Isotope not in (3, 4):
22
+ raise ValueError(f"Isotope must be 3 or 4. Got {Isotope}")
23
+
24
+ if np.any(Temperature <= 0):
25
+ raise ValueError(f"Temperature ({Temperature} K) cannot be negative.")
26
+
27
+ fluid = 'Helium' if Isotope == 4 else 'Helium3'
28
+
29
+ if Ideal_Gas:
30
+ if Isotope == 3:
31
+ Gas_Constant = 2756.6313
32
+ Cp = 6891.3715
33
+ Cv = 4134.7402
34
+ else:
35
+ Gas_Constant = 2077.1499
36
+ Cp = 5192.7190
37
+ Cv = 3115.5691
38
+
39
+ Density = Pressure / (Gas_Constant * Temperature)
40
+ Internal_Energy = Cv * Temperature
41
+ Enthalpy = Cp * Temperature
42
+
43
+ Cp = np.full_like(Temperature, Cp) if isinstance(Temperature, np.ndarray) else Cp
44
+
45
+ Viscosity = CP.PropsSI('V', 'T', Temperature, 'P', Pressure, fluid)
46
+ Thermal_Conductivity = (Viscosity * Cp) / 0.67
47
+
48
+ else:
49
+ Density = CP.PropsSI('D', 'T', Temperature, 'P', Pressure, fluid)
50
+ Internal_Energy = CP.PropsSI('U', 'T', Temperature, 'P', Pressure, fluid)
51
+ Enthalpy = CP.PropsSI('H', 'T', Temperature, 'P', Pressure, fluid)
52
+ Cp = CP.PropsSI('C', 'T', Temperature, 'P', Pressure, fluid)
53
+ Viscosity = CP.PropsSI('V', 'T', Temperature, 'P', Pressure, fluid)
54
+ Thermal_Conductivity = CP.PropsSI('L', 'T', Temperature, 'P', Pressure, fluid)
55
+
56
+ Heat_Transfer_Base = (Viscosity * Cp * (Thermal_Conductivity)**2)**(1/3) / Hydraulic_Diameter
57
+ Friction_Base = (2.0 * Viscosity**2) / (Hydraulic_Diameter**3 * Density)
58
+
59
+ return {'Density': Density, # kg/m3
60
+ 'Internal Energy': Internal_Energy, #J/kg
61
+ 'Enthalpy': Enthalpy, #J/kg
62
+ 'Cp': Cp, #J/KgK
63
+ 'Viscosity': Viscosity, #kg/ms
64
+ 'Thermal Conductivity': Thermal_Conductivity, # W/mK
65
+ 'Heat Transfer Base': Heat_Transfer_Base, # W/m2K
66
+ 'Friction Base': Friction_Base # Pa/m
67
+ }
68
+
69
+ def material_properties(Material_Name: str,
70
+ Temperature: float | np.ndarray,
71
+ Cp_Factor: float = 1.0,
72
+ Conductivity_Factor:float = 1.0) -> dict:
73
+
74
+ '''
75
+ Temperature (K)
76
+ '''
77
+
78
+ if np.any(Temperature <= 0):
79
+ raise ValueError(f'Temperature ({Temperature} K) cannot be negative.')
80
+
81
+ Temperature = np.asarray(Temperature)
82
+
83
+ match Material_Name.strip().lower():
84
+ case 'stainless steel' | 'ss':
85
+ Temperature_samples = [0.0, 3.0, 4.0, 6.0, 8.0, 10.0, 15.0, 20.0, 25.0, 30.0, 40.0, 50.0, 60.0, 80.0, 100.0, 150.0, 200.0, 1000.0]
86
+ Cp_samples = [0.0, 0.0117, 0.0151, 0.0221, 0.0311, 0.0411, 0.067, 0.104, 0.160, 0.226, 0.442, 0.759, 1.11, 1.710, 2.09, 2.91, 3.29, 7.05]
87
+ Conductivity_Samples = [0.0, 0.00182, 0.00270, 0.00475, 0.00675, 0.00895, 0.0152, 0.0216, 0.0273, 0.0340, 0.0457, 0.0565, 0.0665, 0.081, 0.090, 0.108, 0.123, 0.315]
88
+
89
+ Raw_Volumetric_Heating_Capacity = np.interp(Temperature, Temperature_samples, Cp_samples)
90
+ Raw_Thermal_Conductivity = np.interp(Temperature, Temperature_samples, Conductivity_Samples)
91
+ case 'lead' | 'pb':
92
+ Temperature_samples = [0.0, 3.0, 4.0, 6.0, 8.0, 10.0, 15.0, 20.0, 25.0, 30.0, 40.0, 50.0, 60.0, 80.0, 100.0, 150.0, 200.0, 1000.0]
93
+ Cp_samples = [0.0, 0.0035, 0.0079, 0.0340, 0.0828, 0.155, 0.380, 0.602, 0.772, 0.903, 1.070, 1.168, 1.224, 1.293, 1.338, 1.383, 1.418, 1.834]
94
+ Conductivity_Samples = [0.0, 0.020, 0.028, 0.045, 0.061, 0.074, 0.104, 0.122, 0.140, 0.155, 0.176, 0.188, 0.204, 0.222, 0.230, 0.256, 0.270, 0.414]
95
+
96
+ Raw_Volumetric_Heating_Capacity = np.interp(Temperature, Temperature_samples, Cp_samples)
97
+ Raw_Thermal_Conductivity = np.interp(Temperature, Temperature_samples, Conductivity_Samples)
98
+ case 'er3-ni' | 'er3ni':
99
+
100
+ sample_conditions = [
101
+ Temperature > 30.0,
102
+ Temperature > 20.0,
103
+ Temperature > 7.136,
104
+ Temperature > 4.0,
105
+ Temperature <= 4.0
106
+ ]
107
+
108
+ Cp_samples = [
109
+ 1.5792 - 23.1297 / Temperature,
110
+ 0.04257 + 0.02552 * Temperature,
111
+ 1.0832 - 0.1667 * Temperature + 0.01117 * Temperature**2 - 0.0002082 * Temperature**3,
112
+ -0.225 + 0.08574 * Temperature,
113
+ 10 ** (2.5 * np.log10(Temperature) - 2.436)
114
+ ]
115
+
116
+ Raw_Volumetric_Heating_Capacity = np.select(sample_conditions, Cp_samples)
117
+
118
+ sample_conditions_conductivity = [Temperature > 20.7, Temperature <= 20.7]
119
+
120
+ Conductivity_Samples = [
121
+ 0.009246 + 0.001488 * Temperature**0.5 - 1.6426 / Temperature**2,
122
+ np.exp(-8.079 + 1.2116 * np.log(Temperature))
123
+ ]
124
+
125
+ Raw_Thermal_Conductivity = np.select(sample_conditions_conductivity, Conductivity_Samples)
126
+
127
+ case _:
128
+ raise NotImplementedError(f"Material '{Material_Name}' not yet implemented.")
129
+
130
+
131
+ Raw_Volumetric_Heating_Capacity *= 1.0e6
132
+ Raw_Thermal_Conductivity *= 100.0
133
+
134
+
135
+ return {
136
+ 'Volumetric Heat Capacity': Raw_Volumetric_Heating_Capacity * Cp_Factor, # J/m3K
137
+ 'Thermal Conductivity': Raw_Thermal_Conductivity * Conductivity_Factor # W/mK
138
+ }
139
+
140
+ def Flow_Properties(Geometry: str,
141
+ Hydraulic_Diameter: float | np.ndarray,
142
+ Porosity: float | np.ndarray,
143
+ Density: float | np.ndarray,
144
+ Viscosity: float | np.ndarray,
145
+ Velocity: float | np.ndarray,
146
+ Heat_Transfer_Base: float | np.ndarray,
147
+ Friction_Base: float | np.ndarray,
148
+ Gas_Temperature: float | np.ndarray,
149
+ Matrix_Temperature: float | np.ndarray,
150
+ Heat_Transfer_Factor: float = 1.0,
151
+ Pressure_Gradient_Factor: float = 1.0) -> dict:
152
+
153
+ '''
154
+ Hydraulic_Diameter (m)
155
+ Density (kg/m3)
156
+ Viscosity (kg/ms)
157
+ Velocity (m/s)
158
+ Heat_Transfer_Base (W/m2K)
159
+ Friction_Base (Pa/m)
160
+ Gas_Temperature (K)
161
+ '''
162
+
163
+ is_scalar = np.isscalar(Velocity)
164
+
165
+ Porosity = np.atleast_1d(Porosity)
166
+ Density = np.atleast_1d(Density)
167
+ Viscosity = np.atleast_1d(Viscosity)
168
+ Velocity = np.atleast_1d(Velocity)
169
+ Heat_Transfer_Base = np.atleast_1d(Heat_Transfer_Base)
170
+ Friction_Base = np.atleast_1d(Friction_Base)
171
+ Gas_Temperature = np.atleast_1d(Gas_Temperature)
172
+ Matrix_Temperature = np.atleast_1d(Matrix_Temperature)
173
+
174
+ Reynolds = np.maximum(1e-10, (Hydraulic_Diameter * Density * np.abs(Velocity)) / Viscosity)
175
+
176
+ Geometry_Correleation_Factor = 0.0
177
+ Friction_Modifier = 0.0
178
+ Modified_Stanton_Number = 0.0
179
+ Heat_Transfer_Coefficient = 0.0
180
+
181
+ match Geometry.strip().lower():
182
+ case 'parallel plates' | '1':
183
+ Friction_Modifier = 0.33 + 0.09 / (1.0 + np.exp((3500.0 - Reynolds) / 1500.0))
184
+
185
+ laminar_mask = Reynolds <= 2000.0
186
+ turbulent_mask = ~laminar_mask
187
+
188
+ Heat_Transfer_Coefficient = np.zeros_like(Reynolds)
189
+ Modified_Stanton_Number = np.zeros_like(Reynolds)
190
+
191
+ Heat_Transfer_Coefficient[laminar_mask] = Heat_Transfer_Base[laminar_mask]* 8.5
192
+ Modified_Stanton_Number[laminar_mask] = 8.5 * Reynolds[laminar_mask]
193
+
194
+ Heat_Transfer_Coefficient[turbulent_mask] = Heat_Transfer_Base[turbulent_mask] * (3.4e-3 * Reynolds[turbulent_mask] + 2.72e13*Reynolds[turbulent_mask]**-4)
195
+ Modified_Stanton_Number[turbulent_mask] = (3.4e-3 * Reynolds[turbulent_mask]**2 + 2.72e13 * Reynolds[turbulent_mask]**-3)
196
+
197
+ case 'tubes' | '2':
198
+ Friction_Modifier = 0.27 + 0.16 / (1.0 + np.exp((5500.0 - Reynolds) / 2000.0))
199
+
200
+ laminar_mask = Reynolds <= 2000.0
201
+ turbulent_mask = ~laminar_mask
202
+
203
+ Heat_Transfer_Coefficient = np.zeros_like(Reynolds)
204
+ Modified_Stanton_Number = np.zeros_like(Reynolds)
205
+
206
+ Heat_Transfer_Coefficient[laminar_mask] = Heat_Transfer_Base[laminar_mask]* 4.2
207
+ Modified_Stanton_Number[laminar_mask] = 4.2 * Reynolds[laminar_mask]
208
+
209
+ Heat_Transfer_Coefficient[turbulent_mask] = Heat_Transfer_Base[turbulent_mask] * (1.68e-3 * Reynolds[turbulent_mask] + 1.344e13*Reynolds[turbulent_mask]**-4)
210
+ Modified_Stanton_Number[turbulent_mask] = (1.68e-3 * Reynolds[turbulent_mask]**2 + 1.344e13 * Reynolds[turbulent_mask]**-3)
211
+
212
+ case 'screens' | '4':
213
+ Geometry_Correleation_Factor = 0.715 * (5.6 + Porosity * (-16.363 + 13.928 * Porosity))
214
+ Friction_Modifier = np.zeros_like(Reynolds)
215
+ Friction_Modifier[Reynolds < 10.0] = 0.0074 * Reynolds[Reynolds < 10.0]
216
+
217
+ mask_mid = (Reynolds >= 10.0) & (Reynolds < 3000.0)
218
+ friction_mid = np.log(Reynolds[mask_mid] / 200.0)
219
+ Friction_Modifier[mask_mid] = 0.129 - 0.0058 * friction_mid**2
220
+
221
+ mask_high = Reynolds >= 3000.0
222
+ friction_high = np.log(Reynolds[mask_high] / 200.0)
223
+ Friction_Modifier[mask_high] = 0.1498 - 0.0239 * friction_high
224
+
225
+ heat_transfer_alpha = 0.04
226
+ heat_transfer_gamma = 2.0
227
+ beta = 0.43
228
+ alpha = 1.0 - beta
229
+
230
+ blend = np.exp(-np.minimum(50.0, heat_transfer_alpha*Reynolds**2))
231
+ expr = np.exp(alpha*np.log(Reynolds))
232
+
233
+ Heat_Transfer_Coefficient = Geometry_Correleation_Factor * Heat_Transfer_Base * (heat_transfer_gamma*blend+(1.0-blend)*expr)
234
+ Modified_Stanton_Number = Geometry_Correleation_Factor * Reynolds * expr
235
+
236
+ case _:
237
+ raise NotImplementedError(f"Geometry '{Geometry}' not yet implemented.")
238
+
239
+ Pressure_Gradient = -np.sign(Velocity) * Friction_Base * Modified_Stanton_Number / Friction_Modifier
240
+ Pressure_Gradient *= Pressure_Gradient_Factor
241
+
242
+ Heat_Transfer_Rate = 4.0 * Heat_Transfer_Factor * Heat_Transfer_Coefficient * (Matrix_Temperature - Gas_Temperature) / Hydraulic_Diameter
243
+
244
+ if is_scalar:
245
+ Pressure_Gradient = Pressure_Gradient.item()
246
+ Heat_Transfer_Rate = Heat_Transfer_Rate.item()
247
+ Reynolds = Reynolds.item()
248
+
249
+ return {
250
+ 'Pressure Gradient': Pressure_Gradient, # Pa/m
251
+ 'Heat Transfer Rate': Heat_Transfer_Rate, # W/m3
252
+ 'Reynolds Number': Reynolds
253
+ }
254
+
255
+ class RegenState:
256
+ def __init__(self,
257
+ Number_of_Nodes: int,
258
+ Length: float,
259
+ Area: float,
260
+ Porosity: float,
261
+ Hydraulic_Diameter: float,
262
+ bdy_order: int = 1):
263
+
264
+ '''
265
+ Length (m)
266
+ Area (m2)
267
+ Hydraulic Diameter (m)
268
+ '''
269
+
270
+ self.bdy_order = bdy_order
271
+
272
+ self.length = Length
273
+ self.porosity = Porosity
274
+ self.hydraulic_diameter = Hydraulic_Diameter
275
+
276
+
277
+ self.num_nodes = Number_of_Nodes
278
+ self.num_nodes_mid = Number_of_Nodes - 1
279
+
280
+ self.x_endpoints = np.linspace(0.0, Length, Number_of_Nodes)
281
+ self.dz = Length / (Number_of_Nodes - 1)
282
+
283
+ self.x_midpoints = np.zeros(Number_of_Nodes + 1)
284
+ self.x_midpoints[0] = 0.0
285
+ self.x_midpoints[Number_of_Nodes] = Length
286
+ self.x_midpoints[1:Number_of_Nodes] = 0.5 * (self.x_endpoints[1:] + self.x_endpoints[:-1])
287
+
288
+ self.dxs = np.zeros(Number_of_Nodes)
289
+ self.dxs[1:Number_of_Nodes] = self.x_endpoints[1:] - self.x_endpoints[:-1]
290
+
291
+ self.area_mid = np.full(Number_of_Nodes + 1, Area)
292
+ self.porosity_mid = np.full(Number_of_Nodes + 1, Porosity)
293
+ self.hydra_diam_mid = np.full(Number_of_Nodes + 1, Hydraulic_Diameter)
294
+
295
+ self.porosity_area_mid = self.porosity_mid * self.area_mid
296
+ self.solid_area_mid = (1.0 - self.porosity_mid) * self.area_mid
297
+
298
+ self.porosity_area_nodes = np.zeros(Number_of_Nodes)
299
+ self.porosity_area_nodes[1:-1] = 0.5 * (self.porosity_area_mid[1:-2] + self.porosity_area_mid[2:-1])
300
+ self.porosity_area_nodes[0] = self.porosity_area_mid[0]
301
+ self.porosity_area_nodes[-1] = self.porosity_area_mid[-1]
302
+
303
+ self.mass_flux_nodes = np.zeros((Number_of_Nodes, 3))
304
+ self.pressure_nodes = np.zeros((Number_of_Nodes, 3))
305
+ self.matrix_temp_mid = np.zeros((Number_of_Nodes + 1, 3))
306
+ self.gas_temp_mid = np.zeros((Number_of_Nodes + 1, 3))
307
+
308
+ self.gas_density_mid = np.zeros((Number_of_Nodes + 1, 3))
309
+ self.gas_energy_vol_mid = np.zeros((Number_of_Nodes + 1, 3))
310
+ self.gas_mass_flux_mid = np.zeros((Number_of_Nodes + 1, 3))
311
+ self.matrix_enthalpy_mid = np.zeros((Number_of_Nodes + 1, 3))
312
+
313
+ self.time_rev_hot = 0.0
314
+ self.time_rev_cold = 0.0
315
+ self.temp_rev_hot = 0.0
316
+ self.temp_rev_cold = 0.0
317
+
318
+ def get_matrix_enthalpy(self, material_name: str, temp_array: np.ndarray, cp_factor: float = 1.0) -> np.ndarray:
319
+ if not hasattr(self, '_enthalpy_T_table'):
320
+ self._enthalpy_T_table = np.logspace(-2, 3, 5000)
321
+ cp_array = material_properties(material_name, self._enthalpy_T_table, cp_factor)['Volumetric Heat Capacity']
322
+
323
+ self._enthalpy_H_table = integrate.cumulative_trapezoid(cp_array, self._enthalpy_T_table, initial=0.0)
324
+
325
+ return np.interp(temp_array, self._enthalpy_T_table, self._enthalpy_H_table)
326
+
327
+ def extrap_left(self, array_mid: np.ndarray) -> float:
328
+ if self.bdy_order == 0:
329
+ return array_mid[1]
330
+ elif self.bdy_order == 1:
331
+ return 1.5 * array_mid[1] - 0.5 * array_mid[2]
332
+ else:
333
+ return (15.0 * array_mid[1] - 10.0 * array_mid[2] + 3.0 * array_mid[3]) / 8.0
334
+
335
+ def extrap_right(self, array_mid: np.ndarray) -> float:
336
+ n = self.num_nodes
337
+ if self.bdy_order == 0:
338
+ return array_mid[n-1]
339
+ elif self.bdy_order == 1:
340
+ return 1.5 * array_mid[n-1] - 0.5 * array_mid[n-2]
341
+ else:
342
+ return (15.0 * array_mid[n-1] - 10.0 * array_mid[n-2] + 3.0 * array_mid[n-3]) / 8.0
343
+
344
+ def derivative_left(self, array_mid: np.ndarray) -> float:
345
+ if self.bdy_order == 1:
346
+ return (array_mid[2] - array_mid[1]) / self.dz
347
+ else:
348
+ return (-2.0 * array_mid[1] + 3.0 * array_mid[2] - array_mid[3]) / self.dz
349
+
350
+ def derivative_right(self, array_mid: np.ndarray) -> float:
351
+ n = self.num_nodes
352
+ if self.bdy_order == 1:
353
+ return (array_mid[n-1] - array_mid[n-2]) / self.dz
354
+ else:
355
+ return (2.0 * array_mid[n-1] - 3.0 * array_mid[n-2] + array_mid[n-3]) / self.dz
356
+
357
+
358
+ class RegenSolver:
359
+ def __init__(self, state,
360
+ Geometry: str,
361
+ Material: str,
362
+ Frequency: float,
363
+ Mass_Flux_Hot: float,
364
+ Mass_Flux_Cold: float,
365
+ Mass_Phase_Deg: float,
366
+ Gas_Temp_Hot: float,
367
+ Gas_Temp_Cold: float,
368
+ Decay_Time: float = 0.05,
369
+ Use_Advection: bool = True,
370
+ Gas_Isotope: int = 4,
371
+ Ideal_Gas: bool = False,
372
+ Time_Step: float = 0.001,
373
+ cp_factor: float = 1.0,
374
+ cond_factor: float = 1.0,
375
+ eps_newton: float = 1e-6,
376
+ max_newton_iters: int = 4,
377
+ mflux_dc: float = 0.0
378
+ ):
379
+
380
+ self.state = state
381
+ self.geometry = Geometry
382
+ self.material = Material
383
+ self.isotope = Gas_Isotope
384
+ self.ideal_gas = Ideal_Gas
385
+ self.dt = Time_Step
386
+
387
+ self.frequency = Frequency
388
+ self.omega = 2.0 * np.pi * Frequency
389
+ self.mass_flux_hot = Mass_Flux_Hot
390
+ self.mass_flux_cold = Mass_Flux_Cold
391
+ self.mass_phase_rad = np.radians(Mass_Phase_Deg)
392
+ self.gas_temp_hot = Gas_Temp_Hot
393
+ self.gas_temp_cold = Gas_Temp_Cold
394
+ self.decay = Decay_Time
395
+ self.use_advec = 1.0 if Use_Advection else 0.0
396
+
397
+ self.cp_factor = cp_factor
398
+ self.cond_factor = cond_factor
399
+ self.eps_newton = eps_newton
400
+ self.max_newton_iters = max_newton_iters
401
+ self.mflux_dc = mflux_dc
402
+
403
+ self.nvars = 4
404
+ self.neqn = self.nvars * self.state.num_nodes - 2
405
+
406
+ def calculate_residuals(self,
407
+ guess_array: np.ndarray,
408
+ time: float,
409
+ method_order: int = 2) -> np.ndarray:
410
+
411
+ nx = self.state.num_nodes
412
+ dz = self.state.dz
413
+ dxs = np.diff(self.state.x_endpoints)
414
+
415
+ mfxi = guess_array[0::4]
416
+ prsi = guess_array[1::4]
417
+ mtph = guess_array[2::4]
418
+ gtph = guess_array[3::4]
419
+
420
+ mtph_padded = np.zeros(nx + 1)
421
+ gtph_padded = np.zeros(nx + 1)
422
+ mtph_padded[1:nx] = mtph
423
+ gtph_padded[1:nx] = gtph
424
+
425
+ if mfxi[0] >= 1e-8:
426
+ decay_factor = np.exp(-(time - self.state.time_rev_hot) * self.frequency / self.decay)
427
+ gtph_padded[0] = self.gas_temp_hot + (self.state.temp_rev_hot - self.gas_temp_hot) * decay_factor
428
+ else:
429
+ gtph_padded[0] = self.state.extrap_left(gtph_padded)
430
+
431
+ if mfxi[-1] <= -1e-8:
432
+ decay_factor = np.exp(-(time - self.state.time_rev_cold) * self.frequency / self.decay)
433
+ gtph_padded[nx] = self.gas_temp_cold + (self.state.temp_rev_cold - self.gas_temp_cold) * decay_factor
434
+ else:
435
+ gtph_padded[nx] = max(1.0, self.state.extrap_right(gtph_padded))
436
+
437
+ mtph_padded[0] = self.state.extrap_left(mtph_padded)
438
+ mtph_padded[nx] = max(1.0, self.state.extrap_right(mtph_padded))
439
+
440
+ prsi_padded = np.zeros(nx + 1)
441
+ prsi_padded[1:nx] = 0.5 * (prsi[:-1] + prsi[1:])
442
+ prsi_padded[0] = prsi[0]
443
+ prsi_padded[-1] = prsi[-1]
444
+
445
+ gas_props = helium_properties(
446
+ Pressure=prsi_padded,
447
+ Temperature=gtph_padded,
448
+ Hydraulic_Diameter=self.state.hydra_diam_mid,
449
+ Ideal_Gas=self.ideal_gas,
450
+ Isotope=self.isotope
451
+ )
452
+
453
+ solid_props = material_properties(self.material, mtph_padded, self.cp_factor, self.cond_factor)
454
+ matrix_enthalpy = self.state.get_matrix_enthalpy(self.material, mtph_padded, self.cp_factor)
455
+
456
+ den_endpoints = np.zeros(nx)
457
+ den_endpoints[1:-1] = 0.5 * (gas_props['Density'][1:-2] + gas_props['Density'][2:-1])
458
+ den_endpoints[0] = gas_props['Density'][0]
459
+ den_endpoints[-1] = gas_props['Density'][-1]
460
+
461
+ vel_endpoints = mfxi / (self.state.porosity_area_nodes * den_endpoints)
462
+
463
+ vel_midpoints_padded = np.zeros(nx + 1)
464
+ vel_midpoints_padded[1:nx] = 0.5 * (mfxi[:-1] + mfxi[1:]) / (self.state.porosity_area_mid[1:nx] * gas_props['Density'][1:nx])
465
+ vel_midpoints_padded[0] = mfxi[0] / (self.state.porosity_area_mid[0] * gas_props['Density'][0])
466
+ vel_midpoints_padded[-1] = mfxi[-1] / (self.state.porosity_area_mid[-1] * gas_props['Density'][-1])
467
+
468
+ flow_props = Flow_Properties(
469
+ Geometry=self.geometry,
470
+ Hydraulic_Diameter=self.state.hydra_diam_mid[1:nx],
471
+ Porosity=self.state.porosity_mid[1:nx],
472
+ Density=gas_props['Density'][1:nx],
473
+ Viscosity=gas_props['Viscosity'][1:nx],
474
+ Velocity=vel_midpoints_padded[1:nx],
475
+ Heat_Transfer_Base=gas_props['Heat Transfer Base'][1:nx],
476
+ Friction_Base=gas_props['Friction Base'][1:nx],
477
+ Gas_Temperature=gtph_padded[1:nx],
478
+ Matrix_Temperature=mtph_padded[1:nx]
479
+ )
480
+
481
+ kA_solid = solid_props['Thermal Conductivity'] * self.state.solid_area_mid
482
+ mcdi = 2.0 * kA_solid[1:-2] * kA_solid[2:-1] / (dxs[1:] * kA_solid[1:-2] + dxs[:-1] * kA_solid[2:-1])
483
+ hfx_internal = -mcdi * (mtph[1:] - mtph[:-1])
484
+ hfx_0 = -kA_solid[0] * self.state.derivative_left(mtph_padded)
485
+ hfx_L = -kA_solid[-1] * self.state.derivative_right(mtph_padded)
486
+ hfx = np.concatenate(([hfx_0], hfx_internal, [hfx_L]))
487
+
488
+ kA_gas = gas_props['Thermal Conductivity'] * self.state.porosity_area_mid
489
+ gcdi = 2.0 * kA_gas[1:-2] * kA_gas[2:-1] / (dxs[1:] * kA_gas[1:-2] + dxs[:-1] * kA_gas[2:-1])
490
+ gfx_internal = -gcdi * (gtph[1:] - gtph[:-1])
491
+ gfx_0 = -kA_gas[0] * self.state.derivative_left(gtph_padded)
492
+ gfx_L = -kA_gas[-1] * self.state.derivative_right(gtph_padded)
493
+ gfx = np.concatenate(([gfx_0], gfx_internal, [gfx_L]))
494
+
495
+ enti = np.zeros(nx)
496
+ enti[1:-1] = 0.5 * (gas_props['Enthalpy'][1:-2] + gas_props['Enthalpy'][2:-1])
497
+ enti[0] = gas_props['Enthalpy'][0]
498
+ enti[-1] = gas_props['Enthalpy'][-1]
499
+ eng_flux = mfxi * (enti + 0.5 * vel_endpoints**2) + gfx
500
+
501
+ gas_energy_vol = gas_props['Density'] * gas_props['Internal Energy'] + 0.5 * gas_props['Density'] * vel_midpoints_padded**2
502
+
503
+ if method_order == 1:
504
+ c3, c2, c1 = 1.0, -1.0, 0.0
505
+ else:
506
+ c3, c2, c1 = 1.5, -2.0, 0.5
507
+
508
+ dt_mtp = (c3 * matrix_enthalpy[1:nx] +
509
+ c2 * self.state.matrix_enthalpy_mid[1:nx, 1] +
510
+ c1 * self.state.matrix_enthalpy_mid[1:nx, 0]) / self.dt
511
+ dt_den = (c3 * gas_props['Density'][1:nx] + c2 * self.state.gas_density_mid[1:nx, 1] + c1 * self.state.gas_density_mid[1:nx, 0]) / self.dt
512
+ dt_eng = (c3 * gas_energy_vol[1:nx] + c2 * self.state.gas_energy_vol_mid[1:nx, 1] + c1 * self.state.gas_energy_vol_mid[1:nx, 0]) / self.dt
513
+
514
+ mfxh = 0.5 * (mfxi[:-1] + mfxi[1:])
515
+ dt_adv = (c3 * mfxh + c2 * self.state.gas_mass_flux_mid[1:nx, 1] + c1 * self.state.gas_mass_flux_mid[1:nx, 0]) / self.dt
516
+ advec = dz * dt_adv + vel_endpoints[1:] * mfxi[1:] - vel_endpoints[:-1] * mfxi[:-1]
517
+
518
+ res = np.zeros_like(guess_array)
519
+
520
+ res[0::4][:-1] = self.state.porosity_area_mid[1:nx] * (prsi[1:] - prsi[:-1] - dz * flow_props['Pressure Gradient']) + (advec * self.use_advec)
521
+
522
+ res[3::4] = dz * (self.state.porosity_area_mid[1:nx] * dt_den) + (mfxi[1:] - mfxi[:-1])
523
+
524
+ res[2::4] = dz * (self.state.solid_area_mid[1:nx] * dt_mtp + self.state.porosity_area_mid[1:nx] * flow_props['Heat Transfer Rate']) + (hfx[1:] - hfx[:-1])
525
+
526
+ res[1::4][1:] = dz * (self.state.porosity_area_mid[1:nx] * dt_eng - self.state.porosity_area_mid[1:nx] * flow_props['Heat Transfer Rate']) + (eng_flux[1:] - eng_flux[:-1])
527
+
528
+ res[1::4][0] = mfxi[0] - self.mass_flux_hot * np.sin(self.omega * time) - self.mflux_dc
529
+ res[0::4][-1] = mfxi[-1] - self.mass_flux_cold * np.sin(self.omega * time + self.mass_phase_rad) - self.mflux_dc
530
+
531
+ return res[:self.neqn]
532
+
533
+ def _compute_jacobian_banded(self, guess_array: np.ndarray, time: float, method_order: int) -> tuple[np.ndarray, np.ndarray]:
534
+
535
+ ml = 2 * self.nvars - 1
536
+ mu = 2 * self.nvars - 1
537
+
538
+ jac_banded = np.zeros((ml + mu + 1, self.neqn))
539
+ epsjac = 1e-6
540
+
541
+ f0 = self.calculate_residuals(guess_array, time, method_order)
542
+
543
+ for j in range(self.neqn):
544
+
545
+ delta = max(abs(guess_array[j]) * epsjac, epsjac)
546
+
547
+ guess_pert = guess_array.copy()
548
+ guess_pert[j] += delta
549
+
550
+ f_pert = self.calculate_residuals(guess_pert, time, method_order)
551
+ df = (f_pert - f0) / delta
552
+
553
+ i_min = max(0, j - mu)
554
+ i_max = min(self.neqn, j + ml + 1)
555
+ for i in range(i_min, i_max):
556
+ row_index = mu + i - j
557
+ jac_banded[row_index, j] = df[i]
558
+
559
+ return jac_banded, f0
560
+
561
+ def advance_timestep(self, time: float, method_order: int = 2) -> bool:
562
+
563
+ guess_array = np.zeros(self.neqn)
564
+
565
+ for i in range(self.state.num_nodes):
566
+ guess_array[0 + i*4] = self.state.mass_flux_nodes[i, 1] + (self.state.mass_flux_nodes[i, 1] - self.state.mass_flux_nodes[i, 0])
567
+ guess_array[1 + i*4] = self.state.pressure_nodes[i, 1] + (self.state.pressure_nodes[i, 1] - self.state.pressure_nodes[i, 0])
568
+
569
+ if i < self.state.num_nodes - 1:
570
+ guess_array[2 + i*4] = self.state.matrix_temp_mid[i+1, 1] + (self.state.matrix_temp_mid[i+1, 1] - self.state.matrix_temp_mid[i+1, 0])
571
+ guess_array[3 + i*4] = self.state.gas_temp_mid[i+1, 1] + (self.state.gas_temp_mid[i+1, 1] - self.state.gas_temp_mid[i+1, 0])
572
+
573
+ guess_array = guess_array[:self.neqn]
574
+
575
+ max_iters = self.max_newton_iters
576
+ eps_newton = self.eps_newton
577
+
578
+ ml = 2 * self.nvars - 1
579
+ mu = 2 * self.nvars - 1
580
+
581
+ for iteration in range(max_iters):
582
+
583
+ jac_banded, res = self._compute_jacobian_banded(guess_array, time, method_order)
584
+
585
+ row_max = np.zeros(self.neqn)
586
+ for j in range(self.neqn):
587
+ i_min = max(0, j - mu)
588
+ i_max = min(self.neqn, j + ml + 1)
589
+ for i in range(i_min, i_max):
590
+ val = abs(jac_banded[mu + i - j, j])
591
+ if val > row_max[i]:
592
+ row_max[i] = val
593
+
594
+ row_max[row_max == 0.0] = 1.0
595
+ res_scaled = res / row_max
596
+
597
+ err = np.sqrt(np.mean(res_scaled**2))
598
+ if err < eps_newton:
599
+ break
600
+
601
+ try:
602
+ del_w = la.solve_banded((ml, mu), jac_banded, -res)
603
+ except la.LinAlgError:
604
+ print(f"Linear algebra error at time {time}. Jacobian is singular.")
605
+ return False
606
+
607
+ guess_array += del_w
608
+
609
+ res_new = self.calculate_residuals(guess_array, time, method_order)
610
+ err_new = np.sqrt(np.mean((res_new / row_max)**2))
611
+
612
+ if err_new < eps_newton:
613
+ break
614
+
615
+ else:
616
+ print(f"Newton solver failed to converge at time {time}. Error stuck at {err_new:.3e}")
617
+ return False
618
+
619
+ self.state.mass_flux_nodes[:, 0] = self.state.mass_flux_nodes[:, 1]
620
+ self.state.mass_flux_nodes[:, 1] = self.state.mass_flux_nodes[:, 2]
621
+
622
+ self.state.pressure_nodes[:, 0] = self.state.pressure_nodes[:, 1]
623
+ self.state.pressure_nodes[:, 1] = self.state.pressure_nodes[:, 2]
624
+
625
+ self.state.matrix_temp_mid[:, 0] = self.state.matrix_temp_mid[:, 1]
626
+ self.state.matrix_temp_mid[:, 1] = self.state.matrix_temp_mid[:, 2]
627
+
628
+ self.state.gas_temp_mid[:, 0] = self.state.gas_temp_mid[:, 1]
629
+ self.state.gas_temp_mid[:, 1] = self.state.gas_temp_mid[:, 2]
630
+
631
+ self.state.mass_flux_nodes[:, 2] = guess_array[0::4]
632
+ self.state.pressure_nodes[:, 2] = guess_array[1::4]
633
+
634
+ mtph = guess_array[2::4]
635
+ gtph = guess_array[3::4]
636
+ self.state.matrix_temp_mid[1:self.state.num_nodes, 2] = mtph
637
+ self.state.gas_temp_mid[1:self.state.num_nodes, 2] = gtph
638
+
639
+ mfxi_old = self.state.mass_flux_nodes[:, 1]
640
+ mfxi_new = self.state.mass_flux_nodes[:, 2]
641
+
642
+ if (mfxi_old[0] <= 0.0 and mfxi_new[0] > 0.0) or (mfxi_old[0] < 0.0 and mfxi_new[0] >= 0.0):
643
+ self.state.time_rev_hot = time
644
+ self.state.temp_rev_hot = self.state.gas_temp_mid[0, 1]
645
+
646
+ if (mfxi_old[-1] >= 0.0 and mfxi_new[-1] < 0.0) or (mfxi_old[-1] > 0.0 and mfxi_new[-1] <= 0.0):
647
+ self.state.time_rev_cold = time
648
+ self.state.temp_rev_cold = self.state.gas_temp_mid[-1, 1]
649
+
650
+ if mfxi_new[0] >= 1e-8:
651
+ decay_factor = np.exp(-(time - self.state.time_rev_hot) * self.frequency / self.decay)
652
+ self.state.gas_temp_mid[0, 2] = self.gas_temp_hot + (self.state.temp_rev_hot - self.gas_temp_hot) * decay_factor
653
+ else:
654
+ self.state.gas_temp_mid[0, 2] = self.state.extrap_left(self.state.gas_temp_mid[:, 2])
655
+
656
+ if mfxi_new[-1] <= -1e-8:
657
+ decay_factor = np.exp(-(time - self.state.time_rev_cold) * self.frequency / self.decay)
658
+ self.state.gas_temp_mid[-1, 2] = self.gas_temp_cold + (self.state.temp_rev_cold - self.gas_temp_cold) * decay_factor
659
+ else:
660
+ self.state.gas_temp_mid[-1, 2] = max(1.0, self.state.extrap_right(self.state.gas_temp_mid[:, 2]))
661
+
662
+ self.state.matrix_temp_mid[0, 2] = self.state.extrap_left(self.state.matrix_temp_mid[:, 2])
663
+ self.state.matrix_temp_mid[-1, 2] = max(1.0, self.state.extrap_right(self.state.matrix_temp_mid[:, 2]))
664
+
665
+ return True
666
+
667
+ class Regenerator:
668
+
669
+ def __init__(self,
670
+ gas_temp_cold: float,
671
+ gas_temp_hot: float,
672
+ herz: float,
673
+ hydra_diam: float,
674
+ mass_flux_cold: float,
675
+ mass_flux_hot: float,
676
+ mass_phase: float,
677
+ material: str,
678
+ porosity: float,
679
+ pres_initial: float,
680
+ rg_area: float,
681
+ rg_length: float,
682
+ geometry: str = '1',
683
+ mid_temp_ratio: float = 0.5,
684
+ num_points_x: int = 21,
685
+ num_steps_cyc: int = 80,
686
+ ideal_gas: bool = False,
687
+ gas_isotope: int = 4,
688
+ cp_factor: float = 1.0,
689
+ cond_factor: float = 1.0,
690
+ decay_time: float = 0.05,
691
+ use_advection: bool = True,
692
+ bdy_order: int = 1,
693
+ eps_newton: float = 1e-6,
694
+ max_newton_iters: int = 4,
695
+ mflux_dc: float = 0.0):
696
+
697
+ self._validate_inputs(gas_temp_cold, gas_temp_hot, herz, hydra_diam, porosity, rg_area, rg_length, pres_initial)
698
+
699
+ self.gas_temp_cold = gas_temp_cold
700
+ self.gas_temp_hot = gas_temp_hot
701
+ self.herz = herz
702
+ self.num_steps_cyc = num_steps_cyc
703
+ self.pres_initial = pres_initial
704
+ self.mid_temp_ratio = mid_temp_ratio
705
+
706
+ self.dt = 1.0 / (num_steps_cyc * herz)
707
+ self.time = 0.0
708
+ self.step_count = 0
709
+
710
+ self.state = RegenState(
711
+ Number_of_Nodes=num_points_x,
712
+ Area=rg_area,
713
+ Length=rg_length,
714
+ Porosity=porosity,
715
+ Hydraulic_Diameter=hydra_diam
716
+ )
717
+
718
+ self.solver = RegenSolver(
719
+ state=self.state,
720
+ Geometry=geometry,
721
+ Material=material,
722
+ Frequency=herz,
723
+ Mass_Flux_Hot=mass_flux_hot,
724
+ Mass_Flux_Cold=mass_flux_cold,
725
+ Mass_Phase_Deg=mass_phase,
726
+ Gas_Temp_Hot=gas_temp_hot,
727
+ Gas_Temp_Cold=gas_temp_cold,
728
+ Gas_Isotope=gas_isotope,
729
+ Ideal_Gas=ideal_gas,
730
+ Time_Step=self.dt,
731
+ Decay_Time=decay_time,
732
+ Use_Advection=use_advection,
733
+ cp_factor=cp_factor,
734
+ cond_factor=cond_factor,
735
+ eps_newton=eps_newton,
736
+ max_newton_iters=max_newton_iters,
737
+ mflux_dc=mflux_dc
738
+ )
739
+
740
+ self._initialize_conditions()
741
+
742
+ self.history = {
743
+ 'Time': [],
744
+ 'Cycle': [],
745
+ 'X_Midpoints': self.state.x_midpoints.copy(),
746
+ 'X_Endpoints': self.state.x_endpoints.copy(),
747
+ 'Gas_Temperature': [],
748
+ 'Matrix_Temperature': [],
749
+ 'Pressure': [],
750
+ 'Mass_Flux': []
751
+ }
752
+
753
+ def _validate_inputs(self, tc, th, hz, hd, por, area, length, p_init):
754
+ if tc <= 0 or th <= 0: raise ValueError("Temperatures must be > 0 K.")
755
+ if hz <= 0: raise ValueError("Frequency (herz) must be > 0.")
756
+ if hd <= 0 or area <= 0 or length <= 0: raise ValueError("Dimensions must be > 0.")
757
+ if por <= 0 or por >= 1: raise ValueError("Porosity must be between 0 and 1.")
758
+ if p_init <= 0: raise ValueError("Initial pressure must be > 0 Pa.")
759
+
760
+ def _initialize_conditions(self):
761
+ zlen = self.state.length
762
+ gtplft = self.gas_temp_hot
763
+ gtprht = self.gas_temp_cold
764
+
765
+ self.state.pressure_nodes[:, :] = self.pres_initial
766
+
767
+ gtpmid = self.mid_temp_ratio * (gtplft - gtprht) + gtprht
768
+ xh = self.state.x_midpoints
769
+ parabola_mid = (
770
+ (0.5 * zlen - xh) * (zlen - xh) * 2.0 * gtplft +
771
+ xh * (zlen - xh) * 4.0 * gtpmid +
772
+ xh * (xh - 0.5 * zlen) * 2.0 * gtprht
773
+ ) / (zlen ** 2)
774
+
775
+ gas_props = helium_properties(
776
+ Pressure=np.full_like(xh, self.pres_initial),
777
+ Temperature=parabola_mid,
778
+ Hydraulic_Diameter=self.state.hydra_diam_mid,
779
+ Ideal_Gas=self.solver.ideal_gas,
780
+ Isotope=self.solver.isotope
781
+ )
782
+ matrix_enthalpy = self.state.get_matrix_enthalpy(self.solver.material, parabola_mid)
783
+ gas_energy_vol = gas_props['Density'] * gas_props['Internal Energy']
784
+
785
+ for level in range(3):
786
+ self.state.gas_temp_mid[:, level] = parabola_mid
787
+ self.state.matrix_temp_mid[:, level] = parabola_mid
788
+
789
+ self.state.gas_density_mid[:, level] = gas_props['Density']
790
+ self.state.gas_energy_vol_mid[:, level] = gas_energy_vol
791
+ self.state.matrix_enthalpy_mid[:, level] = matrix_enthalpy
792
+ self.state.gas_mass_flux_mid[:, level] = 0.0
793
+ self.state.mass_flux_nodes[:, level] = 0.0
794
+
795
+ def advance(self) -> dict:
796
+
797
+ order = 1 if self.step_count < 2 else 2
798
+
799
+ success = self.solver.advance_timestep(self.time + self.dt, method_order=order)
800
+
801
+ if not success:
802
+ raise RegenError(f"Simulation failed to converge at Step {self.step_count + 1} (Time: {self.time + self.dt:.5f} s).")
803
+
804
+ self.time += self.dt
805
+ self.step_count += 1
806
+ current_cycle = self.time * self.herz
807
+
808
+ current_data = {
809
+ 'Time': self.time,
810
+ 'Cycle': current_cycle,
811
+ 'Gas_Temperature': self.state.gas_temp_mid[:, 2].copy(),
812
+ 'Matrix_Temperature': self.state.matrix_temp_mid[:, 2].copy(),
813
+ 'Pressure': self.state.pressure_nodes[:, 2].copy(),
814
+ 'Mass_Flux': self.state.mass_flux_nodes[:, 2].copy()
815
+ }
816
+
817
+ self.history['Time'].append(current_data['Time'])
818
+ self.history['Cycle'].append(current_data['Cycle'])
819
+ self.history['Gas_Temperature'].append(current_data['Gas_Temperature'])
820
+ self.history['Matrix_Temperature'].append(current_data['Matrix_Temperature'])
821
+ self.history['Pressure'].append(current_data['Pressure'])
822
+ self.history['Mass_Flux'].append(current_data['Mass_Flux'])
823
+
824
+ return current_data
825
+
826
+ def results(self) -> dict:
827
+ return {
828
+ 'Time': np.array(self.history['Time']),
829
+ 'Cycle': np.array(self.history['Cycle']),
830
+ 'X_Midpoints': self.history['X_Midpoints'],
831
+ 'X_Endpoints': self.history['X_Endpoints'],
832
+ 'Gas_Temperature': np.array(self.history['Gas_Temperature']),
833
+ 'Matrix_Temperature': np.array(self.history['Matrix_Temperature']),
834
+ 'Pressure': np.array(self.history['Pressure']),
835
+ 'Mass_Flux': np.array(self.history['Mass_Flux']),
836
+ }
837
+
838
+ def save_to_csv(self, filename: str = "regen_output_data.csv"):
839
+ import csv
840
+
841
+ if len(self.history['Time']) == 0:
842
+ print("No data to save yet!")
843
+ return
844
+
845
+ with open(filename, 'w', newline='') as f:
846
+ writer = csv.writer(f)
847
+
848
+ for i in range(len(self.history['Time'])):
849
+ cycle_num = self.history['Cycle'][i]
850
+ writer.writerow([f"GAS & MATRIX TEMP AT CYCLE= {cycle_num:.3f}"])
851
+ writer.writerow(["X", "TGAS-a", "TMAT-b"])
852
+
853
+ x_mid = self.history['X_Midpoints']
854
+ tgas = self.history['Gas_Temperature'][i]
855
+ tmat = self.history['Matrix_Temperature'][i]
856
+
857
+ for x, tg, tm in zip(x_mid, tgas, tmat):
858
+ writer.writerow([f"{x:.5E}", f"{tg:.5E}", f"{tm:.5E}"])
859
+ writer.writerow([])
860
+
861
+ print(f"Data successfully exported to {filename}")
862
+
863
+
864
+ if __name__ == "__main__":
865
+
866
+ my_regen = Regenerator(
867
+ gas_temp_cold=80.0,
868
+ gas_temp_hot=330.0,
869
+ herz=13.0,
870
+ hydra_diam=4.14e-5,
871
+ mass_flux_cold=4.32e-3,
872
+ mass_flux_hot=4.32e-3,
873
+ mass_phase=30.0,
874
+ material='stainless steel',
875
+ porosity=0.62,
876
+ pres_initial=1.75e6,
877
+ rg_area=7.92e-4,
878
+ rg_length=0.11,
879
+ geometry='screens',
880
+ num_points_x=21,
881
+ num_steps_cyc=80
882
+ )
883
+
884
+ cold_end_mass_fluxes = []
885
+
886
+ target_cycles = 10
887
+ total_steps = target_cycles * my_regen.num_steps_cyc
888
+
889
+ print(f"Starting Engine: Running {total_steps} steps...")
890
+
891
+ for step in range(total_steps):
892
+
893
+ current_data = my_regen.advance()
894
+
895
+ cold_end_mass_fluxes.append(current_data['Mass_Flux'][-1])
896
+
897
+ if (step + 1) % my_regen.num_steps_cyc == 0:
898
+ print(f"Cycle {current_data['Cycle']:.1f} completed.")
899
+
900
+ full_history = my_regen.results()
901
+ print(f"\nFinal Gas Temp at cold end: {full_history['Gas_Temperature'][-1, -1]:.2f} K")
902
+
903
+ my_regen.save_to_csv("my_custom_run.csv")
@@ -0,0 +1,87 @@
1
+ Metadata-Version: 2.4
2
+ Name: pyregen
3
+ Version: 0.1.0
4
+ Summary: A Python port of the NIST REGEN3.3 cryogenic regenerator model.
5
+ Author-email: Kishorekumar S <kishoresathishkumar@gmail.com>
6
+ Project-URL: Homepage, https://github.com/Kiesh628/pyRegen
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Scientific/Engineering :: Physics
11
+ Requires-Python: >=3.10
12
+ Description-Content-Type: text/markdown
13
+ Requires-Dist: numpy>=1.20.0
14
+ Requires-Dist: scipy>=1.7.0
15
+ Requires-Dist: CoolProp>=6.4.1
16
+
17
+ # pyregen
18
+
19
+ **pyregen** is a Python-based port of NIST **REGEN3.3** cryogenic regenerator model.
20
+
21
+ It evaluates the fluid dynamics and heat transfer of oscillating helium flow through porous media at cryogenic temperatures. By replacing Fortran structures with a Object-Oriented Python architecture, `pyregen` acts as a fast, flexible engine.
22
+
23
+
24
+ ## Installation
25
+
26
+ pip install pyregen
27
+
28
+ ## Quick Start
29
+
30
+ `pyregen` is designed to act as a state-machine engine. You initialize the regenerator with your physical parameters, and then drive it forward in a loop.
31
+
32
+ ```python
33
+ from pyregen import Regenerator
34
+
35
+ my_regen = Regenerator(
36
+ gas_temp_cold=80.0, # Cold end temperature (K)
37
+ gas_temp_hot=300.0, # Hot end temperature (K)
38
+ herz=60.0, # Operating frequency (Hz)
39
+ hydra_diam=50e-6, # Hydraulic diameter (m)
40
+ mass_flux_cold=0.01, # Mass flux at cold end (kg/s)
41
+ mass_flux_hot=0.01, # Mass flux at hot end (kg/s)
42
+ mass_phase=30.0, # Phase shift (degrees)
43
+ material='stainless steel', # Matrix material
44
+ porosity=0.68, # Matrix porosity
45
+ pres_initial=2.5e6, # Average pressure (Pa)
46
+ rg_area=0.002, # Cross-sectional area (m^2)
47
+ rg_length=0.05, # Regenerator length (m)
48
+ geometry='screens', # Matrix geometry
49
+ num_points_x=21, # Number of spatial nodes
50
+ num_steps_cyc=80 # Number of time steps per cycle
51
+ )
52
+
53
+ target_cycles = 10
54
+ total_steps = target_cycles * my_regen.num_steps_cyc
55
+
56
+ print(f"Starting Engine: Running {total_steps} steps...")
57
+
58
+ for step in range(total_steps):
59
+
60
+ current_data = my_regen.advance()
61
+
62
+ if (step + 1) % my_regen.num_steps_cyc == 0:
63
+ print(f"Cycle {current_data['Cycle']:.1f} completed.")
64
+
65
+ full_history = my_regen.results()
66
+ final_gas_temp = full_history['Gas_Temperature'][-1, -1]
67
+ print(f"\nFinal Gas Temp at the cold boundary: {final_gas_temp:.2f} K")
68
+
69
+ my_regen.save_to_csv("my_simulation_run.csv")
70
+ ```
71
+
72
+ ## Supported Materials & Geometries
73
+
74
+ **Materials:**
75
+ * `stainless steel` (or `ss`)
76
+ * `lead` (or `pb`)
77
+ * `er3-ni` (or `er3ni`)
78
+ * *(More materials can be easily added via the `material_properties` function)*
79
+
80
+ **Geometries:**
81
+ * `parallel plates` (or `1`)
82
+ * `tubes` (or `2`)
83
+ * `screens` (or `4`)
84
+
85
+ ## License
86
+
87
+ This project is licensed under the MIT License.
@@ -0,0 +1,6 @@
1
+ pyregen/__init__.py,sha256=QH-q4iNeWwwmuhUsGkzknwZZgF8GT5D8fgpIEffjr-A,91
2
+ pyregen/core.py,sha256=ks19J2-scLamfQYmU3VSIKhdfznYXRUmjYPWc6oA9ro,39644
3
+ pyregen-0.1.0.dist-info/METADATA,sha256=zLfxkMvL6qXBSvd3FXVW1OxggXUOvCxbTMK89ryAvNg,3092
4
+ pyregen-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
5
+ pyregen-0.1.0.dist-info/top_level.txt,sha256=TnnoHlmvSZP8pKi38oS-BLtho41Sqp0fsfmB02N7MYY,8
6
+ pyregen-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ pyregen