mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
606 lines
25 KiB
Python
606 lines
25 KiB
Python
#!/usr/bin/env python3
|
||
# ==============================================================================
|
||
# COPYRIGHT NO ONE EVERYWHERE LLC (WYOMING HOLDING COMPANY)
|
||
# PROJECT: SOVEREIGN STACK
|
||
# This artifact is entirely proprietary and cryptographically proven.
|
||
# Open-Source usage requires explicit permission from Brandon Scott Schneider.
|
||
# ==============================================================================
|
||
"""
|
||
semi_jack_constraint_model.py — GeomTREE Semi-Jack structural constraint analyzer
|
||
|
||
Applies the full OSHA/ASME PALD/EN 12839 constraint envelope to the existing
|
||
merkle_tree.json geometry and reports every violation. Works at toy scale (22mm)
|
||
first — if it fails here, scaling up won't fix it.
|
||
|
||
Standards applied:
|
||
ASME PALD-2009 — proof load 200% SWL, no permanent deformation
|
||
ASME B30.1 — design-to-failure ≥ 3× SWL
|
||
OSHA 1926.305 — firm foundation, never under load on jack alone
|
||
OSHA 1910.244 — rated capacity marked, sufficient for load
|
||
EN 12839 — jack stand requirements, 200% proof test
|
||
FMCSA grade ops — stability on 15° incline with full load
|
||
|
||
Usage:
|
||
python 5-Applications/scripts/semi_jack_constraint_model.py
|
||
python 5-Applications/scripts/semi_jack_constraint_model.py --radius 1.5 --swl 34335
|
||
python 5-Applications/scripts/semi_jack_constraint_model.py --radius 1.5 --list-flaws
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import math
|
||
import sys
|
||
from dataclasses import dataclass, field
|
||
from pathlib import Path
|
||
from typing import Dict, List, Optional, Tuple
|
||
|
||
# ── Standards-derived constraint constants ─────────────────────────────────────
|
||
|
||
# ASME PALD / EN 12839 — proof load multiplier (200% of SWL)
|
||
PROOF_LOAD_FACTOR = 2.0
|
||
|
||
# ASME B30.1 — minimum design-to-failure factor (3× SWL)
|
||
DESIGN_FAILURE_FACTOR = 3.0
|
||
|
||
# FMCSA 49 CFR 393 — maximum operating grade for stability check
|
||
MAX_GRADE_DEG = 15.0
|
||
|
||
# OSHA 1926.305 / ASME PALD — minimum base-to-height ratio for stability on level
|
||
MIN_BASE_HEIGHT_RATIO = 0.5
|
||
|
||
# ── Material envelopes ─────────────────────────────────────────────────────────
|
||
# All stresses in N/mm² (MPa)
|
||
|
||
# ── Test mass helpers ─────────────────────────────────────────────────────────
|
||
|
||
def block_load_N(l_mm: float, w_mm: float, h_mm: float, density_g_cm3: float) -> float:
|
||
vol_cm3 = (l_mm * w_mm * h_mm) / 1000.0
|
||
return (vol_cm3 * density_g_cm3 / 1000.0) * 9.81
|
||
|
||
def sphere_load_N(r_mm: float, density_g_cm3: float) -> float:
|
||
vol_cm3 = (4/3 * math.pi * r_mm**3) / 1000.0
|
||
return (vol_cm3 * density_g_cm3 / 1000.0) * 9.81
|
||
|
||
def cylinder_load_N(r_mm: float, h_mm: float, density_g_cm3: float) -> float:
|
||
vol_cm3 = (math.pi * r_mm**2 * h_mm) / 1000.0
|
||
return (vol_cm3 * density_g_cm3 / 1000.0) * 9.81
|
||
|
||
OSMIUM_DENSITY = 22.59 # g/cm³ — densest stable element, reference only
|
||
|
||
def osmium_brick_load_N(l_mm: float, w_mm: float, h_mm: float) -> float:
|
||
return block_load_N(l_mm, w_mm, h_mm, OSMIUM_DENSITY)
|
||
|
||
|
||
# ── Lattice / honeycomb mass ───────────────────────────────────────────────────
|
||
# The test mass IS the same Merkle tree geometry — a lattice of tubular struts.
|
||
# Mass = sum of strut volumes × bulk material density.
|
||
# Fill factor = strut volume / bounding box volume (how hollow it is).
|
||
|
||
def lattice_mass_kg(
|
||
nodes: Dict[int, "Node"],
|
||
edges: List[Tuple[int, int]],
|
||
tubule_radius_mm: float,
|
||
bulk_density_g_cm3: float,
|
||
) -> Tuple[float, float, float]:
|
||
"""
|
||
Returns (mass_kg, fill_factor, strut_vol_mm3).
|
||
bulk_density_g_cm3 is the density of the strut material itself — any value.
|
||
"""
|
||
strut_vol_mm3 = 0.0
|
||
for p_id, c_id in edges:
|
||
p, c = nodes[p_id], nodes[c_id]
|
||
length = math.sqrt(
|
||
(c.x - p.x)**2 + (c.y - p.y)**2 + (c.z - p.z)**2
|
||
)
|
||
strut_vol_mm3 += math.pi * tubule_radius_mm**2 * length
|
||
|
||
# Bounding box of all nodes
|
||
xs = [n.x for n in nodes.values()]
|
||
ys = [n.y for n in nodes.values()]
|
||
zs = [n.z for n in nodes.values()]
|
||
bbox_vol_mm3 = (
|
||
(max(xs) - min(xs) or 1.0) *
|
||
(max(ys) - min(ys) or 1.0) * # guard: planar Y=0 → use 1mm depth
|
||
(max(zs) - min(zs) or 1.0)
|
||
)
|
||
|
||
fill_factor = strut_vol_mm3 / bbox_vol_mm3
|
||
mass_kg = (strut_vol_mm3 / 1000.0) * bulk_density_g_cm3 / 1000.0
|
||
return mass_kg, fill_factor, strut_vol_mm3
|
||
|
||
|
||
MATERIALS = {
|
||
"SLS_PA12": {
|
||
"label": "SLS Nylon PA12 (no fiber)",
|
||
"compressive_yield_MPa": 70.0, # no permanent deformation under proof load
|
||
"compressive_ult_MPa": 95.0, # failure threshold (must exceed 3× SWL stress)
|
||
"tensile_yield_MPa": 48.0,
|
||
"shear_yield_MPa": 30.0, # ~0.6 × tensile yield (von Mises)
|
||
"density_g_cm3": 1.01,
|
||
},
|
||
"SLS_PA12_GF": {
|
||
"label": "SLS Nylon PA12 + 30% glass fiber",
|
||
"compressive_yield_MPa": 120.0,
|
||
"compressive_ult_MPa": 160.0,
|
||
"tensile_yield_MPa": 90.0,
|
||
"shear_yield_MPa": 52.0,
|
||
"density_g_cm3": 1.30,
|
||
},
|
||
"PLA": {
|
||
"label": "FDM PLA (prototype only)",
|
||
"compressive_yield_MPa": 50.0,
|
||
"compressive_ult_MPa": 65.0,
|
||
"tensile_yield_MPa": 37.0,
|
||
"shear_yield_MPa": 22.0,
|
||
"density_g_cm3": 1.24,
|
||
},
|
||
"AL6061_T6": {
|
||
"label": "Aluminum 6061-T6 (original JSON material)",
|
||
"compressive_yield_MPa": 276.0,
|
||
"compressive_ult_MPa": 310.0,
|
||
"tensile_yield_MPa": 276.0,
|
||
"shear_yield_MPa": 165.0,
|
||
"density_g_cm3": 2.70,
|
||
},
|
||
}
|
||
|
||
# ── Data structures ────────────────────────────────────────────────────────────
|
||
|
||
@dataclass
|
||
class Node:
|
||
id: int
|
||
x: float
|
||
y: float
|
||
z: float
|
||
force_N: float # nominal load at this node (from JSON)
|
||
children: List[int] = field(default_factory=list)
|
||
parent: Optional[int] = None
|
||
|
||
@dataclass
|
||
class EdgeResult:
|
||
parent_id: int
|
||
child_id: int
|
||
length_mm: float
|
||
branch_angle_deg: float # angle from vertical (load axis)
|
||
axial_force_N: float # component along branch axis
|
||
lateral_force_N: float # component perpendicular (lateral shear)
|
||
cross_section_mm2: float
|
||
axial_stress_MPa: float
|
||
shear_stress_MPa: float
|
||
von_mises_MPa: float
|
||
proof_ok: bool # survives 200% SWL without yield
|
||
failure_ok: bool # fails at or above 300% SWL
|
||
is_planar: bool # Y-coordinate delta is zero (planarity flag)
|
||
|
||
@dataclass
|
||
class GeometryFlaw:
|
||
severity: str # CRITICAL / WARNING / INFO
|
||
location: str
|
||
description: str
|
||
value: float
|
||
limit: float
|
||
unit: str
|
||
|
||
# ── Geometry loader ────────────────────────────────────────────────────────────
|
||
|
||
def load_tree(path: Path) -> Tuple[Dict[int, Node], List[Tuple[int, int]]]:
|
||
data = json.loads(path.read_text())
|
||
nodes: Dict[int, Node] = {}
|
||
for n in data["nodes"]:
|
||
nodes[n["id"]] = Node(
|
||
id=n["id"], x=n["x"], y=n["y"], z=n["z"],
|
||
force_N=n.get("F", 0.0)
|
||
)
|
||
edges = [(e[0], e[1]) for e in data["edges"]]
|
||
for p, c in edges:
|
||
nodes[p].children.append(c)
|
||
nodes[c].parent = p
|
||
return nodes, edges
|
||
|
||
# ── Constraint analysis ────────────────────────────────────────────────────────
|
||
|
||
def analyze_edge(
|
||
parent: Node, child: Node,
|
||
swl_N: float,
|
||
tubule_radius_mm: float,
|
||
material: dict,
|
||
root_force_N: float = 45000.0,
|
||
) -> EdgeResult:
|
||
dx = child.x - parent.x
|
||
dy = child.y - parent.y
|
||
dz = child.z - parent.z
|
||
length = math.sqrt(dx**2 + dy**2 + dz**2)
|
||
|
||
# Branch angle from vertical (Z axis is load axis, z goes negative downward)
|
||
horiz = math.sqrt(dx**2 + dy**2)
|
||
vert = abs(dz)
|
||
branch_angle_deg = math.degrees(math.atan2(horiz, vert)) if vert > 1e-9 else 90.0
|
||
|
||
# Scale branch force to specified SWL.
|
||
# Use child's fraction of the ROOT load (not just parent), so stress
|
||
# decreases correctly at deeper levels of the tree.
|
||
branch_force = swl_N * (child.force_N / max(root_force_N, 1e-9))
|
||
|
||
# Axial (along branch) and lateral (perpendicular) components
|
||
angle_rad = math.radians(branch_angle_deg)
|
||
axial_force = branch_force / max(math.cos(angle_rad), 1e-6) # actual strut force
|
||
lateral_force = axial_force * math.sin(angle_rad) # lateral component
|
||
|
||
# Cross-section
|
||
area_mm2 = math.pi * tubule_radius_mm**2
|
||
|
||
# Stresses (N/mm² = MPa)
|
||
axial_stress = axial_force / area_mm2
|
||
shear_stress = lateral_force / area_mm2
|
||
von_mises = math.sqrt(axial_stress**2 + 3 * shear_stress**2)
|
||
|
||
# ASME PALD / EN 12839: proof load = 2× SWL — no permanent deformation
|
||
proof_stress = von_mises * PROOF_LOAD_FACTOR
|
||
proof_ok = proof_stress <= material["compressive_yield_MPa"]
|
||
|
||
# ASME B30.1: failure must not occur below 3× SWL
|
||
failure_stress = von_mises * DESIGN_FAILURE_FACTOR
|
||
failure_ok = failure_stress <= material["compressive_ult_MPa"]
|
||
|
||
is_planar = abs(dy) < 1e-6 and abs(child.y) < 1e-6
|
||
|
||
return EdgeResult(
|
||
parent_id=parent.id,
|
||
child_id=child.id,
|
||
length_mm=length,
|
||
branch_angle_deg=branch_angle_deg,
|
||
axial_force_N=axial_force,
|
||
lateral_force_N=lateral_force,
|
||
cross_section_mm2=area_mm2,
|
||
axial_stress_MPa=axial_stress,
|
||
shear_stress_MPa=shear_stress,
|
||
von_mises_MPa=von_mises,
|
||
proof_ok=proof_ok,
|
||
failure_ok=failure_ok,
|
||
is_planar=is_planar,
|
||
)
|
||
|
||
|
||
def check_global_geometry(nodes: Dict[int, Node], swl_N: float) -> List[GeometryFlaw]:
|
||
flaws: List[GeometryFlaw] = []
|
||
|
||
# Find root (no parent) and leaves (no children)
|
||
root = next(n for n in nodes.values() if n.parent is None)
|
||
leaves = [n for n in nodes.values() if not n.children]
|
||
|
||
# ── Flaw 1: Planarity check ──────────────────────────────────────────────
|
||
all_y = [n.y for n in nodes.values()]
|
||
y_span = max(all_y) - min(all_y)
|
||
if y_span < 1e-6:
|
||
flaws.append(GeometryFlaw(
|
||
severity="CRITICAL",
|
||
location="ALL NODES",
|
||
description=(
|
||
"Structure is entirely planar (Y=0 for all nodes). "
|
||
"Zero resistance to any lateral force in the Y direction. "
|
||
"A 15° grade tilt in the Y plane produces unconstrained rotation. "
|
||
"Fix: rotate alternating branch levels by 90° in Y, "
|
||
"or use pentagonal (5-way) branching in 3D."
|
||
),
|
||
value=y_span,
|
||
limit=1.0, # at minimum, leaves must span some Y distance
|
||
unit="mm Y-span",
|
||
))
|
||
|
||
# ── Flaw 2: Base-to-height ratio (stability on level) ───────────────────
|
||
xs = [n.x for n in leaves]
|
||
ys = [n.y for n in leaves]
|
||
base_span_x = max(xs) - min(xs) if xs else 0.0
|
||
base_span_y = max(ys) - min(ys) if ys else 0.0
|
||
base_span = math.sqrt(base_span_x**2 + base_span_y**2) # diagonal
|
||
total_height = abs(root.z - min(n.z for n in nodes.values()))
|
||
|
||
ratio = base_span / max(total_height, 1e-9)
|
||
if ratio < MIN_BASE_HEIGHT_RATIO:
|
||
flaws.append(GeometryFlaw(
|
||
severity="CRITICAL",
|
||
location="ROOT↔LEAVES",
|
||
description=(
|
||
f"Base/height ratio {ratio:.3f} < {MIN_BASE_HEIGHT_RATIO} (ASME PALD / OSHA 1926.305). "
|
||
"Structure tips under lateral load. "
|
||
f"Base span: {base_span:.1f}mm, height: {total_height:.1f}mm. "
|
||
"Fix: widen leaf node spread or reduce height."
|
||
),
|
||
value=ratio,
|
||
limit=MIN_BASE_HEIGHT_RATIO,
|
||
unit="base/height",
|
||
))
|
||
|
||
# ── Flaw 3: 15° grade stability (FMCSA, OSHA field ops) ─────────────────
|
||
# Under 15° tilt, CG must remain over base polygon
|
||
# Simple check: CG horizontal shift = height × tan(15°)
|
||
cg_shift = total_height * math.tan(math.radians(MAX_GRADE_DEG))
|
||
half_base_x = base_span_x / 2.0
|
||
if cg_shift > half_base_x:
|
||
flaws.append(GeometryFlaw(
|
||
severity="CRITICAL",
|
||
location="STABILITY@15°",
|
||
description=(
|
||
f"On a {MAX_GRADE_DEG}° grade the CG shifts {cg_shift:.1f}mm horizontally "
|
||
f"but X half-base is only {half_base_x:.1f}mm. "
|
||
"Structure tips before reaching operating grade. "
|
||
"Fix: increase base span or reduce height."
|
||
),
|
||
value=cg_shift,
|
||
limit=half_base_x,
|
||
unit="mm CG shift vs half-base",
|
||
))
|
||
|
||
# ── Flaw 4: Binary vs pentagonal branching ───────────────────────────────
|
||
max_children = max(len(n.children) for n in nodes.values())
|
||
if max_children <= 2:
|
||
flaws.append(GeometryFlaw(
|
||
severity="WARNING",
|
||
location="BRANCHING FACTOR",
|
||
description=(
|
||
f"Maximum branching factor = {max_children} (binary). "
|
||
"Patent spec calls for pentagonal (5-way) branching. "
|
||
"Binary branching concentrates 50% of load at each parent node; "
|
||
"pentagonal distributes 20% per branch, reducing peak node stress by ~2.5×. "
|
||
"At toy scale this is acceptable for geometry validation but must be "
|
||
"upgraded before load testing."
|
||
),
|
||
value=float(max_children),
|
||
limit=5.0,
|
||
unit="branches/node",
|
||
))
|
||
|
||
# ── Flaw 5: Root force vs SWL ────────────────────────────────────────────
|
||
root_force = root.force_N
|
||
if abs(root_force - swl_N) / max(swl_N, 1e-9) > 0.05:
|
||
flaws.append(GeometryFlaw(
|
||
severity="WARNING",
|
||
location=f"ROOT NODE {root.id}",
|
||
description=(
|
||
f"Root force in JSON ({root_force:.0f}N = {root_force/9.81:.0f}kg) "
|
||
f"does not match specified SWL ({swl_N:.0f}N = {swl_N/9.81:.0f}kg). "
|
||
"Constraint analysis uses specified SWL; JSON force is noted as mismatch."
|
||
),
|
||
value=root_force,
|
||
limit=swl_N,
|
||
unit="N root force",
|
||
))
|
||
|
||
# ── Flaw 6: No leaf pad geometry ─────────────────────────────────────────
|
||
flaws.append(GeometryFlaw(
|
||
severity="INFO",
|
||
location="LEAF NODES",
|
||
description=(
|
||
f"{len(leaves)} leaf nodes are dimensionless points. "
|
||
"OSHA 1926.305(b): jack must sit on firm foundation — "
|
||
"requires a base pad geometry. "
|
||
"At toy scale: minimum pad area = load / allowable_bearing_pressure. "
|
||
"For SLS PA12 on printed surface: ~2× tubule area minimum. "
|
||
"Fix: add cap geometry to leaf nodes in STL output."
|
||
),
|
||
value=0.0,
|
||
limit=1.0,
|
||
unit="pad area defined",
|
||
))
|
||
|
||
return flaws
|
||
|
||
|
||
def _root_force(nodes: Dict[int, Node]) -> float:
|
||
root = next(n for n in nodes.values() if n.parent is None)
|
||
return root.force_N
|
||
|
||
|
||
def find_minimum_radius(
|
||
nodes: Dict[int, Node],
|
||
edges: List[Tuple[int, int]],
|
||
swl_N: float,
|
||
material: dict,
|
||
) -> float:
|
||
"""Binary search for minimum tubule radius that passes all edge constraints."""
|
||
rf = _root_force(nodes)
|
||
lo, hi = 0.1, 50.0
|
||
for _ in range(40):
|
||
mid = (lo + hi) / 2.0
|
||
all_ok = True
|
||
for p_id, c_id in edges:
|
||
r = analyze_edge(nodes[p_id], nodes[c_id], swl_N, mid, material, rf)
|
||
if not r.proof_ok or not r.failure_ok:
|
||
all_ok = False
|
||
break
|
||
if all_ok:
|
||
hi = mid
|
||
else:
|
||
lo = mid
|
||
return hi
|
||
|
||
|
||
# ── Report ─────────────────────────────────────────────────────────────────────
|
||
|
||
def print_report(
|
||
nodes: Dict[int, Node],
|
||
edges: List[Tuple[int, int]],
|
||
swl_N: float,
|
||
tubule_radius_mm: float,
|
||
material_key: str,
|
||
):
|
||
mat = MATERIALS[material_key]
|
||
sep = "=" * 72
|
||
print(f"\n{sep}")
|
||
print(" SEMI-JACK CONSTRAINT MODEL — GeomTREE Structural Analysis")
|
||
print(sep)
|
||
print(f" Material : {mat['label']}")
|
||
print(f" SWL : {swl_N:.0f} N ({swl_N/9.81:.1f} kg)")
|
||
print(f" Proof load : {swl_N*PROOF_LOAD_FACTOR:.0f} N ({swl_N*PROOF_LOAD_FACTOR/9.81:.1f} kg) [ASME PALD 200%]")
|
||
print(f" Fail floor : {swl_N*DESIGN_FAILURE_FACTOR:.0f} N ({swl_N*DESIGN_FAILURE_FACTOR/9.81:.1f} kg) [ASME B30.1 300%]")
|
||
print(f" Tubule r : {tubule_radius_mm:.2f} mm (area {math.pi*tubule_radius_mm**2:.3f} mm²)")
|
||
print(f" Yield allow: {mat['compressive_yield_MPa']} MPa (proof limit)")
|
||
print(f" Ult allow : {mat['compressive_ult_MPa']} MPa (failure floor)")
|
||
print()
|
||
|
||
# Global geometry flaws
|
||
flaws = check_global_geometry(nodes, swl_N)
|
||
print(f" GEOMETRY FLAWS ({len(flaws)} found)")
|
||
print(f" {'-'*68}")
|
||
for f in flaws:
|
||
marker = {"CRITICAL": "✗", "WARNING": "△", "INFO": "·"}[f.severity]
|
||
print(f" {marker} [{f.severity:8s}] {f.location}")
|
||
for line in f.description.split(". "):
|
||
if line.strip():
|
||
print(f" {line.strip()}.")
|
||
print(f" Value: {f.value:.3f} {f.unit} | Limit: {f.limit:.3f} {f.unit}")
|
||
print()
|
||
|
||
# Per-edge analysis
|
||
print(f" EDGE STRESS ANALYSIS (r={tubule_radius_mm:.2f}mm)")
|
||
print(f" {'-'*68}")
|
||
print(f" {'Edge':8s} {'Len':6s} {'Angle':7s} {'Axial':8s} {'Shear':8s} {'vMises':8s} {'Proof':6s} {'Fail':5s} {'Planar':6s}")
|
||
|
||
rf = _root_force(nodes)
|
||
critical_edges = []
|
||
planar_edges = []
|
||
for p_id, c_id in edges:
|
||
r = analyze_edge(nodes[p_id], nodes[c_id], swl_N, tubule_radius_mm, mat, rf)
|
||
proof_str = "OK" if r.proof_ok else "FAIL"
|
||
fail_str = "OK" if r.failure_ok else "FAIL"
|
||
planar_str = "YES" if r.is_planar else "no"
|
||
flag = " "
|
||
if not r.proof_ok or not r.failure_ok:
|
||
flag = "✗ "
|
||
critical_edges.append((p_id, c_id, r))
|
||
if r.is_planar:
|
||
planar_edges.append((p_id, c_id))
|
||
print(
|
||
f" {flag}{p_id:2d}→{c_id:2d} "
|
||
f"{r.length_mm:5.1f}mm "
|
||
f"{r.branch_angle_deg:5.1f}° "
|
||
f"{r.axial_stress_MPa:6.2f}MPa "
|
||
f"{r.shear_stress_MPa:6.2f}MPa "
|
||
f"{r.von_mises_MPa:6.2f}MPa "
|
||
f"{proof_str:6s} "
|
||
f"{fail_str:5s} "
|
||
f"{planar_str}"
|
||
)
|
||
|
||
# Minimum radius
|
||
min_r = find_minimum_radius(nodes, edges, swl_N, mat)
|
||
print()
|
||
print(f" MINIMUM TUBULE RADIUS TO PASS ALL CONSTRAINTS: {min_r:.3f} mm")
|
||
print(f" (at specified SWL={swl_N/9.81:.0f}kg, material={material_key})")
|
||
print()
|
||
|
||
# Summary
|
||
n_crit = sum(1 for f in flaws if f.severity == "CRITICAL")
|
||
n_warn = sum(1 for f in flaws if f.severity == "WARNING")
|
||
n_planar = len(planar_edges)
|
||
n_fail = len(critical_edges)
|
||
print(" SUMMARY")
|
||
print(" " + "-" * 68)
|
||
print(f" Critical geometry flaws : {n_crit}")
|
||
print(f" Warnings : {n_warn}")
|
||
print(f" Planar edges : {n_planar}/{len(edges)} (all Y=0 — zero Y-axis resistance)")
|
||
print(f" Stress failures : {n_fail}/{len(edges)} edge(s) at r={tubule_radius_mm:.2f}mm")
|
||
print(f" Minimum safe radius : {min_r:.3f} mm")
|
||
|
||
if n_crit > 0:
|
||
print()
|
||
print(f" VERDICT: FAILS CONSTRAINT ENVELOPE — {n_crit} critical flaw(s) must be resolved")
|
||
print(" before this geometry is valid at ANY scale.")
|
||
else:
|
||
print()
|
||
print(f" VERDICT: Geometry passes envelope at r={tubule_radius_mm:.2f}mm with {material_key}.")
|
||
print("=" * 72 + "\n")
|
||
|
||
|
||
# ── Entry point ────────────────────────────────────────────────────────────────
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="Semi-Jack GeomTREE constraint analyzer")
|
||
ap.add_argument("--json", default="5-Applications/out/sovereign_jenga/quantum_annealed/merkle_tree.json",
|
||
help="Path to merkle_tree.json")
|
||
ap.add_argument("--swl", type=float, default=34335.0,
|
||
help="Safe Working Load in Newtons (default: 34335 N = 3500 kg)")
|
||
ap.add_argument("--radius", type=float, default=1.0,
|
||
help="Tubule cross-section radius in mm (default: 1.0mm)")
|
||
ap.add_argument("--material", default="SLS_PA12",
|
||
choices=list(MATERIALS.keys()),
|
||
help="Material key (default: SLS_PA12)")
|
||
ap.add_argument("--toy-scale", action="store_true",
|
||
help="Scale SWL to toy proportions (area ratio from 22mm height to real 280mm)")
|
||
ap.add_argument("--lattice-mass", nargs=2, type=float, metavar=("R", "D"),
|
||
help="Use the tree geometry itself as the test mass: "
|
||
"tubule radius R mm, bulk strut density D g/cm³. "
|
||
"SWL = weight of the lattice at that radius and density.")
|
||
ap.add_argument("--osmium-brick", nargs=3, type=float, metavar=("L", "W", "H"),
|
||
help="Osmium block L×W×H mm as SWL (density=22.59 g/cm³)")
|
||
ap.add_argument("--test-block", nargs=4, type=float, metavar=("L", "W", "H", "D"),
|
||
help="Block L×W×H mm at density D g/cm³ as SWL. "
|
||
"Use any density — mass/weight is what matters.")
|
||
ap.add_argument("--test-sphere", nargs=2, type=float, metavar=("R", "D"),
|
||
help="Sphere radius R mm at density D g/cm³ as SWL.")
|
||
ap.add_argument("--test-cylinder", nargs=3, type=float, metavar=("R", "H", "D"),
|
||
help="Cylinder radius R mm, height H mm, density D g/cm³ as SWL.")
|
||
args = ap.parse_args()
|
||
|
||
json_path = Path(args.json)
|
||
if not json_path.exists():
|
||
json_path = Path(__file__).parent.parent / args.json
|
||
if not json_path.exists():
|
||
print(f"ERROR: {args.json} not found", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
nodes, edges = load_tree(json_path)
|
||
|
||
swl = args.swl
|
||
if args.lattice_mass:
|
||
r_mm, density = args.lattice_mass
|
||
mass_kg, fill, vol = lattice_mass_kg(nodes, edges, r_mm, density)
|
||
swl = mass_kg * 9.81
|
||
print(
|
||
f"[lattice-mass] tree geometry as test mass:\n"
|
||
f" strut r={r_mm}mm density={density} g/cm³\n"
|
||
f" strut vol={vol:.2f} mm³ fill factor={fill:.4f}\n"
|
||
f" mass={mass_kg:.6f} kg load={swl:.6f} N"
|
||
)
|
||
elif args.test_block:
|
||
l, w, h, d = args.test_block
|
||
swl = block_load_N(l, w, h, d)
|
||
print(f"[test-block] {l:.1f}×{w:.1f}×{h:.1f}mm density={d} g/cm³ "
|
||
f"→ {swl/9.81:.4f} kg {swl:.4f} N")
|
||
elif args.test_sphere:
|
||
r, d = args.test_sphere
|
||
swl = sphere_load_N(r, d)
|
||
print(f"[test-sphere] r={r:.1f}mm density={d} g/cm³ "
|
||
f"→ {swl/9.81:.4f} kg {swl:.4f} N")
|
||
elif args.test_cylinder:
|
||
r, h, d = args.test_cylinder
|
||
swl = cylinder_load_N(r, h, d)
|
||
print(f"[test-cyl] r={r:.1f}mm h={h:.1f}mm density={d} g/cm³ "
|
||
f"→ {swl/9.81:.4f} kg {swl:.4f} N")
|
||
elif args.osmium_brick:
|
||
l_mm, w_mm, h_mm = args.osmium_brick
|
||
swl = osmium_brick_load_N(l_mm, w_mm, h_mm)
|
||
print(f"[osmium-brick] {l_mm:.0f}×{w_mm:.0f}×{h_mm:.0f}mm "
|
||
f"density={OSMIUM_DENSITY} g/cm³ "
|
||
f"→ {swl/9.81:.4f} kg {swl:.4f} N")
|
||
elif args.toy_scale:
|
||
# Scale SWL by linear scale squared: toy height 22mm vs real min 280mm
|
||
scale = (22.0 / 280.0) ** 2
|
||
swl = args.swl * scale
|
||
print(f"[toy-scale] SWL scaled by {scale:.4f} → {swl:.1f} N ({swl/9.81:.2f} kg)")
|
||
|
||
print_report(nodes, edges, swl, args.radius, args.material)
|
||
|
||
# Material comparison
|
||
print(" MATERIAL COMPARISON (minimum radius to pass at specified SWL)")
|
||
print(" " + "-" * 50)
|
||
for key, mat in MATERIALS.items():
|
||
r = find_minimum_radius(nodes, edges, swl, mat)
|
||
print(f" {key:15s}: min radius = {r:.3f} mm ({mat['label']})")
|
||
print()
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|