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 ADDED
@@ -0,0 +1,3 @@
1
+ """engcalc — Mechanical Engineering calculations in Python."""
2
+
3
+ __version__ = "0.1.0"
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
+ [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://www.python.org/)
21
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
22
+ [![Tests](https://github.com/ankidas687/engcalc/actions/workflows/tests.yml/badge.svg)](https://github.com/ankidas687/engcalc/actions/workflows/tests.yml)
23
+ [![PyPI](https://img.shields.io/badge/PyPI-coming%20soon-orange)]()
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,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.