Research-Stack/5-Applications/tools-scripts/semi_jack/semi_jack_constraint_model.py

606 lines
25 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()