oak-understory-shared-engineering-bench 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- oak_understory_shared_engineering_bench-0.1.0/.gitignore +23 -0
- oak_understory_shared_engineering_bench-0.1.0/Makefile +11 -0
- oak_understory_shared_engineering_bench-0.1.0/PKG-INFO +10 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/__init__.py +25 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/buckling.py +87 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/dimensionless.py +170 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/flow.py +82 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/mechanics.py +156 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/sections.py +173 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/signals.py +79 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/thermal.py +114 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/units.py +277 -0
- oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/universals.py +231 -0
- oak_understory_shared_engineering_bench-0.1.0/pyproject.toml +30 -0
- oak_understory_shared_engineering_bench-0.1.0/tests/test_units.py +155 -0
- oak_understory_shared_engineering_bench-0.1.0/tests/test_universals.py +94 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
.venv/
|
|
5
|
+
.eggs/
|
|
6
|
+
*.egg-info/
|
|
7
|
+
build/
|
|
8
|
+
dist/
|
|
9
|
+
|
|
10
|
+
# uv
|
|
11
|
+
.uv/
|
|
12
|
+
|
|
13
|
+
# OS / editor
|
|
14
|
+
.DS_Store
|
|
15
|
+
|
|
16
|
+
# Backtest working set: the fetched corpus + the ONNX embedding model.
|
|
17
|
+
# Hundreds of MB, reproducible from the pinned snapshot and a bounded
|
|
18
|
+
# date, and therefore fetched rather than carried.
|
|
19
|
+
.cache/
|
|
20
|
+
|
|
21
|
+
# Builder staging (should never appear here, but just in case)
|
|
22
|
+
_staging/
|
|
23
|
+
docs/*.print.html
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# GENERATED — source: manifest.toml
|
|
2
|
+
# DO NOT EDIT BY HAND. Regenerated on every `sync`; edits are lost.
|
|
3
|
+
# To change it, edit manifest.toml (or the template) and re-run sync.
|
|
4
|
+
# A harness library's developer entrypoints. `make check` is what CI runs.
|
|
5
|
+
|
|
6
|
+
.PHONY: test check
|
|
7
|
+
|
|
8
|
+
test:
|
|
9
|
+
python -m pytest -q
|
|
10
|
+
|
|
11
|
+
check: test
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: oak-understory-shared-engineering-bench
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: an Understory commons: shared discipline assets, no seat.
|
|
5
|
+
Requires-Python: >=3.13
|
|
6
|
+
Requires-Dist: cambium-ai<0.4,>=0.3
|
|
7
|
+
Requires-Dist: oak-understory-common<3,>=2
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
10
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/__init__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""The engineering commons — a shared bench, and deliberately no vocabulary.
|
|
2
|
+
|
|
3
|
+
⭐ A commons carries METHODS and NEVER A STOPLIST, and `commons_has_no_stoplist`
|
|
4
|
+
refuses one at scaffold time. That asymmetry is the design: a Reynolds number is the
|
|
5
|
+
same number whichever engineering discipline computes it, so five faculties each
|
|
6
|
+
carrying a copy would be five places for it to drift — but a shared VOCABULARY would
|
|
7
|
+
make every cross-domain match between two of those faculties a topic match, which is
|
|
8
|
+
the one failure the whole thesis rests on avoiding.
|
|
9
|
+
|
|
10
|
+
Five exact Tier-1 calculations, each conferring affordances selected by a computed
|
|
11
|
+
value rather than by applicability:
|
|
12
|
+
|
|
13
|
+
flow laminar preserves, turbulent combines; the transition band confers
|
|
14
|
+
nothing, being unstable rather than intermediate
|
|
15
|
+
mechanics within the elastic limit a member recovers; past it, it degrades
|
|
16
|
+
buckling slenderness limits, material strength bounds
|
|
17
|
+
thermal a stack holds heat or transfers it
|
|
18
|
+
signals above Nyquist reconstructs; below, it degrades irreversibly
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from oak_understory_shared_engineering_bench import ( # noqa: F401
|
|
22
|
+
buckling, flow, mechanics, signals, thermal,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = ["buckling", "flow", "mechanics", "signals", "thermal"]
|
oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/buckling.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Column stability — the Euler critical load (Tier 1).
|
|
2
|
+
|
|
3
|
+
P_cr = pi^2 E I / (K L)^2
|
|
4
|
+
|
|
5
|
+
⭐ The calculation that decides which failure mode you are designing against. A
|
|
6
|
+
short column crushes and a slender one buckles, and buckling arrives without
|
|
7
|
+
warning at a load the material's strength says is perfectly safe. Comparing the two
|
|
8
|
+
is the whole point: a strength check alone is how a slender strut passes on paper
|
|
9
|
+
and folds in service.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from oak_understory_common.method_doc import MethodDoc
|
|
15
|
+
|
|
16
|
+
METHOD_DOC = MethodDoc(
|
|
17
|
+
title="Will this column crush or fold?",
|
|
18
|
+
what="Works out the load at which a slender column suddenly bows sideways, and "
|
|
19
|
+
"says whether that happens before or after the material would be crushed.",
|
|
20
|
+
inputs="Elastic modulus in pascals, second moment of area in metres to the "
|
|
21
|
+
"fourth, unsupported length in metres, an end-fixity factor, the "
|
|
22
|
+
"cross-sectional area in square metres, and the yield strength in pascals.",
|
|
23
|
+
outputs="Critical buckling load in newtons, and the squash load in newtons. "
|
|
24
|
+
"Both are forces; the smaller one is what governs.",
|
|
25
|
+
limits="⭐ Euler is an IDEAL: perfectly straight, perfectly centred, perfectly "
|
|
26
|
+
"elastic. Real columns have an initial bow and an eccentric load, so they "
|
|
27
|
+
"fail BELOW this figure — the number is an upper bound, not a rating, and "
|
|
28
|
+
"using it as a rating is how a strut is undersized. It is invalid for stocky "
|
|
29
|
+
"columns, where the material yields before the elastic formula applies and "
|
|
30
|
+
"the result is meaningless rather than merely optimistic. The end-fixity "
|
|
31
|
+
"factor assumes an idealised support; a real joint is rarely either pinned "
|
|
32
|
+
"or truly fixed, and K is where most of the error enters. Nothing here "
|
|
33
|
+
"covers local buckling of thin walls, torsional modes, or eccentric loading.",
|
|
34
|
+
tier=1,
|
|
35
|
+
when_to_use="Whenever a member carries compression and is not obviously stocky.",
|
|
36
|
+
references=(
|
|
37
|
+
"Euler L, Methodus inveniendi lineas curvas, 1744.",
|
|
38
|
+
"Timoshenko SP & Gere JM, Theory of Elastic Stability, 2nd ed.",
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
import math
|
|
43
|
+
|
|
44
|
+
#: Standard end-fixity factors. The field's, and named so a caller does not
|
|
45
|
+
#: silently pass 1.0 for a cantilever, which understates the length by half.
|
|
46
|
+
K_PINNED_PINNED = 1.0
|
|
47
|
+
K_FIXED_FIXED = 0.5
|
|
48
|
+
K_FIXED_PINNED = 0.699
|
|
49
|
+
K_FIXED_FREE = 2.0
|
|
50
|
+
|
|
51
|
+
METHOD_ID = "engineering.buckling.euler_critical_load"
|
|
52
|
+
VERSION = "0.1.0"
|
|
53
|
+
|
|
54
|
+
AFFORDANCES: tuple[str, ...] = ("constrain", "bound")
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def critical_load_n(modulus_pa: float, second_moment_m4: float,
|
|
58
|
+
length_m: float, end_fixity: float = K_PINNED_PINNED) -> float:
|
|
59
|
+
"""``pi^2 E I / (K L)^2`` in newtons."""
|
|
60
|
+
if min(modulus_pa, second_moment_m4, length_m, end_fixity) <= 0:
|
|
61
|
+
raise ValueError("modulus, second moment, length and fixity must be positive")
|
|
62
|
+
return math.pi ** 2 * modulus_pa * second_moment_m4 / (end_fixity * length_m) ** 2
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def squash_load_n(area_m2: float, yield_pa: float) -> float:
|
|
66
|
+
"""The load at which the material simply crushes: ``A x sigma_y``."""
|
|
67
|
+
if min(area_m2, yield_pa) <= 0:
|
|
68
|
+
raise ValueError("area and yield strength must be positive")
|
|
69
|
+
return area_m2 * yield_pa
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def affordances(critical_n: float, squash_n: float) -> tuple[str, ...]:
|
|
73
|
+
"""What the member lets you do, selected by WHICH failure arrives first.
|
|
74
|
+
|
|
75
|
+
``constrain`` — buckling governs: the usable load is capped well below the
|
|
76
|
+
material's strength, and adding stronger material buys nothing. ⭐ Named
|
|
77
|
+
`constrain` and not `limit`: `electrical` claims `limit`, and RCGS renames the
|
|
78
|
+
capability rather than narrowing a stoplist.
|
|
79
|
+
``bound`` — the material governs: strength sets the ceiling, which is the
|
|
80
|
+
ordinary case a strength calculation already describes.
|
|
81
|
+
"""
|
|
82
|
+
return ("constrain",) if critical_n < squash_n else ("bound",)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ["AFFORDANCES", "K_FIXED_FIXED", "K_FIXED_FREE", "K_FIXED_PINNED",
|
|
86
|
+
"K_PINNED_PINNED", "METHOD_ID", "VERSION", "affordances",
|
|
87
|
+
"critical_load_n", "squash_load_n"]
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"""⭐ DIMENSIONLESS GROUPS — the one method that is domain-free by construction.
|
|
2
|
+
|
|
3
|
+
Every other method in this institution answers one question in one discipline. This
|
|
4
|
+
one does something none of them do, and it is the reason it was built.
|
|
5
|
+
|
|
6
|
+
## ⭐ Why this is a RETRIEVAL key and not just another rule
|
|
7
|
+
|
|
8
|
+
`risks-and-falsifiers.md` §2f measured seven method libraries closing an affordance
|
|
9
|
+
vocabulary and **not one of them making it selective**: five to nine terms over
|
|
10
|
+
hundreds of items, carrying two or three bits between them. The affordance TERM is
|
|
11
|
+
not the retrieval unit.
|
|
12
|
+
|
|
13
|
+
A dimensionless group is. **Re = 270 000 in a chiller pipe is the same structure as
|
|
14
|
+
Re = 270 000 in a blood vessel or a wind tunnel** — not by analogy but by
|
|
15
|
+
equivalence, because the governing equations are identical once non-dimensionalised.
|
|
16
|
+
Two systems with matching groups are *dynamically similar*, and that is the whole
|
|
17
|
+
content of transposition, computed exactly rather than argued.
|
|
18
|
+
|
|
19
|
+
That is why aircraft are tested as scale models and why a heat exchanger correlation
|
|
20
|
+
from 1930 still works on a fluid nobody had then. The group is the invariant; the
|
|
21
|
+
domain is what it was measured in.
|
|
22
|
+
|
|
23
|
+
## What this does and does not claim
|
|
24
|
+
|
|
25
|
+
It computes named groups from whatever quantities a caller supplies and reports which
|
|
26
|
+
groups two systems share. ⚠️ **Matching groups mean the two systems obey the same
|
|
27
|
+
equations — NOT that a solution transfers.** Boundary conditions, geometry and the
|
|
28
|
+
groups nobody thought to compute all still differ, and similitude that ignores an
|
|
29
|
+
unmatched group is how a scale model gives a confidently wrong answer.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
from __future__ import annotations
|
|
33
|
+
|
|
34
|
+
import math
|
|
35
|
+
|
|
36
|
+
from oak_understory_common.method_doc import MethodDoc
|
|
37
|
+
|
|
38
|
+
METHOD_DOC = MethodDoc(
|
|
39
|
+
title="Which dimensionless groups two systems share",
|
|
40
|
+
what="Reduces a set of physical quantities to the dimensionless groups that "
|
|
41
|
+
"govern them, and says which groups two different systems have in common.",
|
|
42
|
+
inputs="A mapping of quantity name to value for each system — density, "
|
|
43
|
+
"velocity, a length, viscosity, a diffusivity, and so on. Missing "
|
|
44
|
+
"quantities simply mean the groups needing them are not computed.",
|
|
45
|
+
outputs="Each group it could form, as a DIMENSIONLESS number, plus the groups "
|
|
46
|
+
"two systems match on and the relative difference on each. A ratio has no "
|
|
47
|
+
"unit and comparing it across domains is the entire point.",
|
|
48
|
+
limits="⭐ Matching groups mean two systems obey the same EQUATIONS. They do not "
|
|
49
|
+
"mean a solution transfers: boundary conditions, geometry and any group "
|
|
50
|
+
"nobody computed still differ, and similitude that ignores an unmatched "
|
|
51
|
+
"group is how a scale model gives a confidently wrong answer. It forms only "
|
|
52
|
+
"the groups it holds definitions for — an unnamed group governing the "
|
|
53
|
+
"problem is invisible here, and its absence is silent. It cannot tell "
|
|
54
|
+
"whether the quantities it was handed are the RIGHT ones: a caller who omits "
|
|
55
|
+
"the governing length scale gets a clean, wrong Reynolds number. And two "
|
|
56
|
+
"systems matching on one group out of five are not similar; the count "
|
|
57
|
+
"matters more than any single match.",
|
|
58
|
+
tier=1,
|
|
59
|
+
when_to_use="Before assuming a result from one domain applies in another, and to "
|
|
60
|
+
"find domains where a problem has already been solved.",
|
|
61
|
+
references=(
|
|
62
|
+
"Buckingham E, On physically similar systems, Phys. Rev. 1914.",
|
|
63
|
+
"Barenblatt GI, Scaling, Self-similarity, and Intermediate Asymptotics.",
|
|
64
|
+
),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
METHOD_ID = "engineering.dimensionless.groups"
|
|
68
|
+
VERSION = "0.1.0"
|
|
69
|
+
|
|
70
|
+
#: ⭐ Selected by the computed match, never by identity. `compare` when two systems
|
|
71
|
+
#: share enough groups to be worth reading across; `distinguish` when they do not.
|
|
72
|
+
AFFORDANCES: tuple[str, ...] = ("compare", "distinguish")
|
|
73
|
+
|
|
74
|
+
#: Standard gravity, m/s².
|
|
75
|
+
GRAVITY = 9.80665
|
|
76
|
+
|
|
77
|
+
#: How close two groups must be to count as matched, as a relative difference.
|
|
78
|
+
#: 10% because similitude is judged on order rather than on decimals — a scale
|
|
79
|
+
#: model matched to 1% on Reynolds is a rare luxury, and 10% is the working line.
|
|
80
|
+
MATCH_TOLERANCE = 0.10
|
|
81
|
+
|
|
82
|
+
#: Two systems sharing fewer than this many groups are not similar in any useful
|
|
83
|
+
#: sense. One shared group out of five is a coincidence with a name.
|
|
84
|
+
MIN_SHARED = 2
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _q(system, *names):
|
|
88
|
+
"""The quantities, or None if any is missing. Absence is not zero."""
|
|
89
|
+
out = []
|
|
90
|
+
for name in names:
|
|
91
|
+
value = system.get(name)
|
|
92
|
+
if value is None:
|
|
93
|
+
return None
|
|
94
|
+
out.append(float(value))
|
|
95
|
+
return out
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def groups(system: dict) -> dict[str, float]:
|
|
99
|
+
"""Every named group this module can form from what it was given.
|
|
100
|
+
|
|
101
|
+
⭐ A group whose quantities are absent is OMITTED, never defaulted. A Reynolds
|
|
102
|
+
number computed with an assumed viscosity is a clean number about a system
|
|
103
|
+
nobody described.
|
|
104
|
+
"""
|
|
105
|
+
out: dict[str, float] = {}
|
|
106
|
+
|
|
107
|
+
if (v := _q(system, "density", "velocity", "length", "viscosity")) and v[3] > 0:
|
|
108
|
+
out["reynolds"] = v[0] * v[1] * v[2] / v[3]
|
|
109
|
+
if (v := _q(system, "velocity", "length")) and v[1] > 0:
|
|
110
|
+
out["froude"] = v[0] / math.sqrt(GRAVITY * v[1])
|
|
111
|
+
if (v := _q(system, "velocity", "sound_speed")) and v[1] > 0:
|
|
112
|
+
out["mach"] = v[0] / v[1]
|
|
113
|
+
if (v := _q(system, "frequency", "length", "velocity")) and v[2] > 0:
|
|
114
|
+
out["strouhal"] = v[0] * v[1] / v[2]
|
|
115
|
+
if (v := _q(system, "heat_transfer_coefficient", "length", "conductivity")) and v[2] > 0:
|
|
116
|
+
out["biot"] = v[0] * v[1] / v[2]
|
|
117
|
+
if (v := _q(system, "velocity", "length", "diffusivity")) and v[2] > 0:
|
|
118
|
+
out["peclet"] = v[0] * v[1] / v[2]
|
|
119
|
+
if (v := _q(system, "viscosity", "density", "diffusivity")) and v[1] > 0 and v[2] > 0:
|
|
120
|
+
out["prandtl"] = (v[0] / v[1]) / v[2]
|
|
121
|
+
if (v := _q(system, "density", "velocity", "length", "surface_tension")) and v[3] > 0:
|
|
122
|
+
out["weber"] = v[0] * v[1] ** 2 * v[2] / v[3]
|
|
123
|
+
if (v := _q(system, "reaction_rate", "length", "velocity")) and v[2] > 0:
|
|
124
|
+
out["damkohler"] = v[0] * v[1] / v[2]
|
|
125
|
+
return out
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def shared(a: dict, b: dict, *, tolerance: float = MATCH_TOLERANCE) -> dict[str, float]:
|
|
129
|
+
"""Groups both systems form, and the relative difference on each.
|
|
130
|
+
|
|
131
|
+
Only groups BOTH systems could form are considered. A group one system lacks is
|
|
132
|
+
not a mismatch — it is an unknown, and reporting it as a difference would make
|
|
133
|
+
an incomplete description look like a dissimilar system.
|
|
134
|
+
"""
|
|
135
|
+
ga, gb = groups(a), groups(b)
|
|
136
|
+
out: dict[str, float] = {}
|
|
137
|
+
for name in sorted(set(ga) & set(gb)):
|
|
138
|
+
scale = max(abs(ga[name]), abs(gb[name]))
|
|
139
|
+
if scale == 0:
|
|
140
|
+
out[name] = 0.0
|
|
141
|
+
continue
|
|
142
|
+
out[name] = abs(ga[name] - gb[name]) / scale
|
|
143
|
+
return out
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def matched(a: dict, b: dict, *, tolerance: float = MATCH_TOLERANCE) -> tuple[str, ...]:
|
|
147
|
+
"""The groups on which two systems agree within ``tolerance``."""
|
|
148
|
+
return tuple(name for name, diff in shared(a, b, tolerance=tolerance).items()
|
|
149
|
+
if diff <= tolerance)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def affordances(matches: tuple[str, ...], comparable: int) -> tuple[str, ...]:
|
|
153
|
+
"""What the comparison lets you do, selected by how many groups matched.
|
|
154
|
+
|
|
155
|
+
``compare`` — enough shared groups agree that the two systems obey the same
|
|
156
|
+
equations and a result from one is worth reading across.
|
|
157
|
+
``distinguish`` — they do not: whatever the systems have in common, it is not
|
|
158
|
+
the physics, and a transposition between them would be a metaphor.
|
|
159
|
+
|
|
160
|
+
⭐ It confers NOTHING when the two systems share too few computable groups to
|
|
161
|
+
judge. That is an unknown, not a negative, and the third outcome exists for the
|
|
162
|
+
same reason the impasse detector's does.
|
|
163
|
+
"""
|
|
164
|
+
if comparable < MIN_SHARED:
|
|
165
|
+
return ()
|
|
166
|
+
return ("compare",) if len(matches) >= MIN_SHARED else ("distinguish",)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
__all__ = ["AFFORDANCES", "GRAVITY", "MATCH_TOLERANCE", "METHOD_DOC", "METHOD_ID",
|
|
170
|
+
"MIN_SHARED", "VERSION", "affordances", "groups", "matched", "shared"]
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Flow regime — the Reynolds number (Tier 1).
|
|
2
|
+
|
|
3
|
+
Re = rho x v x D / mu
|
|
4
|
+
|
|
5
|
+
Dimensionless, exact, and the calculation fluid engineering is organised around: it
|
|
6
|
+
decides whether a flow is orderly enough to predict from first principles or has to
|
|
7
|
+
be treated statistically. Nothing else about a pipe changes that answer.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from oak_understory_common.method_doc import MethodDoc
|
|
13
|
+
|
|
14
|
+
METHOD_DOC = MethodDoc(
|
|
15
|
+
title="Is this flow orderly or churning?",
|
|
16
|
+
what="Works out whether a fluid moving through a pipe travels in smooth layers "
|
|
17
|
+
"or breaks up into churning eddies.",
|
|
18
|
+
inputs="Fluid density in kilograms per cubic metre, average speed in metres per "
|
|
19
|
+
"second, the pipe's internal diameter in metres, and dynamic viscosity in "
|
|
20
|
+
"pascal-seconds.",
|
|
21
|
+
outputs="The Reynolds number — a pure ratio with NO units. It is not a speed, "
|
|
22
|
+
"not a pressure, and a bigger number is not a faster flow.",
|
|
23
|
+
limits="⭐ The 2300 and 4000 boundaries are for STEADY flow in a straight, "
|
|
24
|
+
"smooth, circular pipe, and every one of those words is load-bearing: a "
|
|
25
|
+
"bend, a rough wall, a sudden expansion or a pulsating supply all trip "
|
|
26
|
+
"turbulence early, and a carefully isolated laboratory pipe can stay "
|
|
27
|
+
"laminar into the tens of thousands. Between 2300 and 4000 the flow is "
|
|
28
|
+
"genuinely indeterminate — it is not 'mildly turbulent', it is unstable and "
|
|
29
|
+
"may be either — so this method confers nothing there rather than guessing. "
|
|
30
|
+
"It assumes a Newtonian fluid, so it is wrong for blood, slurries and "
|
|
31
|
+
"polymer melts, whose viscosity depends on how hard they are sheared.",
|
|
32
|
+
tier=1,
|
|
33
|
+
when_to_use="Before applying any pressure-drop or heat-transfer correlation, "
|
|
34
|
+
"because almost all of them are valid in one regime only.",
|
|
35
|
+
references=(
|
|
36
|
+
"Reynolds O, An experimental investigation of the circumstances which "
|
|
37
|
+
"determine whether the motion of water shall be direct or sinuous, 1883.",
|
|
38
|
+
"White FM, Fluid Mechanics, 7th ed.",
|
|
39
|
+
),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
#: The classical transition band for pipe flow. The field's, not derived here.
|
|
43
|
+
LAMINAR_BELOW = 2300.0
|
|
44
|
+
TURBULENT_ABOVE = 4000.0
|
|
45
|
+
|
|
46
|
+
METHOD_ID = "engineering.flow.reynolds_number"
|
|
47
|
+
VERSION = "0.1.0"
|
|
48
|
+
|
|
49
|
+
AFFORDANCES: tuple[str, ...] = ("preserve", "combine")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def reynolds_number(density_kg_m3: float, velocity_m_s: float,
|
|
53
|
+
diameter_m: float, viscosity_pa_s: float) -> float:
|
|
54
|
+
"""``rho v D / mu``. Dimensionless and exact given the inputs."""
|
|
55
|
+
if min(density_kg_m3, diameter_m, viscosity_pa_s) <= 0:
|
|
56
|
+
raise ValueError("density, diameter and viscosity must be positive")
|
|
57
|
+
if velocity_m_s < 0:
|
|
58
|
+
raise ValueError("velocity may not be negative")
|
|
59
|
+
return density_kg_m3 * velocity_m_s * diameter_m / viscosity_pa_s
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def affordances(reynolds: float) -> tuple[str, ...]:
|
|
63
|
+
"""What a flow regime lets you do, selected by the computed number.
|
|
64
|
+
|
|
65
|
+
``preserve`` — laminar: layers stay separate, so anything carried in one stays
|
|
66
|
+
there and the profile is predictable in closed form.
|
|
67
|
+
``combine`` — turbulent: eddies mix across the whole cross-section, which is
|
|
68
|
+
what you want for mixing and heat transfer and cannot have in laminar flow.
|
|
69
|
+
|
|
70
|
+
Between the two boundaries it confers NOTHING. The transition band is unstable
|
|
71
|
+
rather than intermediate, and a method that split the difference there would be
|
|
72
|
+
asserting a regime the physics does not have.
|
|
73
|
+
"""
|
|
74
|
+
if reynolds < LAMINAR_BELOW:
|
|
75
|
+
return ("preserve",)
|
|
76
|
+
if reynolds > TURBULENT_ABOVE:
|
|
77
|
+
return ("combine",)
|
|
78
|
+
return ()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
__all__ = ["AFFORDANCES", "LAMINAR_BELOW", "METHOD_ID", "TURBULENT_ABOVE", "VERSION",
|
|
82
|
+
"affordances", "reynolds_number"]
|
oak_understory_shared_engineering_bench-0.1.0/oak_understory_shared_engineering_bench/mechanics.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Elastic response — deflection and the yield margin, for a NAMED arrangement (Tier 1).
|
|
2
|
+
|
|
3
|
+
delta = a F L^3 / (E I) a from the support condition
|
|
4
|
+
sigma = M c / I, M = b F L b from the support condition
|
|
5
|
+
|
|
6
|
+
Exact within the elastic regime, which is precisely the thing the second calculation
|
|
7
|
+
is for: it says whether the first one is still allowed to be believed.
|
|
8
|
+
|
|
9
|
+
## ⭐ Why the support condition is now an argument and not an assumption
|
|
10
|
+
|
|
11
|
+
Until 2026-08-03 this module computed **one** arrangement — a cantilever with a load
|
|
12
|
+
at the free end — behind a decision named `beam-loading`, and exposed no way to say
|
|
13
|
+
so. §3e measured what that cost: the chiller briefing's headline finding, *"423% of
|
|
14
|
+
yield; PAST YIELD"*, reads **52.8% of yield and confers `restore`** for the same total
|
|
15
|
+
load carried as a distributed load on a simple span. **Not a caveat on a failure: a
|
|
16
|
+
pass.** The method's own `limits` had named the gap — *"other supports … are all
|
|
17
|
+
outside it"* — and the protocol gave nobody a way to close it.
|
|
18
|
+
|
|
19
|
+
So the arrangement is a **reading**: a required, declared argument, refused rather
|
|
20
|
+
than defaulted. `M = F L` and `M = F L / 8` are both right, and which one you meant is
|
|
21
|
+
not a detail the library may choose on your behalf.
|
|
22
|
+
|
|
23
|
+
⭐ The table below is enumerated, which this codebase normally refuses. It is the
|
|
24
|
+
legitimate exception, and the same one `units.py` makes for the SI base symbols:
|
|
25
|
+
these are the standard tabulated cases of the discipline, fixed by Roark rather than
|
|
26
|
+
by judgement, and **an unrecognised name raises instead of falling back.**
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
from __future__ import annotations
|
|
30
|
+
|
|
31
|
+
from oak_understory_common.method_doc import MethodDoc
|
|
32
|
+
|
|
33
|
+
METHOD_DOC = MethodDoc(
|
|
34
|
+
title="How far it bends, and whether it springs back",
|
|
35
|
+
what="Works out how far a loaded beam bends, and whether the load leaves it "
|
|
36
|
+
"permanently deformed or lets it return to shape.",
|
|
37
|
+
inputs="Load in newtons, length in metres, the material's elastic modulus in "
|
|
38
|
+
"pascals, the section's second moment of area in metres to the fourth, the "
|
|
39
|
+
"distance from the neutral axis to the outer fibre in metres, and the "
|
|
40
|
+
"material's yield strength in pascals — and the SUPPORT CONDITION, which is "
|
|
41
|
+
"required and has no default.",
|
|
42
|
+
outputs="Deflection in metres and bending stress in pascals. A deflection is a "
|
|
43
|
+
"distance, not a fraction of the length.",
|
|
44
|
+
limits="⭐ Both formulas are ELASTIC and small-deflection: they assume the beam "
|
|
45
|
+
"returns to shape and that it has barely moved. Past yield the first is "
|
|
46
|
+
"simply wrong — it keeps returning a tidy number for a beam that is "
|
|
47
|
+
"bending permanently — which is why the yield margin is computed alongside "
|
|
48
|
+
"rather than left to the caller. ⭐ It covers the five STANDARD support "
|
|
49
|
+
"conditions and no others: a partially fixed end, a continuous beam over "
|
|
50
|
+
"several supports, an overhang, a non-central point load and any combined "
|
|
51
|
+
"loading are all outside it, and the answers differ by factors of several "
|
|
52
|
+
"between arrangements — which is why the support condition is a required "
|
|
53
|
+
"argument here and not a default. Buckling, fatigue under repeated loading "
|
|
54
|
+
"and dynamic amplification are outside it too, and fatigue in particular "
|
|
55
|
+
"breaks things that never come close to yield in a single application.",
|
|
56
|
+
tier=1,
|
|
57
|
+
when_to_use="For a first-pass sizing check, and to know whether a more careful "
|
|
58
|
+
"analysis is needed because yield is close.",
|
|
59
|
+
references=(
|
|
60
|
+
"Gere JM & Goodno BJ, Mechanics of Materials, 8th ed.",
|
|
61
|
+
"Roark's Formulas for Stress and Strain, 8th ed., Table 8.1.",
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
#: Utilisation above which the elastic assumption is no longer safe to rely on. A
|
|
66
|
+
#: conventional design margin, not derived from anything measured here.
|
|
67
|
+
YIELD_MARGIN = 1.0
|
|
68
|
+
|
|
69
|
+
#: ⭐ The standard tabulated cases: (deflection coefficient a, moment coefficient b),
|
|
70
|
+
#: so ``delta = a F L^3 / (E I)`` and ``M = b F L``. For the distributed cases F is
|
|
71
|
+
#: the TOTAL load on the span, which is what makes the arrangements comparable at
|
|
72
|
+
#: equal load — the comparison §3e turned on.
|
|
73
|
+
#:
|
|
74
|
+
#: ⚠️ Enumerated deliberately, and an unknown name RAISES. See the module docstring.
|
|
75
|
+
SUPPORTS: dict[str, tuple[float, float]] = {
|
|
76
|
+
"cantilever-tip-point": (1.0 / 3.0, 1.0),
|
|
77
|
+
"simply-supported-central-point": (1.0 / 48.0, 1.0 / 4.0),
|
|
78
|
+
"simply-supported-distributed": (5.0 / 384.0, 1.0 / 8.0),
|
|
79
|
+
"fixed-fixed-central-point": (1.0 / 192.0, 1.0 / 8.0),
|
|
80
|
+
"fixed-fixed-distributed": (1.0 / 384.0, 1.0 / 12.0),
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#: ⚠️ The id is KEPT although the module now covers five arrangements. A method id is
|
|
84
|
+
#: a published name that experiments, results JSONs and tests bind to by string, and
|
|
85
|
+
#: the standing rule is EXTEND, NEVER RENAME. The VERSION carries the change instead.
|
|
86
|
+
METHOD_ID = "engineering.mechanics.cantilever"
|
|
87
|
+
VERSION = "0.2.0"
|
|
88
|
+
|
|
89
|
+
AFFORDANCES: tuple[str, ...] = ("restore", "degrade")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def coefficients(support: str) -> tuple[float, float]:
|
|
93
|
+
"""``(a, b)`` for a named arrangement. ⭐ Raises on anything unrecognised.
|
|
94
|
+
|
|
95
|
+
Falling back to a default here would reinstate the exact defect this argument
|
|
96
|
+
was added to remove, and it would do it invisibly.
|
|
97
|
+
"""
|
|
98
|
+
try:
|
|
99
|
+
return SUPPORTS[support]
|
|
100
|
+
except (KeyError, TypeError):
|
|
101
|
+
raise ValueError(
|
|
102
|
+
f"{support!r} is not one of the standard arrangements "
|
|
103
|
+
f"{sorted(SUPPORTS)}. There is no default: which arrangement the number "
|
|
104
|
+
f"describes changes it by a factor of several, and guessing on the "
|
|
105
|
+
f"caller's behalf is how a pass was reported as a failure (§3e)."
|
|
106
|
+
) from None
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def max_deflection_m(load_n: float, length_m: float, modulus_pa: float,
|
|
110
|
+
second_moment_m4: float, support: str) -> float:
|
|
111
|
+
"""``a F L³ / (E I)`` in metres, for the named arrangement."""
|
|
112
|
+
if min(length_m, modulus_pa, second_moment_m4) <= 0:
|
|
113
|
+
raise ValueError("length, modulus and second moment must be positive")
|
|
114
|
+
a, _ = coefficients(support)
|
|
115
|
+
return a * load_n * length_m ** 3 / (modulus_pa * second_moment_m4)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def bending_stress_pa(load_n: float, length_m: float, fibre_distance_m: float,
|
|
119
|
+
second_moment_m4: float, support: str) -> float:
|
|
120
|
+
"""``M c / I`` at the governing section, where ``M = b F L``."""
|
|
121
|
+
if min(length_m, fibre_distance_m, second_moment_m4) <= 0:
|
|
122
|
+
raise ValueError("length, fibre distance and second moment must be positive")
|
|
123
|
+
_, b = coefficients(support)
|
|
124
|
+
return b * load_n * length_m * fibre_distance_m / second_moment_m4
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def tip_deflection_m(load_n: float, length_m: float, modulus_pa: float,
|
|
128
|
+
second_moment_m4: float) -> float:
|
|
129
|
+
"""⚠️ LEGACY — the cantilever-only deflection, kept because it is a published
|
|
130
|
+
name. New callers use :func:`max_deflection_m` with an explicit arrangement."""
|
|
131
|
+
return max_deflection_m(load_n, length_m, modulus_pa, second_moment_m4,
|
|
132
|
+
"cantilever-tip-point")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def utilisation(stress_pa: float, yield_pa: float) -> float:
|
|
136
|
+
"""Stress over yield strength. 1.0 is the elastic limit exactly."""
|
|
137
|
+
if yield_pa <= 0:
|
|
138
|
+
raise ValueError("yield strength must be positive")
|
|
139
|
+
return stress_pa / yield_pa
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def affordances(stress_pa: float, yield_pa: float) -> tuple[str, ...]:
|
|
143
|
+
"""What the loaded member lets you do, selected by the computed utilisation.
|
|
144
|
+
|
|
145
|
+
``restore`` — within the elastic limit, so the deflection is reversible and the
|
|
146
|
+
part returns to shape when the load comes off. ⭐ Named `restore` and not
|
|
147
|
+
`recover`, which `materials` claims.
|
|
148
|
+
``degrade`` — past yield: the deformation is permanent, and the deflection
|
|
149
|
+
number computed above has stopped being true of it.
|
|
150
|
+
"""
|
|
151
|
+
return ("restore",) if utilisation(stress_pa, yield_pa) < YIELD_MARGIN else ("degrade",)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
__all__ = ["AFFORDANCES", "METHOD_ID", "SUPPORTS", "VERSION", "YIELD_MARGIN",
|
|
155
|
+
"affordances", "bending_stress_pa", "coefficients", "max_deflection_m",
|
|
156
|
+
"tip_deflection_m", "utilisation"]
|