pyregen 0.1.0__tar.gz
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-0.1.0/PKG-INFO +87 -0
- pyregen-0.1.0/README.md +71 -0
- pyregen-0.1.0/pyproject.toml +27 -0
- pyregen-0.1.0/setup.cfg +4 -0
- pyregen-0.1.0/src/pyregen/__init__.py +3 -0
- pyregen-0.1.0/src/pyregen/core.py +903 -0
- pyregen-0.1.0/src/pyregen.egg-info/PKG-INFO +87 -0
- pyregen-0.1.0/src/pyregen.egg-info/SOURCES.txt +9 -0
- pyregen-0.1.0/src/pyregen.egg-info/dependency_links.txt +1 -0
- pyregen-0.1.0/src/pyregen.egg-info/requires.txt +3 -0
- pyregen-0.1.0/src/pyregen.egg-info/top_level.txt +1 -0
pyregen-0.1.0/PKG-INFO
ADDED
|
@@ -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.
|
pyregen-0.1.0/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# pyregen
|
|
2
|
+
|
|
3
|
+
**pyregen** is a Python-based port of NIST **REGEN3.3** cryogenic regenerator model.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
pip install pyregen
|
|
11
|
+
|
|
12
|
+
## Quick Start
|
|
13
|
+
|
|
14
|
+
`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.
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from pyregen import Regenerator
|
|
18
|
+
|
|
19
|
+
my_regen = Regenerator(
|
|
20
|
+
gas_temp_cold=80.0, # Cold end temperature (K)
|
|
21
|
+
gas_temp_hot=300.0, # Hot end temperature (K)
|
|
22
|
+
herz=60.0, # Operating frequency (Hz)
|
|
23
|
+
hydra_diam=50e-6, # Hydraulic diameter (m)
|
|
24
|
+
mass_flux_cold=0.01, # Mass flux at cold end (kg/s)
|
|
25
|
+
mass_flux_hot=0.01, # Mass flux at hot end (kg/s)
|
|
26
|
+
mass_phase=30.0, # Phase shift (degrees)
|
|
27
|
+
material='stainless steel', # Matrix material
|
|
28
|
+
porosity=0.68, # Matrix porosity
|
|
29
|
+
pres_initial=2.5e6, # Average pressure (Pa)
|
|
30
|
+
rg_area=0.002, # Cross-sectional area (m^2)
|
|
31
|
+
rg_length=0.05, # Regenerator length (m)
|
|
32
|
+
geometry='screens', # Matrix geometry
|
|
33
|
+
num_points_x=21, # Number of spatial nodes
|
|
34
|
+
num_steps_cyc=80 # Number of time steps per cycle
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
target_cycles = 10
|
|
38
|
+
total_steps = target_cycles * my_regen.num_steps_cyc
|
|
39
|
+
|
|
40
|
+
print(f"Starting Engine: Running {total_steps} steps...")
|
|
41
|
+
|
|
42
|
+
for step in range(total_steps):
|
|
43
|
+
|
|
44
|
+
current_data = my_regen.advance()
|
|
45
|
+
|
|
46
|
+
if (step + 1) % my_regen.num_steps_cyc == 0:
|
|
47
|
+
print(f"Cycle {current_data['Cycle']:.1f} completed.")
|
|
48
|
+
|
|
49
|
+
full_history = my_regen.results()
|
|
50
|
+
final_gas_temp = full_history['Gas_Temperature'][-1, -1]
|
|
51
|
+
print(f"\nFinal Gas Temp at the cold boundary: {final_gas_temp:.2f} K")
|
|
52
|
+
|
|
53
|
+
my_regen.save_to_csv("my_simulation_run.csv")
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Supported Materials & Geometries
|
|
57
|
+
|
|
58
|
+
**Materials:**
|
|
59
|
+
* `stainless steel` (or `ss`)
|
|
60
|
+
* `lead` (or `pb`)
|
|
61
|
+
* `er3-ni` (or `er3ni`)
|
|
62
|
+
* *(More materials can be easily added via the `material_properties` function)*
|
|
63
|
+
|
|
64
|
+
**Geometries:**
|
|
65
|
+
* `parallel plates` (or `1`)
|
|
66
|
+
* `tubes` (or `2`)
|
|
67
|
+
* `screens` (or `4`)
|
|
68
|
+
|
|
69
|
+
## License
|
|
70
|
+
|
|
71
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyregen"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
authors = [
|
|
9
|
+
{ name="Kishorekumar S", email="kishoresathishkumar@gmail.com"},
|
|
10
|
+
]
|
|
11
|
+
description = "A Python port of the NIST REGEN3.3 cryogenic regenerator model."
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
requires-python = ">=3.10"
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
19
|
+
]
|
|
20
|
+
dependencies = [
|
|
21
|
+
"numpy>=1.20.0",
|
|
22
|
+
"scipy>=1.7.0",
|
|
23
|
+
"CoolProp>=6.4.1"
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
"Homepage" = "https://github.com/Kiesh628/pyRegen"
|
pyregen-0.1.0/setup.cfg
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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pyregen
|