esclab 0.0.1__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.
- esclab/__init__.py +8 -0
- esclab/components/__init__.py +35 -0
- esclab/components/brayton_simple.py +123 -0
- esclab/components/circuit_elements.py +163 -0
- esclab/components/esol_properties.py +215 -0
- esclab/components/template_component.py +120 -0
- esclab/models/__init__.py +1 -0
- esclab/models/sample_air-brayton.py +102 -0
- esclab/models/sample_circuit.py +109 -0
- esclab/models/sample_circuit_hilopass.py +144 -0
- esclab/models/sample_circuit_secsub.py +361 -0
- esclab/models/sample_circuit_tee.py +111 -0
- esclab/models/sample_dispatch.py +295 -0
- esclab/models/sample_lc.py +55 -0
- esclab/models/template_model.py +89 -0
- esclab/plotting/__init__.py +4 -0
- esclab/plotting/network_topology.py +2651 -0
- esclab/plotting/online_plotter.py +780 -0
- esclab/simulate.py +1342 -0
- esclab-0.0.1.dist-info/METADATA +186 -0
- esclab-0.0.1.dist-info/RECORD +23 -0
- esclab-0.0.1.dist-info/WHEEL +5 -0
- esclab-0.0.1.dist-info/top_level.txt +1 -0
esclab/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file ensures that the component libraries are available for import from the top-level components module.
|
|
3
|
+
|
|
4
|
+
Add new modules to this file using:
|
|
5
|
+
from . import my_component_library
|
|
6
|
+
|
|
7
|
+
And add the module name to __all__ to make it available using the flat import syntax:
|
|
8
|
+
from esclab.components import my_component_library
|
|
9
|
+
or
|
|
10
|
+
from esclab.components import *
|
|
11
|
+
|
|
12
|
+
It's best practice to import the component library as a named item, and then
|
|
13
|
+
reference the components through that namespace to avoid name collisions across libraries. For example:
|
|
14
|
+
import esclab.circuit_elements as ce
|
|
15
|
+
ce.Resistor(...)
|
|
16
|
+
|
|
17
|
+
Import the submodule you need and reference components through it to avoid
|
|
18
|
+
name collisions across libraries:
|
|
19
|
+
|
|
20
|
+
from esclab.components import circuit_elements
|
|
21
|
+
circuit_elements.TeeOut(...)
|
|
22
|
+
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from . import brayton_simple
|
|
26
|
+
from . import circuit_elements
|
|
27
|
+
from . import esol_properties
|
|
28
|
+
from . import template_component
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"brayton_simple",
|
|
32
|
+
"circuit_elements",
|
|
33
|
+
"esol_properties",
|
|
34
|
+
"template_component"
|
|
35
|
+
]
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
from esclab.simulate import *
|
|
2
|
+
from eeslib import fluid_properties
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
# -------------------------------------------
|
|
6
|
+
class Turbine(Component):
|
|
7
|
+
|
|
8
|
+
eta_s = Component.Parameter(0.9)
|
|
9
|
+
mdot_in = Component.Input()
|
|
10
|
+
T_in = Component.Input()
|
|
11
|
+
P_in = Component.Input()
|
|
12
|
+
h_out = Component.Output()
|
|
13
|
+
T_out = Component.Output()
|
|
14
|
+
P_out = Component.Output()
|
|
15
|
+
mdot_out = Component.Output()
|
|
16
|
+
W = Component.Output()
|
|
17
|
+
fluid = 'Air'
|
|
18
|
+
|
|
19
|
+
def calculate(self):
|
|
20
|
+
self.mdot_out = self.mdot_in
|
|
21
|
+
h_in = fluid_properties.enthalpy(self.fluid, T=self.T_in, P=self.P_in)
|
|
22
|
+
s_in = fluid_properties.entropy(self.fluid, T=self.T_in, P=self.P_in)
|
|
23
|
+
self.P_out = self.P_in / self.model.design.PR
|
|
24
|
+
h_out_s = fluid_properties.enthalpy(self.fluid,P=self.P_out, s=s_in)
|
|
25
|
+
self.h_out = h_in - (h_in-h_out_s)*self.eta_s
|
|
26
|
+
self.T_out = fluid_properties.temperature(self.fluid, P=self.P_out, h=self.h_out)
|
|
27
|
+
self.W = (h_in - self.h_out)*self.mdot_in
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
# -------------------------------------------
|
|
31
|
+
class Compressor(Component):
|
|
32
|
+
eta_s = Component.Parameter(0.85)
|
|
33
|
+
PR = Component.Parameter(3.)
|
|
34
|
+
|
|
35
|
+
mdot_in = Component.Input()
|
|
36
|
+
T_in = Component.Input()
|
|
37
|
+
P_in = Component.Input()
|
|
38
|
+
|
|
39
|
+
h_out = Component.Output()
|
|
40
|
+
T_out = Component.Output()
|
|
41
|
+
P_out = Component.Output()
|
|
42
|
+
mdot_out = Component.Output()
|
|
43
|
+
W = Component.Output()
|
|
44
|
+
|
|
45
|
+
fluid = 'Air'
|
|
46
|
+
|
|
47
|
+
def calculate(self):
|
|
48
|
+
self.mdot_out = self.mdot_in
|
|
49
|
+
h_in = fluid_properties.enthalpy(self.fluid, T=self.T_in, P=self.P_in)
|
|
50
|
+
s_in = fluid_properties.entropy(self.fluid, T=self.T_in, P=self.P_in)
|
|
51
|
+
self.P_out = self.P_in * self.PR
|
|
52
|
+
h_out_s = fluid_properties.enthalpy(self.fluid,P=self.P_out, s=s_in)
|
|
53
|
+
self.h_out = h_in + (h_out_s - h_in)/self.eta_s
|
|
54
|
+
self.T_out = fluid_properties.temperature(self.fluid, P=self.P_out, h=self.h_out)
|
|
55
|
+
self.W = (h_in - self.h_out)*self.mdot_in
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
# -------------------------------------------
|
|
59
|
+
class Combustor(Component):
|
|
60
|
+
mdot_in = Component.Input()
|
|
61
|
+
T_in = Component.Input()
|
|
62
|
+
P_in = Component.Input()
|
|
63
|
+
|
|
64
|
+
h_out = Component.Output()
|
|
65
|
+
T_out = Component.Output()
|
|
66
|
+
P_out = Component.Output()
|
|
67
|
+
mdot_out = Component.Output()
|
|
68
|
+
qdot = Component.Output()
|
|
69
|
+
|
|
70
|
+
def presim_setup(self, **kwargs):
|
|
71
|
+
return super().presim_setup(**kwargs)
|
|
72
|
+
|
|
73
|
+
def calculate(self):
|
|
74
|
+
super().calculate()
|
|
75
|
+
self.mdot_out = self.mdot_in
|
|
76
|
+
self.P_out = self.P_in
|
|
77
|
+
self.T_out = self.model.design.T_turb_in
|
|
78
|
+
h_in = fluid_properties.enthalpy(self.model.design.fluid, T=self.T_in, P=self.P_in)
|
|
79
|
+
self.h_out = fluid_properties.enthalpy(self.model.design.fluid, T=self.T_out, P=self.P_out)
|
|
80
|
+
self.qdot = (self.h_out - h_in)*self.mdot_in
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
# -------------------------------------------
|
|
84
|
+
class Cooler(Component):
|
|
85
|
+
dT_approach = Component.Parameter(20)
|
|
86
|
+
|
|
87
|
+
mdot_in = Component.Input()
|
|
88
|
+
T_in = Component.Input()
|
|
89
|
+
P_in = Component.Input()
|
|
90
|
+
T_amb = Component.Input()
|
|
91
|
+
|
|
92
|
+
h_out = Component.Output()
|
|
93
|
+
T_out = Component.Output()
|
|
94
|
+
P_out = Component.Output()
|
|
95
|
+
mdot_out = Component.Output()
|
|
96
|
+
qdot = Component.Output()
|
|
97
|
+
|
|
98
|
+
def calculate(self):
|
|
99
|
+
self.mdot_out = self.mdot_in
|
|
100
|
+
self.P_out = self.P_in
|
|
101
|
+
h_in = fluid_properties.enthalpy(self.model.design.fluid, T=self.T_in, P=self.P_in)
|
|
102
|
+
self.T_out = self.T_amb + self.dT_approach
|
|
103
|
+
self.h_out = fluid_properties.enthalpy(self.model.design.fluid, P=self.model.design.P_low, T=self.T_out)
|
|
104
|
+
self.qdot = (self.h_out - h_in)*self.mdot_in
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
class Summary(Component):
|
|
108
|
+
W_turbine = Component.Input()
|
|
109
|
+
W_compressor = Component.Input()
|
|
110
|
+
Q_combustor = Component.Input()
|
|
111
|
+
eta_cycle = Component.Output()
|
|
112
|
+
|
|
113
|
+
def calculate(self):
|
|
114
|
+
self.eta_cycle = (self.W_turbine + self.W_compressor)/self.Q_combustor
|
|
115
|
+
|
|
116
|
+
class Weather(Component):
|
|
117
|
+
def presim_setup(self):
|
|
118
|
+
self.T_amb = Component.Output()
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
def calculate(self):
|
|
122
|
+
super().calculate()
|
|
123
|
+
self.T_amb = self.model.design.T_amb + 15*np.sin(self.model.time/1e5/np.pi) + 15*np.sin(self.model.time/1e5/np.pi*2+.5)
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Circuit component library using simple models.
|
|
3
|
+
The components are set up to be compatible with the matrix inversion method.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from esclab.simulate import *
|
|
7
|
+
|
|
8
|
+
class __CircuitElement(Component):
|
|
9
|
+
"""Base class for circuit elements."""
|
|
10
|
+
u_in = Component.Input() # voltage
|
|
11
|
+
i_in = Component.Input() # current
|
|
12
|
+
|
|
13
|
+
u_out = Component.Output() # voltage
|
|
14
|
+
i_out = Component.Output() # current
|
|
15
|
+
|
|
16
|
+
def calculate(self):
|
|
17
|
+
if self.coupled_eqs is not None:
|
|
18
|
+
# All series elements pass current straight through: i_out = i_in.
|
|
19
|
+
# Child classes call super().calculate() to contribute this equation,
|
|
20
|
+
# then add their own voltage equation(s).
|
|
21
|
+
self.coupled_eqs.add_equation({
|
|
22
|
+
self.i_out: 1.0,
|
|
23
|
+
self.coupled_eqs.source(self.i_in): -1.0,
|
|
24
|
+
}, rhs=0.0)
|
|
25
|
+
|
|
26
|
+
class VoltageSource(__CircuitElement):
|
|
27
|
+
"""Ideal voltage source element."""
|
|
28
|
+
V = Component.Parameter() # source voltage
|
|
29
|
+
omega_hi = Component.Parameter(0) # cutoff frequency for high-pass behavior
|
|
30
|
+
omega_lo = Component.Parameter(0) # cutoff frequency for low-pass behavior
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.V_t = self.V # stored voltage for time-varying sources
|
|
35
|
+
|
|
36
|
+
def calculate(self):
|
|
37
|
+
super().calculate() # i_out = i_in pass-through (only when coupled_eqs is set)
|
|
38
|
+
if self.coupled_eqs is not None:
|
|
39
|
+
V_t = 1
|
|
40
|
+
V_t += np.sin(2 * np.pi * self.omega_hi * self.model.time) * 0.25
|
|
41
|
+
V_t += np.sin(2 * np.pi * self.omega_lo * self.model.time) * 0.25
|
|
42
|
+
V_t += self.V * V_t
|
|
43
|
+
self.coupled_eqs.add_equation({
|
|
44
|
+
self.u_out: 1.0,
|
|
45
|
+
self.coupled_eqs.source(self.u_in): -1.0,
|
|
46
|
+
}, rhs=V_t)
|
|
47
|
+
self.coupled_eqs.add_equation({
|
|
48
|
+
self.coupled_eqs.source(self.u_in): 1.0,
|
|
49
|
+
}, rhs=0.0)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Resistor(__CircuitElement):
|
|
53
|
+
"""Resistor element."""
|
|
54
|
+
R = Component.Parameter() # resistance
|
|
55
|
+
|
|
56
|
+
def calculate(self):
|
|
57
|
+
super().calculate() # i_out = i_in pass-through
|
|
58
|
+
if self.coupled_eqs is not None:
|
|
59
|
+
# u_out = u_in - R*i_in => u_out - u_in + R*i_in = 0
|
|
60
|
+
self.coupled_eqs.add_equation({
|
|
61
|
+
self.u_out: 1.0,
|
|
62
|
+
self.coupled_eqs.source(self.u_in): -1.0,
|
|
63
|
+
self.coupled_eqs.source(self.i_in): self.R,
|
|
64
|
+
}, rhs=0.0)
|
|
65
|
+
|
|
66
|
+
class Capacitor(__CircuitElement):
|
|
67
|
+
"""Capacitor element."""
|
|
68
|
+
C = Component.Parameter() # capacitance
|
|
69
|
+
U_C0 = Component.Parameter(0.) # initial voltage across the capacitor
|
|
70
|
+
|
|
71
|
+
def presim_setup(self, **kwargs):
|
|
72
|
+
self.U_C_prev = self.U_C0 # stored capacitor voltage from previous timestep
|
|
73
|
+
|
|
74
|
+
def calculate(self):
|
|
75
|
+
super().calculate() # i_out = i_in pass-through
|
|
76
|
+
dt = self.model.settings.timestep
|
|
77
|
+
if self.coupled_eqs is not None:
|
|
78
|
+
# Backward Euler: u_out = u_in - U_C_prev - (dt/C)*i_in
|
|
79
|
+
# u_out - u_in + (dt/C)*i_in = -U_C_prev
|
|
80
|
+
self.coupled_eqs.add_equation({
|
|
81
|
+
self.u_out: 1.0,
|
|
82
|
+
self.coupled_eqs.source(self.u_in): -1.0,
|
|
83
|
+
self.coupled_eqs.source(self.i_in): dt / self.C,
|
|
84
|
+
}, rhs=-self.U_C_prev)
|
|
85
|
+
elif self.model.is_converged:
|
|
86
|
+
# After convergence, store the capacitor voltage for the next timestep.
|
|
87
|
+
self.U_C_prev = self.U_C_prev + self.i_in * dt / self.C
|
|
88
|
+
|
|
89
|
+
class Inductor(__CircuitElement):
|
|
90
|
+
"""Inductor element."""
|
|
91
|
+
L = Component.Parameter() # inductance
|
|
92
|
+
I_L0 = Component.Parameter(0.) # initial current through the inductor
|
|
93
|
+
|
|
94
|
+
def presim_setup(self, **kwargs):
|
|
95
|
+
self.I_L_prev = self.I_L0 # stored inductor current from previous timestep
|
|
96
|
+
|
|
97
|
+
def calculate(self):
|
|
98
|
+
super().calculate() # i_out = i_in pass-through
|
|
99
|
+
dt = self.model.settings.timestep
|
|
100
|
+
L = self.L
|
|
101
|
+
if self.coupled_eqs is not None:
|
|
102
|
+
# Backward Euler: u_out = u_in - (L/dt)*i_in + (L/dt)*I_L_prev
|
|
103
|
+
# u_out - u_in + (L/dt)*i_in = (L/dt)*I_L_prev
|
|
104
|
+
self.coupled_eqs.add_equation({
|
|
105
|
+
self.u_out: 1.0,
|
|
106
|
+
self.coupled_eqs.source(self.u_in): -1.0,
|
|
107
|
+
self.coupled_eqs.source(self.i_in): L / dt,
|
|
108
|
+
}, rhs=(L / dt) * self.I_L_prev)
|
|
109
|
+
elif self.model.is_converged:
|
|
110
|
+
# After convergence, store the inductor current for the next timestep.
|
|
111
|
+
self.I_L_prev = self.i_in
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TeeOut(Component):
|
|
115
|
+
"""Tee element for splitting voltage and current."""
|
|
116
|
+
u_in = Component.Input() # voltage
|
|
117
|
+
i_in = Component.Input() # current
|
|
118
|
+
|
|
119
|
+
u_out = Component.Output() # voltage output
|
|
120
|
+
i_out_1 = Component.Output() # current output 1
|
|
121
|
+
i_out_2 = Component.Output() # current output 2
|
|
122
|
+
|
|
123
|
+
def calculate(self):
|
|
124
|
+
if self.coupled_eqs is not None:
|
|
125
|
+
# Current splits: i_in = i_out_1 + i_out_2
|
|
126
|
+
self.coupled_eqs.add_equation({
|
|
127
|
+
self.coupled_eqs.source(self.i_in): 1.0,
|
|
128
|
+
self.i_out_1: -1.0,
|
|
129
|
+
self.i_out_2: -1.0,
|
|
130
|
+
}, rhs=0.0)
|
|
131
|
+
# Output voltage equals input voltage: u_out = u_in
|
|
132
|
+
self.coupled_eqs.add_equation({
|
|
133
|
+
self.u_out: 1.0,
|
|
134
|
+
self.coupled_eqs.source(self.u_in): -1.0,
|
|
135
|
+
}, rhs=0.0)
|
|
136
|
+
|
|
137
|
+
class TeeReturn(Component):
|
|
138
|
+
"""Tee element for combining voltage and current."""
|
|
139
|
+
u_in_1 = Component.Input() # voltage from branch 1
|
|
140
|
+
i_in_1 = Component.Input() # current from branch 1
|
|
141
|
+
u_in_2 = Component.Input() # voltage from branch 2
|
|
142
|
+
i_in_2 = Component.Input() # current from branch 2
|
|
143
|
+
|
|
144
|
+
u_out = Component.Output() # voltage output
|
|
145
|
+
i_out = Component.Output() # current output
|
|
146
|
+
|
|
147
|
+
def calculate(self):
|
|
148
|
+
if self.coupled_eqs is not None:
|
|
149
|
+
# Current combines: i_out = i_in_1 + i_in_2
|
|
150
|
+
self.coupled_eqs.add_equation({
|
|
151
|
+
self.i_out: 1.0,
|
|
152
|
+
self.coupled_eqs.source(self.i_in_1): -1.0,
|
|
153
|
+
self.coupled_eqs.source(self.i_in_2): -1.0,
|
|
154
|
+
}, rhs=0.0)
|
|
155
|
+
# Output voltage equals both input voltages: u_out = u_in_1 = u_in_2
|
|
156
|
+
self.coupled_eqs.add_equation({
|
|
157
|
+
self.u_out: 1.0,
|
|
158
|
+
self.coupled_eqs.source(self.u_in_1): -1.0,
|
|
159
|
+
}, rhs=0.0)
|
|
160
|
+
self.coupled_eqs.add_equation({
|
|
161
|
+
self.u_out: 1.0,
|
|
162
|
+
self.coupled_eqs.source(self.u_in_2): -1.0,
|
|
163
|
+
}, rhs=0.0)
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
from esclab.simulate import *
|
|
2
|
+
from eeslib import fluid_properties as fp
|
|
3
|
+
from eeslib.functions import convert
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
Unified property database for incompressible fluids, including density, viscosity, and specific heat, where available.
|
|
8
|
+
|
|
9
|
+
Note that some water properties are provided here, and other fluids that are potentially compressible (e.g. air, argon, hydrogen) are included with ideal gas density models. For more detailed and comprehensive property data, users should import **eeslib** which provides an API for CoolProp.
|
|
10
|
+
|
|
11
|
+
Usage:
|
|
12
|
+
from esclab.components.esol_properties import Incompressible
|
|
13
|
+
props = Incompressible()
|
|
14
|
+
props.viscosity('Air', T=300) # Viscosity of air at 300 K
|
|
15
|
+
props.specheat('Therminol Oil', T=350) # Specific heat of Therminol Oil at 350 K
|
|
16
|
+
props.enthalpy('Salt (60 NaNO3, 40 KNO3)', T=400, T_ref=300) # Enthalpy change of salt from 300 K to 400 K
|
|
17
|
+
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
class Incompressible:
|
|
21
|
+
"""Incompressible substance property database with density, viscosity, and specific heat, where available"""
|
|
22
|
+
|
|
23
|
+
funcmap = {
|
|
24
|
+
"Stainless_AISI316": {
|
|
25
|
+
'density': (lambda T: 8349.38 - 0.341708*T - 0.0000865128*T*T),
|
|
26
|
+
'specheat': (lambda T: 0.368455 + 0.000399548*T - 1.70558E-07*T*T),
|
|
27
|
+
},
|
|
28
|
+
"Water (liquid)": {
|
|
29
|
+
# Use with caution - this is only valid near room temp/pressure
|
|
30
|
+
'density': (lambda T: 1000),
|
|
31
|
+
'specheat': (lambda T: 4.181),
|
|
32
|
+
},
|
|
33
|
+
"Air": {
|
|
34
|
+
'viscosity': (lambda T: max([0.0000010765 + 7.15173E-08*T - 5.03525E-11*T*T + 2.02799E-14*T*T*T, 1.e-6])),
|
|
35
|
+
'specheat': (lambda T: 1.03749 - 0.000305497*T + 7.49335E-07*T*T - 3.39363E-10*T*T*T),
|
|
36
|
+
},
|
|
37
|
+
"Salt (68 KCl, 32 MgCl2)": {
|
|
38
|
+
'density': (lambda T: 1E-10*T*T*T - 3E-07*T*T - 0.4739*T + 2384.2),
|
|
39
|
+
'viscosity': (lambda T: 0.0146*np.exp(2230./T)*0.001),
|
|
40
|
+
'specheat': (lambda T: 1.156),
|
|
41
|
+
},
|
|
42
|
+
"Salt (8 NaF, 92 NaBF4)": {
|
|
43
|
+
'density': (lambda T: 8E-09*T*T*T - 2E-05*T*T - 0.6867*T + 2438.5),
|
|
44
|
+
'viscosity': (lambda T: 0.0877*np.exp(2240./T)*0.001),
|
|
45
|
+
'specheat': (lambda T: 1.507),
|
|
46
|
+
},
|
|
47
|
+
"Salt (25 KF, 75 KBF4)": {
|
|
48
|
+
'density': (lambda T: 2E-08*T*T*T - 6E-05*T*T - 0.7701*T + 2466.1),
|
|
49
|
+
'viscosity': (lambda T: 0.0431*np.exp(3060./T)*0.001),
|
|
50
|
+
'specheat': (lambda T: 1.306),
|
|
51
|
+
},
|
|
52
|
+
"Salt (31 RbF, 69 RbBF4)": {
|
|
53
|
+
'density': (lambda T: -1E-08*T*T*T + 4E-05*T*T - 1.0836*T + 3242.6),
|
|
54
|
+
'viscosity': (lambda T: 0.0009),
|
|
55
|
+
'specheat': (lambda T: 9.127),
|
|
56
|
+
},
|
|
57
|
+
"Salt (46.5 LiF, 11.5 NaF, 42 KF)": {
|
|
58
|
+
'density': (lambda T: -2E-09*T*T*T + 1E-05*T*T - 0.7427*T + 2734.7),
|
|
59
|
+
'viscosity': (lambda T: 0.0400*np.exp(4170./T)*0.001),
|
|
60
|
+
'specheat': (lambda T: 2.010),
|
|
61
|
+
},
|
|
62
|
+
"Salt (49 LiF, 29 NaF, 29 ZrF4)": {
|
|
63
|
+
'density': (lambda T: -2E-11*T*T*T + 1E-07*T*T - 0.5172*T + 3674.3),
|
|
64
|
+
'viscosity': (lambda T: 0.0069),
|
|
65
|
+
'specheat': (lambda T: 1.239),
|
|
66
|
+
},
|
|
67
|
+
"Salt (58 KF, 42 ZrF4)": {
|
|
68
|
+
'density': (lambda T: -6E-10*T*T*T + 4E-06*T*T - 0.8931*T + 3661.3),
|
|
69
|
+
'viscosity': (lambda T: 0.0159*np.exp(3179./T)*0.001),
|
|
70
|
+
'specheat': (lambda T: 1.051),
|
|
71
|
+
},
|
|
72
|
+
"Salt (58 LiCl, 42 RbCl)": {
|
|
73
|
+
'density': (lambda T: -8E-10*T*T*T + 1E-06*T*T - 0.689*T + 2929.5),
|
|
74
|
+
'viscosity': (lambda T: 0.0861*np.exp(2517./T)*0.001),
|
|
75
|
+
'specheat': (lambda T: 8.918),
|
|
76
|
+
},
|
|
77
|
+
"Salt (58 NaCl, 42 MgCl2)": {
|
|
78
|
+
'density': (lambda T: -5E-09*T*T*T + 2E-05*T*T - 0.5298*T + 2444.1),
|
|
79
|
+
'viscosity': (lambda T: 0.0286*np.exp(1441./T)*0.001),
|
|
80
|
+
'specheat': (lambda T: 1.080),
|
|
81
|
+
},
|
|
82
|
+
"Salt (59.5 LiCl, 40.5 KCl)": {
|
|
83
|
+
'density': (lambda T: 1E-09*T*T*T - 5E-06*T*T - 0.864*T + 2112.6),
|
|
84
|
+
'viscosity': (lambda T: 0.0861*np.exp(2517./T)*0.001),
|
|
85
|
+
'specheat': (lambda T: 1.202),
|
|
86
|
+
},
|
|
87
|
+
"Salt (59.5 NaF, 40.5 ZrF4)": {
|
|
88
|
+
'density': (lambda T: -5E-09*T*T*T + 2E-05*T*T - 0.9144*T + 3837.),
|
|
89
|
+
'viscosity': (lambda T: 0.0767*np.exp(3977./T)*0.001),
|
|
90
|
+
'specheat': (lambda T: 1.172),
|
|
91
|
+
},
|
|
92
|
+
"Salt (60 NaNO3, 40 KNO3)": {
|
|
93
|
+
'density': (lambda T: max([-1E-07*T*T*T + 0.0002*T*T - 0.7875*T + 2299.4, 1000.0])),
|
|
94
|
+
'viscosity': (lambda T: max([-1.473302E-10*(T-273.15)**3 + 2.279989E-07*(T-273.15)**2 - 1.199514E-04*(T-273.15) + 2.270616E-02, 0.0001])),
|
|
95
|
+
'specheat': (lambda T: -1E-10*T*T*T + 2E-07*T*T + 5E-06*T + 1.4387),
|
|
96
|
+
},
|
|
97
|
+
"Nitrate Salt": {
|
|
98
|
+
'density': (lambda T: max([2090 - 0.636 * (T-273.15), 1000.0])),
|
|
99
|
+
'viscosity': (lambda T: max([(22.714 - 0.12 * (T-273.15) + 0.0002281 * (T-273.15)**2 - 0.0000001474 * (T-273.15)**3) / 1000, 1.e-6])),
|
|
100
|
+
'specheat': (lambda T: (1443. + 0.172 * (T-273.15))/1000.),
|
|
101
|
+
},
|
|
102
|
+
"Caloria HT 43": {
|
|
103
|
+
'density': (lambda T: max([885 - 0.6617 * (T-273.15) - 0.0001265 * (T-273.15)**2, 100.0])),
|
|
104
|
+
'specheat': (lambda T: (3.88 * (T-273.15) + 1606.0)/1000.),
|
|
105
|
+
},
|
|
106
|
+
"HITEC XL Nitrate Salt,": {
|
|
107
|
+
'density': (lambda T: max([2240 - 0.8266 * (T-273.15), 800.0])),
|
|
108
|
+
'viscosity': (lambda T: 1372000 * (T-273.15)**-3.364),
|
|
109
|
+
'specheat': (lambda T: max([1536 - 0.2624 * (T-273.15) - 0.0001139 * (T-273.15)**2, 1000.])/1000.),
|
|
110
|
+
},
|
|
111
|
+
"Therminol Oil": {
|
|
112
|
+
'density': (lambda T: max([1074.0 - 0.6367 * (T-273.15) - 0.0007762 * (T-273.15)**2, 400.0])),
|
|
113
|
+
'viscosity': (lambda T: 0.001 * (10**0.8703 * max(T-273.15, 20.)**(0.2877 + np.log10(max(T-273.15, 20.)**-0.3638)))),
|
|
114
|
+
'specheat': (lambda T: 1.509 + 0.002496 * (T-273.15) + 0.0000007888 * (T-273.15)**2),
|
|
115
|
+
},
|
|
116
|
+
"HITEC Salt": {
|
|
117
|
+
'density': (lambda T: max([2080 - 0.733 * (T-273.15), 1000.0])),
|
|
118
|
+
'viscosity': (lambda T: max([0.00622 - 0.0000102 * (T-273.15), 1.e-6])),
|
|
119
|
+
'specheat': (lambda T: (1560 - 0.0 * (T-273.15))/1000.),
|
|
120
|
+
},
|
|
121
|
+
"Dowtherm Q": {
|
|
122
|
+
'density': (lambda T: max([-0.757332 * (T-273.15) + 980.787, 100.0])),
|
|
123
|
+
'viscosity': (lambda T: 1 / (132.40658 + 4.36107 * (T-273.15) + 0.0781417 * (T-273.15)**2 - 0.00011035416 * (T-273.15)**3)),
|
|
124
|
+
'specheat': (lambda T: (-0.00053943 * (T-273.15)**2 + 3.2028 * (T-273.15) + 1589.2)/1000.),
|
|
125
|
+
},
|
|
126
|
+
"Dowtherm RP": {
|
|
127
|
+
'density': (lambda T: max([-0.000186495 * (T-273.15)**2 - 0.668337 * (T-273.15) + 1042.11, 200.0])),
|
|
128
|
+
'viscosity': (lambda T: 1 / (4.523003 + 0.39156855 * (T-273.15) + 0.028604206 * (T-273.15)**2)),
|
|
129
|
+
'specheat': (lambda T: (-0.0000031915 * (T-273.15)**2 + 2.977 * (T-273.15) + 1560.8)/1000.),
|
|
130
|
+
},
|
|
131
|
+
"HITEC XL": {
|
|
132
|
+
'density': (lambda T: max([2240 - 0.8266 * (T-273.15), 800.0])),
|
|
133
|
+
'viscosity': (lambda T: 1372000 * (T-273.15)**-3.364),
|
|
134
|
+
'specheat': (lambda T: max([1536 - 0.2624 * (T-273.15) - 0.0001139 * (T-273.15)**2, 1000.])/1000.),
|
|
135
|
+
},
|
|
136
|
+
"T-91 Steel": {
|
|
137
|
+
'density': (lambda T: -0.3289*(T-273.15) + 7742.5),
|
|
138
|
+
'specheat': (lambda T: 0.0004*(T-273.15)**2 + 0.2473*(T-273.15) + 450.08),
|
|
139
|
+
},
|
|
140
|
+
"Therminol 66": {
|
|
141
|
+
'density': (lambda T: -0.7146*(T-273.15) + 1024.8),
|
|
142
|
+
'viscosity': (lambda T: (1.31959963 - 0.171204729*(T-273.15) + 0.0100351594*(T-273.15)**2 - 0.000313556341*(T-273.15)**3 + 0.0000053430666*(T-273.15)**4 - 4.66597650E-08*(T-273.15)**5 + 1.63046296E-10*(T-273.15)**6) if (T-273.15) < 80. else (0.0490075884 - 0.00120478233*(T-273.15) + 0.0000130162082*(T-273.15)**2 - 7.58913847E-08*(T-273.15)**3 + 2.47856063E-10*(T-273.15)**4 - 4.26872345E-13*(T-273.15)**5 + 3.01949160E-16*(T-273.15)**6)),
|
|
143
|
+
'specheat': (lambda T: 0.0036*(T-273.15) + 1.4801),
|
|
144
|
+
},
|
|
145
|
+
"Therminol 59": {
|
|
146
|
+
'density': (lambda T: -0.0003*(T-273.15)**2 - 0.6963*(T-273.15) + 988.44),
|
|
147
|
+
'viscosity': (lambda T: (0.0137267822 - 0.000218740224*(T-273.15) + 0.0000759248815*(T-273.15)**2 - 0.00000473464744*(T-273.15)**3 - 1.97083667E-07*(T-273.15)**4 + 4.35487179E-09*(T-273.15)**5 + 2.40243056E-10*(T-273.15)**6) if (T-273.15) < 25. else (0.0114608807 - 0.000313431056*(T-273.15) + 0.00000416778121*(T-273.15)**2 - 3.04668508E-08*(T-273.15)**3 + 1.23719006E-10*(T-273.15)**4 - 2.60834697E-13*(T-273.15)**5 + 2.22227675E-16*(T-273.15)**6)),
|
|
148
|
+
'specheat': (lambda T: 0.0033*(T-273.15) + 1.6132),
|
|
149
|
+
},
|
|
150
|
+
"Argon": {
|
|
151
|
+
'density': (lambda T, P: max([P/(208.13*T), 1.e-10])),
|
|
152
|
+
'viscosity': (lambda T: 4.4997e-6 + 6.38920E-08*T - 1.24550E-11*T*T),
|
|
153
|
+
'specheat': (lambda T: 0.5203),
|
|
154
|
+
},
|
|
155
|
+
"Hydrogen": {
|
|
156
|
+
'density': (lambda T, P: max([P/(4124.*T), 1.e-10])),
|
|
157
|
+
'viscosity': (lambda T: 0.00000231 + 2.37842E-08*T - 5.73624E-12*T*T),
|
|
158
|
+
'specheat': (lambda T: min([max([-45.4022 + 0.690156*T - 0.00327354*T*T + 0.00000817326*T**3 - 1.13234E-08*T**4 + 8.24995E-12*T**5 - 2.46804E-15*T**6, 11.3]), 14.7])),
|
|
159
|
+
},
|
|
160
|
+
"Dowtherm A": {
|
|
161
|
+
# Density polynomial fit to Dow Chemical published data (kg/m³), T in K.
|
|
162
|
+
# Fitted to: T_C=15→1061, 50→1031, 100→985, 150→936, 200→884, 250→830, 300→772, 350→712 kg/m³
|
|
163
|
+
'density': (lambda T: max([1071.0 - 0.803*(T-273.15) - 0.000644*(T-273.15)**2, 100.0])),
|
|
164
|
+
'viscosity': (lambda T: 0.786512*max(T-273.15, 20.)**-1.44263),
|
|
165
|
+
'specheat': (lambda T: 1.47524 + 0.00368606*((T-273.15)-273) - 0.00000516458*((T-273.15)-273)**2 + 8.99399E-09*((T-273.15)-273)**3),
|
|
166
|
+
},
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
def enthalpy(self, fluid: str, T: float, T_ref: float = 273.15, P: float = float('nan')):
|
|
170
|
+
"""Calculate specific enthalpy [J/kg] from temperature T [K] relative to T_ref [K] (default 0°C).
|
|
171
|
+
Computed by integrating specific heat [J/(kg·K)] from T_ref to T.
|
|
172
|
+
Only enthalpy differences are physically meaningful; the reference temperature cancels in all
|
|
173
|
+
energy balance applications."""
|
|
174
|
+
from scipy.integrate import quad
|
|
175
|
+
assert fluid in self.funcmap.keys(), f"Fluid '{fluid}' not found in database"
|
|
176
|
+
assert 'specheat' in self.funcmap[fluid], f"No specific heat data available for '{fluid}'"
|
|
177
|
+
cp_func = self.funcmap[fluid]['specheat']
|
|
178
|
+
h_kJ_per_kg, _ = quad(cp_func, T_ref, T)
|
|
179
|
+
return h_kJ_per_kg # Convert kJ/kg → J/kg
|
|
180
|
+
|
|
181
|
+
def density(self, fluid: str, T: float, P: float = float('nan')):
|
|
182
|
+
"""Calculate density [kg/m^3] from temperature [K] and optional pressure [Pa].
|
|
183
|
+
Argon and Hydrogen require pressure."""
|
|
184
|
+
assert fluid in self.funcmap.keys(), f"Fluid '{fluid}' not found in database"
|
|
185
|
+
assert 'density' in self.funcmap[fluid], f"No density data available for '{fluid}'"
|
|
186
|
+
|
|
187
|
+
if fluid in ['Argon', 'Hydrogen']:
|
|
188
|
+
assert P == P, "Pressure required for Argon and Hydrogen"
|
|
189
|
+
return self.funcmap[fluid]['density'](T, P)
|
|
190
|
+
else:
|
|
191
|
+
return self.funcmap[fluid]['density'](T)
|
|
192
|
+
|
|
193
|
+
def viscosity(self, fluid: str, T: float, P: float = float('nan')):
|
|
194
|
+
"""Calculate dynamic viscosity [Pa-s] from temperature [K] and optional pressure [Pa]."""
|
|
195
|
+
assert fluid in self.funcmap.keys(), f"Fluid '{fluid}' not found in database"
|
|
196
|
+
assert 'viscosity' in self.funcmap[fluid], f"No viscosity data available for '{fluid}'"
|
|
197
|
+
return self.funcmap[fluid]['viscosity'](T)
|
|
198
|
+
|
|
199
|
+
def specheat(self, fluid: str, T: float, P: float = float('nan')):
|
|
200
|
+
"""Calculate specific heat [J/kg-K] from temperature [K] and optional pressure [Pa]."""
|
|
201
|
+
assert fluid in self.funcmap.keys(), f"Fluid '{fluid}' not found in database"
|
|
202
|
+
assert 'specheat' in self.funcmap[fluid], f"No specific heat data available for '{fluid}'"
|
|
203
|
+
return self.funcmap[fluid]['specheat'](T) * 1000. # Convert kJ/kg-K → J/kg-K
|
|
204
|
+
|
|
205
|
+
def print_fluids(self):
|
|
206
|
+
"""Print list of fluids in database"""
|
|
207
|
+
for fluid in self.funcmap.keys():
|
|
208
|
+
available_properties = self.funcmap[fluid].keys()
|
|
209
|
+
abbrev_properties = {'density': 'ρ', 'viscosity': 'μ', 'specheat': 'c'}
|
|
210
|
+
abbrev_available = [abbrev_properties[prop] for prop in available_properties]
|
|
211
|
+
print(fluid + " → (" + ", ".join(abbrev_available) + ")")
|
|
212
|
+
|
|
213
|
+
if __name__ == "__main__":
|
|
214
|
+
props = Incompressible()
|
|
215
|
+
props.print_fluids()
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This file serves as a template for a Component library.
|
|
3
|
+
|
|
4
|
+
The file is stuctured to include definitions for multiple Components.
|
|
5
|
+
|
|
6
|
+
To make this library available to esclab, add the following line to src/esclab/components/__init__.py:
|
|
7
|
+
from .template_component import *
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
# import necessary simulation tools
|
|
11
|
+
from esclab.simulate import *
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------
|
|
15
|
+
# Define component classes. Each component is a class that inherits from Component, and defines its own parameters, inputs, outputs, and calculate() method.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FirstComponent(Component):
|
|
19
|
+
"""Template component - a simple calculator"""
|
|
20
|
+
# Parameters.
|
|
21
|
+
# Any parameter not given a default value must be set by the user before running the simulation.
|
|
22
|
+
a = Component.Parameter() # multiplier
|
|
23
|
+
b = Component.Parameter() # offset
|
|
24
|
+
# Inputs.
|
|
25
|
+
# The default input initial value is 1, but this can be passed as an argument here, or changed by directly setting the input value to a different number before running the simulation.
|
|
26
|
+
x_in = Component.Input() # input variable
|
|
27
|
+
# Outputs.
|
|
28
|
+
# The output value will be whatever is assigned in calculate().
|
|
29
|
+
y_out = Component.Output() # output variable
|
|
30
|
+
z_out = Component.Output() # another output variable
|
|
31
|
+
|
|
32
|
+
def __init__(self):
|
|
33
|
+
# [optional]
|
|
34
|
+
# initialize any internal variables here. Items stored as attributes of self will persist across time steps and iterations, and can be used to store intermediate values or state information.
|
|
35
|
+
super().__init__() # call the parent class constructor [required if you define __init__()]
|
|
36
|
+
self.x_last = 0. # example of an internal variable to store the value of the input from the last time step
|
|
37
|
+
|
|
38
|
+
def presim_setup(self, **kwargs):
|
|
39
|
+
super().presim_setup(**kwargs)
|
|
40
|
+
# [optional] do any setup before the simulation starts here. This method is called once after the model is initialized, and before the first time step. You can use this to set up any internal variables or perform any calculations that are needed before the simulation starts. This differs from __init__ in that the model and all components are guaranteed to be fully initialized at this point, so you can access other components and their parameters and outputs if needed.
|
|
41
|
+
return
|
|
42
|
+
|
|
43
|
+
def calculate(self):
|
|
44
|
+
# [required] define the equations that govern the behavior of the component here. This method is called at every time step and iteration.
|
|
45
|
+
|
|
46
|
+
# super().calculate() # << call this if inheriting from a parent class that also uses calculate(). Not needed otherwise.
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------
|
|
49
|
+
# Do any first timestep calculations
|
|
50
|
+
if self.is_first_timestep:
|
|
51
|
+
# do any calculations that are needed at the first time step here. For example, you might
|
|
52
|
+
# want to set the output to a specific value at the first time step, or calculate an initial
|
|
53
|
+
# condition based on the input value at the first time step.
|
|
54
|
+
# return # << uncomment to skip the rest of the calculations for the first time step
|
|
55
|
+
pass # continue out of this block if regular calculations are needed at the first time step
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------
|
|
58
|
+
# Do post-convergence calculations
|
|
59
|
+
if self.is_converged:
|
|
60
|
+
# do any calculations that are needed after convergence at each step(). For example, you
|
|
61
|
+
# might want to update the output based on the input and parameters after convergence is
|
|
62
|
+
# reached, or calculate some additional outputs that are only needed after convergence.
|
|
63
|
+
self.x_last = self.x_in # store the input value for use in the next time step
|
|
64
|
+
return # skip the rest of the calculations if needed
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------
|
|
69
|
+
# Do regular calculations for each iteration here. This is where the main behavior of the component
|
|
70
|
+
# should be defined. For example, you might want to define the output as a function of the input
|
|
71
|
+
# and parameters here.
|
|
72
|
+
|
|
73
|
+
self.y_out = self.a * self.x_in + self.b # example calculation for y_out
|
|
74
|
+
self.z_out = self.x_in - self.x_last # example calculation for z_out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------
|
|
78
|
+
# If using the simultaneous equation solving feature, add equations to self.coupled_eqs here instead
|
|
79
|
+
# of directly calculating the outputs. This allows the equations to be solved simultaneously with
|
|
80
|
+
# other components in the same solve group. For example:
|
|
81
|
+
if self.coupled_eqs is not None: # check if we are in the matrix-build phase
|
|
82
|
+
# Note that we use self.coupled_eqs.source(self.x_in) to refer to the input variable in the
|
|
83
|
+
# equation, which allows the solver to correctly link it to the output of whatever component
|
|
84
|
+
# is connected to this input. Point to the output variable directly (self.y_out) since it is
|
|
85
|
+
# an output of this component.
|
|
86
|
+
|
|
87
|
+
# add an equation for y_out: y_out = a * x_in + b --> y_out - a*x_in = b
|
|
88
|
+
self.coupled_eqs.add_equation({
|
|
89
|
+
self.y_out: 1.0,
|
|
90
|
+
self.coupled_eqs.source(self.x_in): -1.0,
|
|
91
|
+
}, rhs=self.b)
|
|
92
|
+
# add an equation for z_out: z_out = x_in - x_last --> z_out - x_in = -x_last
|
|
93
|
+
self.coupled_eqs.add_equation({
|
|
94
|
+
self.z_out: 1.0,
|
|
95
|
+
self.coupled_eqs.source(self.x_in): -1.0,
|
|
96
|
+
}, rhs=-self.x_last)
|
|
97
|
+
|
|
98
|
+
# The syntax of the add_equation() method is as follows:
|
|
99
|
+
# {var1: coeff1, | dictionary of variables and their
|
|
100
|
+
# var2: coeff2, | coefficients in the equation
|
|
101
|
+
# ...}, |
|
|
102
|
+
|
|
103
|
+
# rhs=value | right-hand side value of the equation
|
|
104
|
+
# | (any constants that are not multiplied
|
|
105
|
+
# | by a variable should be included here)
|
|
106
|
+
# ----------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
# ----------------------------------------------------------
|
|
109
|
+
class SecondComponent(Component):
|
|
110
|
+
"""Another example component. This one doesn't do anything, but serves as an example of how to define multiple components in the same file."""
|
|
111
|
+
x_in = Component.Input()
|
|
112
|
+
y_out = Component.Output()
|
|
113
|
+
def calculate(self):
|
|
114
|
+
if self.coupled_eqs is not None: # check if we are in the matrix-build phase
|
|
115
|
+
# add an equation for y_out: y_out = x_in --> y_out - x_in = 0
|
|
116
|
+
self.coupled_eqs.add_equation({
|
|
117
|
+
self.y_out: 1.0,
|
|
118
|
+
self.coupled_eqs.source(self.x_in): -1.0,
|
|
119
|
+
}, rhs=0.0)
|
|
120
|
+
# ----------------------------------------------------------
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# This file marks the models directory as a Python package.
|