engcalc 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- engcalc/__init__.py +3 -0
- engcalc/cycles.py +148 -0
- engcalc/data/materials.json +122 -0
- engcalc/fluids.py +182 -0
- engcalc/heat.py +152 -0
- engcalc/ideal_gas.py +36 -0
- engcalc/materials.py +133 -0
- engcalc/units.py +253 -0
- engcalc-0.1.0.dist-info/METADATA +164 -0
- engcalc-0.1.0.dist-info/RECORD +12 -0
- engcalc-0.1.0.dist-info/WHEEL +4 -0
- engcalc-0.1.0.dist-info/licenses/LICENSE +21 -0
engcalc/__init__.py
ADDED
engcalc/cycles.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Thermodynamic cycle efficiency calculations.
|
|
2
|
+
|
|
3
|
+
Reference: Cengel & Boles, "Thermodynamics: An Engineering Approach"
|
|
4
|
+
All temperatures must be in Kelvin (K).
|
|
5
|
+
Efficiency is returned as a fraction between 0 and 1 (multiply by 100 for %).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def carnot_efficiency(T_hot: float, T_cold: float) -> float:
|
|
10
|
+
"""Carnot cycle thermal efficiency.
|
|
11
|
+
|
|
12
|
+
The Carnot cycle is the most efficient cycle operating between
|
|
13
|
+
two thermal reservoirs.
|
|
14
|
+
|
|
15
|
+
Formula:
|
|
16
|
+
eta = 1 - T_cold / T_hot
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
T_hot: Hot reservoir temperature in Kelvin (K).
|
|
20
|
+
T_cold: Cold reservoir temperature in Kelvin (K).
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Thermal efficiency as a fraction (0 to 1).
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If T_hot <= T_cold or T_hot <= 0.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> carnot_efficiency(T_hot=800, T_cold=300)
|
|
30
|
+
0.625
|
|
31
|
+
"""
|
|
32
|
+
if T_hot <= 0:
|
|
33
|
+
raise ValueError("T_hot must be positive (Kelvin).")
|
|
34
|
+
if T_cold <= 0:
|
|
35
|
+
raise ValueError("T_cold must be positive (Kelvin).")
|
|
36
|
+
if T_hot <= T_cold:
|
|
37
|
+
raise ValueError("T_hot must be greater than T_cold.")
|
|
38
|
+
|
|
39
|
+
return 1 - (T_cold / T_hot)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def otto_efficiency(compression_ratio: float, gamma: float = 1.4) -> float:
|
|
43
|
+
"""Otto cycle thermal efficiency (petrol engine).
|
|
44
|
+
|
|
45
|
+
Formula:
|
|
46
|
+
eta = 1 - 1 / r^(gamma - 1)
|
|
47
|
+
|
|
48
|
+
where:
|
|
49
|
+
r = compression ratio (V1/V2)
|
|
50
|
+
gamma = specific heat ratio (Cp/Cv), default 1.4 for air
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
compression_ratio: Compression ratio r (> 1).
|
|
54
|
+
gamma: Specific heat ratio, default 1.4 for air.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Thermal efficiency as a fraction (0 to 1).
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
ValueError: If compression_ratio <= 1 or gamma <= 1.
|
|
61
|
+
|
|
62
|
+
Example:
|
|
63
|
+
>>> otto_efficiency(compression_ratio=8)
|
|
64
|
+
0.5647...
|
|
65
|
+
"""
|
|
66
|
+
if compression_ratio <= 1:
|
|
67
|
+
raise ValueError("Compression ratio must be greater than 1.")
|
|
68
|
+
if gamma <= 1:
|
|
69
|
+
raise ValueError("Gamma must be greater than 1.")
|
|
70
|
+
|
|
71
|
+
return 1 - 1 / (compression_ratio ** (gamma - 1))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def diesel_efficiency(
|
|
75
|
+
compression_ratio: float,
|
|
76
|
+
cutoff_ratio: float,
|
|
77
|
+
gamma: float = 1.4,
|
|
78
|
+
) -> float:
|
|
79
|
+
"""Diesel cycle thermal efficiency.
|
|
80
|
+
|
|
81
|
+
Formula:
|
|
82
|
+
eta = 1 - (1 / r^(gamma-1)) * (rc^gamma - 1) / (gamma * (rc - 1))
|
|
83
|
+
|
|
84
|
+
where:
|
|
85
|
+
r = compression ratio
|
|
86
|
+
rc = cutoff ratio (V3/V2)
|
|
87
|
+
gamma = specific heat ratio
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
compression_ratio: Compression ratio r (> 1).
|
|
91
|
+
cutoff_ratio: Cutoff ratio rc (> 1).
|
|
92
|
+
gamma: Specific heat ratio, default 1.4 for air.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Thermal efficiency as a fraction (0 to 1).
|
|
96
|
+
|
|
97
|
+
Raises:
|
|
98
|
+
ValueError: If inputs are out of valid range.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> diesel_efficiency(compression_ratio=18, cutoff_ratio=2)
|
|
102
|
+
0.631...
|
|
103
|
+
"""
|
|
104
|
+
if compression_ratio <= 1:
|
|
105
|
+
raise ValueError("Compression ratio must be greater than 1.")
|
|
106
|
+
if cutoff_ratio <= 1:
|
|
107
|
+
raise ValueError("Cutoff ratio must be greater than 1.")
|
|
108
|
+
if gamma <= 1:
|
|
109
|
+
raise ValueError("Gamma must be greater than 1.")
|
|
110
|
+
|
|
111
|
+
r = compression_ratio
|
|
112
|
+
rc = cutoff_ratio
|
|
113
|
+
|
|
114
|
+
term1 = 1 / (r ** (gamma - 1))
|
|
115
|
+
term2 = (rc**gamma - 1) / (gamma * (rc - 1))
|
|
116
|
+
|
|
117
|
+
return 1 - term1 * term2
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def brayton_efficiency(pressure_ratio: float, gamma: float = 1.4) -> float:
|
|
121
|
+
"""Brayton cycle thermal efficiency (gas turbine).
|
|
122
|
+
|
|
123
|
+
Formula:
|
|
124
|
+
eta = 1 - 1 / rp^((gamma-1)/gamma)
|
|
125
|
+
|
|
126
|
+
where:
|
|
127
|
+
rp = pressure ratio (P2/P1)
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
pressure_ratio: Pressure ratio rp (> 1).
|
|
131
|
+
gamma: Specific heat ratio, default 1.4 for air.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Thermal efficiency as a fraction (0 to 1).
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
ValueError: If inputs are out of valid range.
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
>>> brayton_efficiency(pressure_ratio=10)
|
|
141
|
+
0.482...
|
|
142
|
+
"""
|
|
143
|
+
if pressure_ratio <= 1:
|
|
144
|
+
raise ValueError("Pressure ratio must be greater than 1.")
|
|
145
|
+
if gamma <= 1:
|
|
146
|
+
raise ValueError("Gamma must be greater than 1.")
|
|
147
|
+
|
|
148
|
+
return 1 - 1 / (pressure_ratio ** ((gamma - 1) / gamma))
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
{
|
|
2
|
+
"steel_AISI_1018": {
|
|
3
|
+
"name": "AISI 1018 Carbon Steel",
|
|
4
|
+
"category": "steel",
|
|
5
|
+
"density": 7870,
|
|
6
|
+
"youngs_modulus": 205e9,
|
|
7
|
+
"poisson_ratio": 0.29,
|
|
8
|
+
"thermal_conductivity": 51.9,
|
|
9
|
+
"specific_heat": 486,
|
|
10
|
+
"thermal_expansion": 11.7e-6,
|
|
11
|
+
"yield_strength": 370e6,
|
|
12
|
+
"ultimate_strength": 440e6
|
|
13
|
+
},
|
|
14
|
+
"steel_AISI_1040": {
|
|
15
|
+
"name": "AISI 1040 Carbon Steel",
|
|
16
|
+
"category": "steel",
|
|
17
|
+
"density": 7850,
|
|
18
|
+
"youngs_modulus": 200e9,
|
|
19
|
+
"poisson_ratio": 0.29,
|
|
20
|
+
"thermal_conductivity": 50.7,
|
|
21
|
+
"specific_heat": 490,
|
|
22
|
+
"thermal_expansion": 11.3e-6,
|
|
23
|
+
"yield_strength": 415e6,
|
|
24
|
+
"ultimate_strength": 620e6
|
|
25
|
+
},
|
|
26
|
+
"steel_AISI_4140": {
|
|
27
|
+
"name": "AISI 4140 Alloy Steel",
|
|
28
|
+
"category": "steel",
|
|
29
|
+
"density": 7850,
|
|
30
|
+
"youngs_modulus": 205e9,
|
|
31
|
+
"poisson_ratio": 0.29,
|
|
32
|
+
"thermal_conductivity": 42.6,
|
|
33
|
+
"specific_heat": 473,
|
|
34
|
+
"thermal_expansion": 12.2e-6,
|
|
35
|
+
"yield_strength": 655e6,
|
|
36
|
+
"ultimate_strength": 1020e6
|
|
37
|
+
},
|
|
38
|
+
"steel_SS_304": {
|
|
39
|
+
"name": "Stainless Steel 304",
|
|
40
|
+
"category": "steel",
|
|
41
|
+
"density": 8000,
|
|
42
|
+
"youngs_modulus": 193e9,
|
|
43
|
+
"poisson_ratio": 0.29,
|
|
44
|
+
"thermal_conductivity": 16.2,
|
|
45
|
+
"specific_heat": 500,
|
|
46
|
+
"thermal_expansion": 17.3e-6,
|
|
47
|
+
"yield_strength": 215e6,
|
|
48
|
+
"ultimate_strength": 505e6
|
|
49
|
+
},
|
|
50
|
+
"steel_SS_316": {
|
|
51
|
+
"name": "Stainless Steel 316",
|
|
52
|
+
"category": "steel",
|
|
53
|
+
"density": 8000,
|
|
54
|
+
"youngs_modulus": 193e9,
|
|
55
|
+
"poisson_ratio": 0.27,
|
|
56
|
+
"thermal_conductivity": 16.3,
|
|
57
|
+
"specific_heat": 500,
|
|
58
|
+
"thermal_expansion": 16.0e-6,
|
|
59
|
+
"yield_strength": 205e6,
|
|
60
|
+
"ultimate_strength": 515e6
|
|
61
|
+
},
|
|
62
|
+
"aluminum_6061_T6": {
|
|
63
|
+
"name": "Aluminum 6061-T6",
|
|
64
|
+
"category": "aluminum",
|
|
65
|
+
"density": 2700,
|
|
66
|
+
"youngs_modulus": 68.9e9,
|
|
67
|
+
"poisson_ratio": 0.33,
|
|
68
|
+
"thermal_conductivity": 167,
|
|
69
|
+
"specific_heat": 896,
|
|
70
|
+
"thermal_expansion": 23.6e-6,
|
|
71
|
+
"yield_strength": 276e6,
|
|
72
|
+
"ultimate_strength": 310e6
|
|
73
|
+
},
|
|
74
|
+
"aluminum_7075_T6": {
|
|
75
|
+
"name": "Aluminum 7075-T6",
|
|
76
|
+
"category": "aluminum",
|
|
77
|
+
"density": 2810,
|
|
78
|
+
"youngs_modulus": 71.7e9,
|
|
79
|
+
"poisson_ratio": 0.33,
|
|
80
|
+
"thermal_conductivity": 130,
|
|
81
|
+
"specific_heat": 960,
|
|
82
|
+
"thermal_expansion": 23.6e-6,
|
|
83
|
+
"yield_strength": 503e6,
|
|
84
|
+
"ultimate_strength": 572e6
|
|
85
|
+
},
|
|
86
|
+
"copper_C11000": {
|
|
87
|
+
"name": "Copper C11000 (ETP)",
|
|
88
|
+
"category": "copper",
|
|
89
|
+
"density": 8940,
|
|
90
|
+
"youngs_modulus": 117e9,
|
|
91
|
+
"poisson_ratio": 0.34,
|
|
92
|
+
"thermal_conductivity": 401,
|
|
93
|
+
"specific_heat": 385,
|
|
94
|
+
"thermal_expansion": 17.0e-6,
|
|
95
|
+
"yield_strength": 69e6,
|
|
96
|
+
"ultimate_strength": 220e6
|
|
97
|
+
},
|
|
98
|
+
"titanium_Ti_6Al_4V": {
|
|
99
|
+
"name": "Titanium Ti-6Al-4V",
|
|
100
|
+
"category": "titanium",
|
|
101
|
+
"density": 4430,
|
|
102
|
+
"youngs_modulus": 113.8e9,
|
|
103
|
+
"poisson_ratio": 0.342,
|
|
104
|
+
"thermal_conductivity": 6.7,
|
|
105
|
+
"specific_heat": 526,
|
|
106
|
+
"thermal_expansion": 8.6e-6,
|
|
107
|
+
"yield_strength": 880e6,
|
|
108
|
+
"ultimate_strength": 950e6
|
|
109
|
+
},
|
|
110
|
+
"cast_iron_gray": {
|
|
111
|
+
"name": "Gray Cast Iron",
|
|
112
|
+
"category": "cast_iron",
|
|
113
|
+
"density": 7200,
|
|
114
|
+
"youngs_modulus": 100e9,
|
|
115
|
+
"poisson_ratio": 0.29,
|
|
116
|
+
"thermal_conductivity": 52,
|
|
117
|
+
"specific_heat": 490,
|
|
118
|
+
"thermal_expansion": 10.8e-6,
|
|
119
|
+
"yield_strength": null,
|
|
120
|
+
"ultimate_strength": 150e6
|
|
121
|
+
}
|
|
122
|
+
}
|
engcalc/fluids.py
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"""Fluid mechanics calculations.
|
|
2
|
+
|
|
3
|
+
Reference: Munson et al., "Fundamentals of Fluid Mechanics"
|
|
4
|
+
All values in SI units unless stated otherwise.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import math
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def reynolds_number(
|
|
11
|
+
rho: float,
|
|
12
|
+
velocity: float,
|
|
13
|
+
diameter: float,
|
|
14
|
+
mu: float,
|
|
15
|
+
) -> float:
|
|
16
|
+
"""Reynolds number for pipe flow.
|
|
17
|
+
|
|
18
|
+
Formula:
|
|
19
|
+
Re = (rho * v * D) / mu
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
rho: Fluid density (kg/m^3).
|
|
23
|
+
velocity: Flow velocity (m/s).
|
|
24
|
+
diameter: Pipe inner diameter (m).
|
|
25
|
+
mu: Dynamic viscosity (Pa·s).
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Reynolds number (dimensionless).
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
ValueError: If mu <= 0 or any input is negative.
|
|
32
|
+
|
|
33
|
+
Example:
|
|
34
|
+
>>> reynolds_number(rho=1000, velocity=2, diameter=0.05, mu=0.001)
|
|
35
|
+
100000.0
|
|
36
|
+
"""
|
|
37
|
+
if rho < 0 or velocity < 0 or diameter < 0:
|
|
38
|
+
raise ValueError("rho, velocity, diameter must be non-negative.")
|
|
39
|
+
if mu <= 0:
|
|
40
|
+
raise ValueError("Dynamic viscosity must be positive.")
|
|
41
|
+
return (rho * velocity * diameter) / mu
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def flow_regime(Re: float) -> str:
|
|
45
|
+
"""Classify flow regime based on Reynolds number.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
Re: Reynolds number.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
"laminar" (Re < 2300), "transitional" (2300 ≤ Re < 4000),
|
|
52
|
+
or "turbulent" (Re ≥ 4000).
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> flow_regime(1500)
|
|
56
|
+
'laminar'
|
|
57
|
+
>>> flow_regime(5000)
|
|
58
|
+
'turbulent'
|
|
59
|
+
"""
|
|
60
|
+
if Re < 2300:
|
|
61
|
+
return "laminar"
|
|
62
|
+
elif Re < 4000:
|
|
63
|
+
return "transitional"
|
|
64
|
+
else:
|
|
65
|
+
return "turbulent"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def bernoulli_pressure(
|
|
69
|
+
P1: float,
|
|
70
|
+
v1: float,
|
|
71
|
+
h1: float,
|
|
72
|
+
v2: float,
|
|
73
|
+
h2: float,
|
|
74
|
+
rho: float,
|
|
75
|
+
g: float = 9.81,
|
|
76
|
+
) -> float:
|
|
77
|
+
"""Bernoulli equation: find P2 given state 1 and velocity/elevation at state 2.
|
|
78
|
+
|
|
79
|
+
Formula (incompressible, no losses):
|
|
80
|
+
P1 + 0.5*rho*v1^2 + rho*g*h1 = P2 + 0.5*rho*v2^2 + rho*g*h2
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
P1: Pressure at point 1 (Pa).
|
|
84
|
+
v1: Velocity at point 1 (m/s).
|
|
85
|
+
h1: Elevation at point 1 (m).
|
|
86
|
+
v2: Velocity at point 2 (m/s).
|
|
87
|
+
h2: Elevation at point 2 (m).
|
|
88
|
+
rho: Fluid density (kg/m^3).
|
|
89
|
+
g: Gravitational acceleration (m/s^2), default 9.81.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Pressure at point 2 (Pa).
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
ValueError: If rho <= 0.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
>>> bernoulli_pressure(P1=101325, v1=0, h1=0, v2=5, h2=0, rho=1000)
|
|
99
|
+
88825.0
|
|
100
|
+
"""
|
|
101
|
+
if rho <= 0:
|
|
102
|
+
raise ValueError("Density must be positive.")
|
|
103
|
+
return (
|
|
104
|
+
P1
|
|
105
|
+
+ 0.5 * rho * (v1**2 - v2**2)
|
|
106
|
+
+ rho * g * (h1 - h2)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def darcy_weisbach_head_loss(
|
|
111
|
+
f: float,
|
|
112
|
+
L: float,
|
|
113
|
+
D: float,
|
|
114
|
+
velocity: float,
|
|
115
|
+
g: float = 9.81,
|
|
116
|
+
) -> float:
|
|
117
|
+
"""Darcy-Weisbach head loss in a pipe.
|
|
118
|
+
|
|
119
|
+
Formula:
|
|
120
|
+
h_f = f * (L/D) * (v^2 / (2*g))
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
f: Darcy friction factor (dimensionless).
|
|
124
|
+
L: Pipe length (m).
|
|
125
|
+
D: Pipe inner diameter (m).
|
|
126
|
+
velocity: Flow velocity (m/s).
|
|
127
|
+
g: Gravitational acceleration (m/s^2), default 9.81.
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
Head loss (m).
|
|
131
|
+
|
|
132
|
+
Raises:
|
|
133
|
+
ValueError: If D <= 0 or f < 0.
|
|
134
|
+
|
|
135
|
+
Example:
|
|
136
|
+
>>> darcy_weisbach_head_loss(f=0.02, L=100, D=0.05, velocity=2)
|
|
137
|
+
8.154...
|
|
138
|
+
"""
|
|
139
|
+
if D <= 0:
|
|
140
|
+
raise ValueError("Diameter must be positive.")
|
|
141
|
+
if f < 0:
|
|
142
|
+
raise ValueError("Friction factor must be non-negative.")
|
|
143
|
+
return f * (L / D) * (velocity**2 / (2 * g))
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def pump_power(
|
|
147
|
+
rho: float,
|
|
148
|
+
g: float,
|
|
149
|
+
Q: float,
|
|
150
|
+
head: float,
|
|
151
|
+
efficiency: float = 1.0,
|
|
152
|
+
) -> float:
|
|
153
|
+
"""Hydraulic power required by a pump.
|
|
154
|
+
|
|
155
|
+
Formula:
|
|
156
|
+
P_hydraulic = rho * g * Q * H
|
|
157
|
+
P_shaft = P_hydraulic / efficiency
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
rho: Fluid density (kg/m^3).
|
|
161
|
+
g: Gravitational acceleration (m/s^2).
|
|
162
|
+
Q: Volumetric flow rate (m^3/s).
|
|
163
|
+
head: Pump head (m).
|
|
164
|
+
efficiency: Pump efficiency (0 < eta <= 1), default 1.0.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Shaft power required (W).
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
ValueError: If efficiency not in (0, 1] or inputs invalid.
|
|
171
|
+
|
|
172
|
+
Example:
|
|
173
|
+
>>> pump_power(rho=1000, g=9.81, Q=0.01, head=10, efficiency=0.8)
|
|
174
|
+
1226.25
|
|
175
|
+
"""
|
|
176
|
+
if rho <= 0 or g <= 0 or Q < 0 or head < 0:
|
|
177
|
+
raise ValueError("Invalid input: rho, g must be positive; Q, head non-negative.")
|
|
178
|
+
if not (0 < efficiency <= 1):
|
|
179
|
+
raise ValueError("Efficiency must be in (0, 1].")
|
|
180
|
+
|
|
181
|
+
P_hydraulic = rho * g * Q * head
|
|
182
|
+
return P_hydraulic / efficiency
|
engcalc/heat.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Heat transfer calculations.
|
|
2
|
+
|
|
3
|
+
Reference: Incropera et al., "Fundamentals of Heat and Mass Transfer"
|
|
4
|
+
All values in SI units.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def conduction_rate(
|
|
9
|
+
k: float,
|
|
10
|
+
A: float,
|
|
11
|
+
dT: float,
|
|
12
|
+
L: float,
|
|
13
|
+
) -> float:
|
|
14
|
+
"""Fourier's law of conduction through a plane wall.
|
|
15
|
+
|
|
16
|
+
Formula:
|
|
17
|
+
Q = k * A * dT / L
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
k: Thermal conductivity (W/(m·K)).
|
|
21
|
+
A: Cross-sectional area (m^2).
|
|
22
|
+
dT: Temperature difference (K).
|
|
23
|
+
L: Wall thickness (m).
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Heat transfer rate (W).
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: If L <= 0 or k < 0 or A < 0.
|
|
30
|
+
|
|
31
|
+
Example:
|
|
32
|
+
>>> conduction_rate(k=50, A=2, dT=100, L=0.1)
|
|
33
|
+
100000.0
|
|
34
|
+
"""
|
|
35
|
+
if L <= 0:
|
|
36
|
+
raise ValueError("Thickness L must be positive.")
|
|
37
|
+
if k < 0 or A < 0:
|
|
38
|
+
raise ValueError("k and A must be non-negative.")
|
|
39
|
+
return k * A * dT / L
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def convection_rate(
|
|
43
|
+
h: float,
|
|
44
|
+
A: float,
|
|
45
|
+
T_surface: float,
|
|
46
|
+
T_fluid: float,
|
|
47
|
+
) -> float:
|
|
48
|
+
"""Newton's law of cooling for convection.
|
|
49
|
+
|
|
50
|
+
Formula:
|
|
51
|
+
Q = h * A * (T_surface - T_fluid)
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
h: Convective heat transfer coefficient (W/(m^2·K)).
|
|
55
|
+
A: Surface area (m^2).
|
|
56
|
+
T_surface: Surface temperature (K or °C).
|
|
57
|
+
T_fluid: Fluid temperature (K or °C).
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Heat transfer rate (W).
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
ValueError: If h < 0 or A < 0.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
>>> convection_rate(h=25, A=2, T_surface=350, T_fluid=300)
|
|
67
|
+
2500.0
|
|
68
|
+
"""
|
|
69
|
+
if h < 0 or A < 0:
|
|
70
|
+
raise ValueError("h and A must be non-negative.")
|
|
71
|
+
return h * A * (T_surface - T_fluid)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def radiation_rate(
|
|
75
|
+
emissivity: float,
|
|
76
|
+
A: float,
|
|
77
|
+
T_surface: float,
|
|
78
|
+
T_surroundings: float,
|
|
79
|
+
sigma: float = 5.67e-8,
|
|
80
|
+
) -> float:
|
|
81
|
+
"""Stefan-Boltzmann law for radiation heat transfer.
|
|
82
|
+
|
|
83
|
+
Formula:
|
|
84
|
+
Q = emissivity * sigma * A * (T_surface^4 - T_surroundings^4)
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
emissivity: Surface emissivity (0 to 1).
|
|
88
|
+
A: Surface area (m^2).
|
|
89
|
+
T_surface: Surface temperature (K).
|
|
90
|
+
T_surroundings: Surrounding temperature (K).
|
|
91
|
+
sigma: Stefan-Boltzmann constant (W/(m^2·K^4)), default 5.67e-8.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
Net radiation heat transfer rate (W).
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
ValueError: If emissivity not in [0, 1], or temperatures negative.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> radiation_rate(emissivity=0.9, A=1, T_surface=500, T_surroundings=300)
|
|
101
|
+
2892.9...
|
|
102
|
+
"""
|
|
103
|
+
if not (0 <= emissivity <= 1):
|
|
104
|
+
raise ValueError("Emissivity must be between 0 and 1.")
|
|
105
|
+
if T_surface < 0 or T_surroundings < 0:
|
|
106
|
+
raise ValueError("Temperatures must be non-negative (Kelvin).")
|
|
107
|
+
return emissivity * sigma * A * (T_surface**4 - T_surroundings**4)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def lmtd(
|
|
111
|
+
T_hot_in: float,
|
|
112
|
+
T_hot_out: float,
|
|
113
|
+
T_cold_in: float,
|
|
114
|
+
T_cold_out: float,
|
|
115
|
+
) -> float:
|
|
116
|
+
"""Log Mean Temperature Difference for heat exchangers.
|
|
117
|
+
|
|
118
|
+
Formula:
|
|
119
|
+
dT1 = T_hot_in - T_cold_out
|
|
120
|
+
dT2 = T_hot_out - T_cold_in
|
|
121
|
+
LMTD = (dT1 - dT2) / ln(dT1 / dT2)
|
|
122
|
+
|
|
123
|
+
If dT1 ≈ dT2, returns dT1 (limit case).
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
T_hot_in: Hot fluid inlet temperature.
|
|
127
|
+
T_hot_out: Hot fluid outlet temperature.
|
|
128
|
+
T_cold_in: Cold fluid inlet temperature.
|
|
129
|
+
T_cold_out: Cold fluid outlet temperature.
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
LMTD in same units as inputs.
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
ValueError: If dT1 <= 0 or dT2 <= 0 (invalid exchanger).
|
|
136
|
+
|
|
137
|
+
Example:
|
|
138
|
+
>>> lmtd(T_hot_in=400, T_hot_out=350, T_cold_in=300, T_cold_out=330)
|
|
139
|
+
59.44...
|
|
140
|
+
"""
|
|
141
|
+
dT1 = T_hot_in - T_cold_out
|
|
142
|
+
dT2 = T_hot_out - T_cold_in
|
|
143
|
+
|
|
144
|
+
if dT1 <= 0 or dT2 <= 0:
|
|
145
|
+
raise ValueError("Temperature differences must be positive.")
|
|
146
|
+
|
|
147
|
+
# Handle equal case (limit)
|
|
148
|
+
if abs(dT1 - dT2) < 1e-9:
|
|
149
|
+
return dT1
|
|
150
|
+
|
|
151
|
+
import math
|
|
152
|
+
return (dT1 - dT2) / math.log(dT1 / dT2)
|
engcalc/ideal_gas.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""Ideal gas law calculations.
|
|
2
|
+
|
|
3
|
+
The ideal gas law: PV = nRT
|
|
4
|
+
where:
|
|
5
|
+
P = pressure (Pa)
|
|
6
|
+
V = volume (m^3)
|
|
7
|
+
n = amount of substance (mol)
|
|
8
|
+
R = universal gas constant (8.314 J/(mol·K))
|
|
9
|
+
T = absolute temperature (K)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# Universal gas constant in SI units: J/(mol·K)
|
|
13
|
+
R = 8.314462618
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def pressure(n: float, T: float, V: float) -> float:
|
|
17
|
+
"""Calculate pressure using ideal gas law: P = nRT/V.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
n: Amount of substance in moles (mol).
|
|
21
|
+
T: Absolute temperature in Kelvin (K).
|
|
22
|
+
V: Volume in cubic meters (m^3).
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Pressure in Pascals (Pa).
|
|
26
|
+
|
|
27
|
+
Raises:
|
|
28
|
+
ValueError: If V is zero or negative.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> pressure(n=1, T=300, V=0.024)
|
|
32
|
+
103930.782725
|
|
33
|
+
"""
|
|
34
|
+
if V <= 0:
|
|
35
|
+
raise ValueError("Volume must be positive.")
|
|
36
|
+
return (n * R * T) / V
|
engcalc/materials.py
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""Material property database for engineering calculations.
|
|
2
|
+
|
|
3
|
+
Data is loaded from a bundled JSON file. Properties are typical
|
|
4
|
+
textbook values — verify against manufacturer datasheets before
|
|
5
|
+
critical design use.
|
|
6
|
+
|
|
7
|
+
All values in SI units:
|
|
8
|
+
density : kg/m^3
|
|
9
|
+
youngs_modulus : Pa
|
|
10
|
+
poisson_ratio : dimensionless
|
|
11
|
+
thermal_conductivity: W/(m·K)
|
|
12
|
+
specific_heat : J/(kg·K)
|
|
13
|
+
thermal_expansion : 1/K
|
|
14
|
+
yield_strength : Pa
|
|
15
|
+
ultimate_strength : Pa
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import json
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
# Load material data once at import time
|
|
23
|
+
_DATA_FILE = Path(__file__).parent / "data" / "materials.json"
|
|
24
|
+
|
|
25
|
+
with open(_DATA_FILE, encoding="utf-8") as f:
|
|
26
|
+
_MATERIALS: dict[str, dict[str, Any]] = json.load(f)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def list_materials() -> list[str]:
|
|
30
|
+
"""Return a sorted list of all available material keys.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of material IDs.
|
|
34
|
+
|
|
35
|
+
Example:
|
|
36
|
+
>>> "steel_AISI_1040" in list_materials()
|
|
37
|
+
True
|
|
38
|
+
"""
|
|
39
|
+
return sorted(_MATERIALS.keys())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def get(material: str, property_name: str) -> float:
|
|
43
|
+
"""Get a single property of a material.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
material: Material key (see list_materials()).
|
|
47
|
+
property_name: Property name, e.g. "density", "youngs_modulus".
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
Property value in SI units.
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
KeyError: If material or property does not exist.
|
|
54
|
+
ValueError: If property value is None (not available).
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
>>> get("steel_AISI_1040", "density")
|
|
58
|
+
7850
|
|
59
|
+
"""
|
|
60
|
+
if material not in _MATERIALS:
|
|
61
|
+
raise KeyError(
|
|
62
|
+
f"Unknown material: '{material}'. "
|
|
63
|
+
f"Use list_materials() to see available options."
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
props = _MATERIALS[material]
|
|
67
|
+
|
|
68
|
+
if property_name not in props:
|
|
69
|
+
raise KeyError(
|
|
70
|
+
f"Material '{material}' has no property '{property_name}'. "
|
|
71
|
+
f"Available: {sorted(props.keys())}"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
value = props[property_name]
|
|
75
|
+
|
|
76
|
+
if value is None:
|
|
77
|
+
raise ValueError(
|
|
78
|
+
f"Property '{property_name}' is not available for '{material}'."
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
return value
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_all(material: str) -> dict[str, Any]:
|
|
85
|
+
"""Get all properties of a material as a dictionary.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
material: Material key.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Dictionary of all properties.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
KeyError: If material does not exist.
|
|
95
|
+
|
|
96
|
+
Example:
|
|
97
|
+
>>> props = get_all("aluminum_6061_T6")
|
|
98
|
+
>>> props["density"]
|
|
99
|
+
2700
|
|
100
|
+
"""
|
|
101
|
+
if material not in _MATERIALS:
|
|
102
|
+
raise KeyError(f"Unknown material: '{material}'.")
|
|
103
|
+
|
|
104
|
+
return dict(_MATERIALS[material])
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def search(category: str) -> list[str]:
|
|
108
|
+
"""Find all materials in a given category.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
category: Category name, e.g. "steel", "aluminum".
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of material keys in that category.
|
|
115
|
+
|
|
116
|
+
Example:
|
|
117
|
+
>>> search("aluminum")
|
|
118
|
+
['aluminum_6061_T6', 'aluminum_7075_T6']
|
|
119
|
+
"""
|
|
120
|
+
return sorted(
|
|
121
|
+
key for key, props in _MATERIALS.items()
|
|
122
|
+
if props.get("category") == category
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def list_categories() -> list[str]:
|
|
127
|
+
"""Return a sorted list of all unique material categories.
|
|
128
|
+
|
|
129
|
+
Example:
|
|
130
|
+
>>> "steel" in list_categories()
|
|
131
|
+
True
|
|
132
|
+
"""
|
|
133
|
+
return sorted({props["category"] for props in _MATERIALS.values()})
|
engcalc/units.py
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""Unit conversion utilities for engineering calculations.
|
|
2
|
+
|
|
3
|
+
A pure-Python alternative to heavier libraries like `pint`, focused on
|
|
4
|
+
the units most commonly used in Mechanical Engineering.
|
|
5
|
+
|
|
6
|
+
Most categories work by defining a linear factor to a base unit:
|
|
7
|
+
value_in_base = value * factor
|
|
8
|
+
|
|
9
|
+
Temperature is special (has an offset), so it uses its own logic.
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from engcalc import units
|
|
13
|
+
>>> units.convert(100, "cm", "m")
|
|
14
|
+
1.0
|
|
15
|
+
>>> units.convert(1, "atm", "psi")
|
|
16
|
+
14.695948775513449
|
|
17
|
+
>>> units.convert(100, "degC", "K")
|
|
18
|
+
373.15
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Linear conversion tables
|
|
23
|
+
# Each category's first entry is the base unit with factor 1.0.
|
|
24
|
+
# factor = how many base units in one of this unit
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
_LINEAR_UNITS: dict[str, dict[str, float]] = {
|
|
28
|
+
"length": {
|
|
29
|
+
"m": 1.0,
|
|
30
|
+
"mm": 1e-3,
|
|
31
|
+
"cm": 1e-2,
|
|
32
|
+
"km": 1e3,
|
|
33
|
+
"in": 0.0254,
|
|
34
|
+
"ft": 0.3048,
|
|
35
|
+
"yd": 0.9144,
|
|
36
|
+
"mile": 1609.344,
|
|
37
|
+
},
|
|
38
|
+
"mass": {
|
|
39
|
+
"kg": 1.0,
|
|
40
|
+
"g": 1e-3,
|
|
41
|
+
"mg": 1e-6,
|
|
42
|
+
"lb": 0.45359237,
|
|
43
|
+
"oz": 0.028349523125,
|
|
44
|
+
"ton": 1000.0,
|
|
45
|
+
},
|
|
46
|
+
"time": {
|
|
47
|
+
"s": 1.0,
|
|
48
|
+
"ms": 1e-3,
|
|
49
|
+
"us": 1e-6,
|
|
50
|
+
"min": 60.0,
|
|
51
|
+
"h": 3600.0,
|
|
52
|
+
"day": 86400.0,
|
|
53
|
+
},
|
|
54
|
+
"pressure": {
|
|
55
|
+
"Pa": 1.0,
|
|
56
|
+
"kPa": 1e3,
|
|
57
|
+
"MPa": 1e6,
|
|
58
|
+
"bar": 1e5,
|
|
59
|
+
"atm": 101325.0,
|
|
60
|
+
"psi": 6894.757293168361,
|
|
61
|
+
"mmHg": 133.322387415,
|
|
62
|
+
"torr": 133.322368421,
|
|
63
|
+
},
|
|
64
|
+
"energy": {
|
|
65
|
+
"J": 1.0,
|
|
66
|
+
"kJ": 1e3,
|
|
67
|
+
"MJ": 1e6,
|
|
68
|
+
"cal": 4.184,
|
|
69
|
+
"kcal": 4184.0,
|
|
70
|
+
"BTU": 1055.05585262,
|
|
71
|
+
"kWh": 3.6e6,
|
|
72
|
+
"eV": 1.602176634e-19,
|
|
73
|
+
},
|
|
74
|
+
"power": {
|
|
75
|
+
"W": 1.0,
|
|
76
|
+
"kW": 1e3,
|
|
77
|
+
"MW": 1e6,
|
|
78
|
+
"hp": 745.6998715822702,
|
|
79
|
+
"BTU/h": 0.2930710701722222,
|
|
80
|
+
},
|
|
81
|
+
"force": {
|
|
82
|
+
"N": 1.0,
|
|
83
|
+
"kN": 1e3,
|
|
84
|
+
"lbf": 4.4482216152605,
|
|
85
|
+
"kgf": 9.80665,
|
|
86
|
+
"dyne": 1e-5,
|
|
87
|
+
},
|
|
88
|
+
"area": {
|
|
89
|
+
"m^2": 1.0,
|
|
90
|
+
"mm^2": 1e-6,
|
|
91
|
+
"cm^2": 1e-4,
|
|
92
|
+
"km^2": 1e6,
|
|
93
|
+
"in^2": 0.00064516,
|
|
94
|
+
"ft^2": 0.09290304,
|
|
95
|
+
"acre": 4046.8564224,
|
|
96
|
+
},
|
|
97
|
+
"volume": {
|
|
98
|
+
"m^3": 1.0,
|
|
99
|
+
"L": 1e-3,
|
|
100
|
+
"mL": 1e-6,
|
|
101
|
+
"cm^3": 1e-6,
|
|
102
|
+
"in^3": 1.6387064e-5,
|
|
103
|
+
"ft^3": 0.028316846592,
|
|
104
|
+
"gal_US": 3.785411784e-3,
|
|
105
|
+
},
|
|
106
|
+
"velocity": {
|
|
107
|
+
"m/s": 1.0,
|
|
108
|
+
"km/h": 1.0 / 3.6,
|
|
109
|
+
"mph": 0.44704,
|
|
110
|
+
"ft/s": 0.3048,
|
|
111
|
+
"knot": 0.5144444444444444,
|
|
112
|
+
},
|
|
113
|
+
"angle": {
|
|
114
|
+
"rad": 1.0,
|
|
115
|
+
"deg": 3.141592653589793 / 180.0,
|
|
116
|
+
"grad": 3.141592653589793 / 200.0,
|
|
117
|
+
"turn": 2.0 * 3.141592653589793,
|
|
118
|
+
},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
_TEMPERATURE_UNITS = ("K", "degC", "degF", "degR")
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# Helpers
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
def _find_category(unit: str) -> str:
|
|
129
|
+
"""Find which category a unit belongs to. Raises KeyError if not found."""
|
|
130
|
+
if unit in _TEMPERATURE_UNITS:
|
|
131
|
+
return "temperature"
|
|
132
|
+
for category, table in _LINEAR_UNITS.items():
|
|
133
|
+
if unit in table:
|
|
134
|
+
return category
|
|
135
|
+
raise KeyError(f"Unknown unit: '{unit}'.")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _to_kelvin(value: float, unit: str) -> float:
|
|
139
|
+
"""Convert a temperature value to Kelvin."""
|
|
140
|
+
if unit == "K":
|
|
141
|
+
return value
|
|
142
|
+
if unit == "degC":
|
|
143
|
+
return value + 273.15
|
|
144
|
+
if unit == "degF":
|
|
145
|
+
return (value - 32.0) * 5.0 / 9.0 + 273.15
|
|
146
|
+
if unit == "degR":
|
|
147
|
+
return value * 5.0 / 9.0
|
|
148
|
+
raise KeyError(f"Unknown temperature unit: '{unit}'.")
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _from_kelvin(value: float, unit: str) -> float:
|
|
152
|
+
"""Convert a Kelvin value to another temperature unit."""
|
|
153
|
+
if unit == "K":
|
|
154
|
+
return value
|
|
155
|
+
if unit == "degC":
|
|
156
|
+
return value - 273.15
|
|
157
|
+
if unit == "degF":
|
|
158
|
+
return (value - 273.15) * 9.0 / 5.0 + 32.0
|
|
159
|
+
if unit == "degR":
|
|
160
|
+
return value * 9.0 / 5.0
|
|
161
|
+
raise KeyError(f"Unknown temperature unit: '{unit}'.")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ---------------------------------------------------------------------------
|
|
165
|
+
# Public API
|
|
166
|
+
# ---------------------------------------------------------------------------
|
|
167
|
+
|
|
168
|
+
def convert(value: float, from_unit: str, to_unit: str) -> float:
|
|
169
|
+
"""Convert a value from one unit to another.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
value: Numeric value to convert.
|
|
173
|
+
from_unit: Source unit symbol.
|
|
174
|
+
to_unit: Target unit symbol.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Converted value.
|
|
178
|
+
|
|
179
|
+
Raises:
|
|
180
|
+
KeyError: If either unit is unknown.
|
|
181
|
+
ValueError: If the two units belong to different categories.
|
|
182
|
+
|
|
183
|
+
Example:
|
|
184
|
+
>>> convert(100, "cm", "m")
|
|
185
|
+
1.0
|
|
186
|
+
>>> convert(100, "degC", "K")
|
|
187
|
+
373.15
|
|
188
|
+
>>> convert(1, "atm", "psi")
|
|
189
|
+
14.695948775513449
|
|
190
|
+
"""
|
|
191
|
+
cat_from = _find_category(from_unit)
|
|
192
|
+
cat_to = _find_category(to_unit)
|
|
193
|
+
|
|
194
|
+
if cat_from != cat_to:
|
|
195
|
+
raise ValueError(
|
|
196
|
+
f"Cannot convert between '{from_unit}' ({cat_from}) and "
|
|
197
|
+
f"'{to_unit}' ({cat_to})."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
if cat_from == "temperature":
|
|
201
|
+
return _from_kelvin(_to_kelvin(value, from_unit), to_unit)
|
|
202
|
+
|
|
203
|
+
factor_from = _LINEAR_UNITS[cat_from][from_unit]
|
|
204
|
+
factor_to = _LINEAR_UNITS[cat_from][to_unit]
|
|
205
|
+
return value * factor_from / factor_to
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def list_categories() -> list[str]:
|
|
209
|
+
"""Return a sorted list of all supported unit categories.
|
|
210
|
+
|
|
211
|
+
Example:
|
|
212
|
+
>>> "length" in list_categories()
|
|
213
|
+
True
|
|
214
|
+
"""
|
|
215
|
+
return sorted(list(_LINEAR_UNITS.keys()) + ["temperature"])
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def list_units(category: str) -> list[str]:
|
|
219
|
+
"""Return all units in a given category.
|
|
220
|
+
|
|
221
|
+
Args:
|
|
222
|
+
category: Category name (see list_categories()).
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
List of unit symbols.
|
|
226
|
+
|
|
227
|
+
Raises:
|
|
228
|
+
KeyError: If category is unknown.
|
|
229
|
+
|
|
230
|
+
Example:
|
|
231
|
+
>>> "m" in list_units("length")
|
|
232
|
+
True
|
|
233
|
+
>>> "degC" in list_units("temperature")
|
|
234
|
+
True
|
|
235
|
+
"""
|
|
236
|
+
if category == "temperature":
|
|
237
|
+
return list(_TEMPERATURE_UNITS)
|
|
238
|
+
if category not in _LINEAR_UNITS:
|
|
239
|
+
raise KeyError(
|
|
240
|
+
f"Unknown category: '{category}'. "
|
|
241
|
+
f"Available: {list_categories()}"
|
|
242
|
+
)
|
|
243
|
+
return list(_LINEAR_UNITS[category].keys())
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def find_category(unit: str) -> str:
|
|
247
|
+
"""Return the category of a given unit.
|
|
248
|
+
|
|
249
|
+
Example:
|
|
250
|
+
>>> find_category("psi")
|
|
251
|
+
'pressure'
|
|
252
|
+
"""
|
|
253
|
+
return _find_category(unit)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: engcalc
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Mechanical Engineering calculations in Python
|
|
5
|
+
Author-email: Ankita Das <ankidas687@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: numpy>=1.24
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
13
|
+
Requires-Dist: twine>=4.0; extra == 'dev'
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# engcalc
|
|
17
|
+
|
|
18
|
+
> Mechanical Engineering calculations in Python — fast, tested, and easy to use.
|
|
19
|
+
|
|
20
|
+
[](https://www.python.org/)
|
|
21
|
+
[](https://opensource.org/licenses/MIT)
|
|
22
|
+
[](https://github.com/ankidas687/engcalc/actions/workflows/tests.yml)
|
|
23
|
+
[]()
|
|
24
|
+
|
|
25
|
+
`engcalc` is a pure-Python library for common Mechanical Engineering
|
|
26
|
+
calculations — thermodynamics, fluid mechanics, heat transfer,
|
|
27
|
+
material properties, and unit conversions. Designed for students,
|
|
28
|
+
engineers, and researchers who want clean, tested formulas without
|
|
29
|
+
the spreadsheet mess.
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
- **Thermodynamics** — ideal gas law, Carnot, Otto, Diesel, Brayton cycles
|
|
34
|
+
- **Fluid Mechanics** — Reynolds number, Bernoulli, Darcy-Weisbach, pump power
|
|
35
|
+
- **Heat Transfer** — conduction, convection, radiation, LMTD
|
|
36
|
+
- **Materials Database** — 10 common engineering materials (steel, aluminum, copper, titanium, cast iron) with 8 properties each
|
|
37
|
+
- **Unit Conversions** — 60+ units across 12 categories (length, mass, time, temperature, pressure, energy, power, force, area, volume, velocity, angle)
|
|
38
|
+
- **SI units** throughout — no unit confusion
|
|
39
|
+
- **Zero heavy dependencies** — only NumPy
|
|
40
|
+
- **Fully type-hinted** and tested (91 tests passing)
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install engcalc
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
> ⚠️ Not yet on PyPI. Install from source for now:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/ankidas687/engcalc.git
|
|
52
|
+
cd engcalc
|
|
53
|
+
pip install -e .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
from engcalc import ideal_gas, cycles, fluids, heat, materials, units
|
|
60
|
+
|
|
61
|
+
# Ideal gas law: PV = nRT
|
|
62
|
+
P = ideal_gas.pressure(n=1, T=300, V=0.024)
|
|
63
|
+
print(f"Pressure: {P:.2f} Pa") # 103930.78 Pa
|
|
64
|
+
|
|
65
|
+
# Carnot cycle efficiency
|
|
66
|
+
eta = cycles.carnot_efficiency(T_hot=800, T_cold=300)
|
|
67
|
+
print(f"Carnot efficiency: {eta * 100:.2f}%") # 62.50%
|
|
68
|
+
|
|
69
|
+
# Reynolds number
|
|
70
|
+
Re = fluids.reynolds_number(rho=1000, velocity=2, diameter=0.05, mu=0.001)
|
|
71
|
+
print(f"Re = {Re:.0f} → {fluids.flow_regime(Re)}") # 100000 → turbulent
|
|
72
|
+
|
|
73
|
+
# Heat conduction through a wall
|
|
74
|
+
Q = heat.conduction_rate(k=50, A=2, dT=100, L=0.1)
|
|
75
|
+
print(f"Heat rate: {Q:.2f} W") # 100000.00 W
|
|
76
|
+
|
|
77
|
+
# Material property lookup
|
|
78
|
+
rho = materials.get("steel_AISI_1040", "density")
|
|
79
|
+
print(f"Steel density: {rho} kg/m^3") # 7850 kg/m^3
|
|
80
|
+
|
|
81
|
+
# Unit conversion
|
|
82
|
+
temp_K = units.convert(100, "degC", "K")
|
|
83
|
+
print(f"100 °C = {temp_K:.2f} K") # 373.15 K
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Modules
|
|
87
|
+
|
|
88
|
+
| Module | Functions | Description |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `engcalc.ideal_gas` | `pressure`, `volume`, `temperature`, `moles` | Ideal gas law (PV = nRT) |
|
|
91
|
+
| `engcalc.cycles` | `carnot_efficiency`, `otto_efficiency`, `diesel_efficiency`, `brayton_efficiency` | Thermodynamic cycle efficiencies |
|
|
92
|
+
| `engcalc.fluids` | `reynolds_number`, `flow_regime`, `bernoulli_pressure`, `darcy_weisbach_head_loss`, `pump_power` | Fluid mechanics |
|
|
93
|
+
| `engcalc.heat` | `conduction_rate`, `convection_rate`, `radiation_rate`, `lmtd` | Heat transfer |
|
|
94
|
+
| `engcalc.materials` | `get`, `get_all`, `list_materials`, `search`, `list_categories` | Material property database |
|
|
95
|
+
| `engcalc.units` | `convert`, `list_categories`, `list_units`, `find_category` | Unit conversion utilities |
|
|
96
|
+
|
|
97
|
+
## Examples
|
|
98
|
+
|
|
99
|
+
Check the [`examples/`](examples/) folder for complete demo scripts:
|
|
100
|
+
|
|
101
|
+
- `examples/demo.py` — Ideal gas law
|
|
102
|
+
- `examples/cycles_demo.py` — Thermodynamic cycles
|
|
103
|
+
- `examples/fluids_demo.py` — Fluid mechanics
|
|
104
|
+
- `examples/heat_demo.py` — Heat transfer
|
|
105
|
+
- `examples/materials_demo.py` — Materials database
|
|
106
|
+
- `examples/units_demo.py` — Unit conversions
|
|
107
|
+
|
|
108
|
+
Run any demo:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
python examples/cycles_demo.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Development
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# Clone the repo
|
|
118
|
+
git clone https://github.com/ankidas687/engcalc.git
|
|
119
|
+
cd engcalc
|
|
120
|
+
|
|
121
|
+
# Create virtual environment
|
|
122
|
+
python -m venv venv
|
|
123
|
+
venv\Scripts\Activate.ps1 # Windows
|
|
124
|
+
# source venv/bin/activate # macOS/Linux
|
|
125
|
+
|
|
126
|
+
# Install with dev dependencies
|
|
127
|
+
pip install -e ".[dev]"
|
|
128
|
+
|
|
129
|
+
# Run tests
|
|
130
|
+
pytest
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Contributing
|
|
134
|
+
|
|
135
|
+
Contributions are welcome! If you find a bug or want to add a new
|
|
136
|
+
calculation, feel free to:
|
|
137
|
+
|
|
138
|
+
1. Fork the repo
|
|
139
|
+
2. Create a feature branch (`git checkout -b feature/new-formula`)
|
|
140
|
+
3. Commit your changes (`git commit -m "Add new formula"`)
|
|
141
|
+
4. Push to the branch (`git push origin feature/new-formula`)
|
|
142
|
+
5. Open a Pull Request
|
|
143
|
+
|
|
144
|
+
## Roadmap
|
|
145
|
+
|
|
146
|
+
- [x] Thermodynamics (ideal gas, cycles)
|
|
147
|
+
- [x] Fluid mechanics
|
|
148
|
+
- [x] Heat transfer
|
|
149
|
+
- [x] Materials database
|
|
150
|
+
- [x] Unit conversion utilities
|
|
151
|
+
- [ ] Mechanics of materials (stress, strain, beam)
|
|
152
|
+
- [ ] Publish to PyPI
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
This project is licensed under the MIT License — see the [LICENSE](LICENSE) file for details.
|
|
157
|
+
|
|
158
|
+
## Author
|
|
159
|
+
|
|
160
|
+
**Ankita Das** — [@ankidas687](https://github.com/ankidas687)
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
⭐ If you find this useful, consider giving it a star!
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
engcalc/__init__.py,sha256=4gpIB_W_693axfbDC9LMDHrlsce4EWxhDeQON4Cj4DQ,89
|
|
2
|
+
engcalc/cycles.py,sha256=V2c_LOjNKnHlqz57nfz9kWlYC_peSZKg6RCKMMWzYgY,4145
|
|
3
|
+
engcalc/fluids.py,sha256=pdL-tBQKmxHXIERiwOXvxlWwM9qN1I-T_FbZV-zcNbE,4590
|
|
4
|
+
engcalc/heat.py,sha256=TFTV-6c8AQwW1Pm8tbX7P-OgzRnw4LtYykAn46A9sKw,3967
|
|
5
|
+
engcalc/ideal_gas.py,sha256=i3x5Sq9BxADCVHbz-XMAmDF2-D4oGCFDHSCwv5x7HCI,878
|
|
6
|
+
engcalc/materials.py,sha256=qTo02wPkqKkZJOuk2ZvE0MbhFnC02aaxDnKvOTdgkVE,3456
|
|
7
|
+
engcalc/units.py,sha256=V1PNc01UdjiiaBrkayPY24CN29xExhb5pRSaHMUMhEc,6806
|
|
8
|
+
engcalc/data/materials.json,sha256=VXMnFT5VrgE2yRdOTZ1mUvq_M2xnaxYi4OUKdrrCUiE,3316
|
|
9
|
+
engcalc-0.1.0.dist-info/METADATA,sha256=LV3pBb4YTwBZWfvZNBTskYRqgfph6MDSFAdpUviY93E,5356
|
|
10
|
+
engcalc-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
11
|
+
engcalc-0.1.0.dist-info/licenses/LICENSE,sha256=U49qDhKdNv6Acfgh5VY-t7aqwT-r1t6wIJ8JQN5In1o,1086
|
|
12
|
+
engcalc-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ankita Das
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|