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 ADDED
@@ -0,0 +1,8 @@
1
+ # This file marks the src directory as a Python package.
2
+ """
3
+ ESCLab: Engineering Simulation and Control Lab
4
+ """
5
+
6
+ __version__ = "0.0.1"
7
+ __author__ = "Mike Wagner"
8
+ __email__ = "mjwagner2@wisc.edu"
@@ -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.