mirror of
https://github.com/allaunthefox/Research-Stack.git
synced 2026-07-31 03:05:21 +00:00
383 lines
14 KiB
Python
383 lines
14 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_vibration.py — Vibration matrix analysis for Semi-Jack geometry
|
||
|
||
Assembles the global stiffness K, geometric stiffness K_geo (from atmospheric
|
||
pressure), and mass matrix M for a 3D pin-jointed truss.
|
||
|
||
Atmospheric pressure contribution:
|
||
Each strut acts as a cylinder under external pressure P_atm.
|
||
Net compressive axial force = P_atm × A_cross_section.
|
||
This modifies K via the geometric (stress-stiffening) matrix K_geo.
|
||
Under compression K_geo is negative → reduces effective stiffness → lowers ω.
|
||
|
||
Eigenvalue problem:
|
||
(K + K_geo) u = ω² M u
|
||
Natural frequencies: f_n = ω_n / (2π)
|
||
|
||
Boundary conditions:
|
||
Leaf nodes (no children) = fixed to ground (zero displacement).
|
||
Root + internal nodes = free.
|
||
|
||
Usage:
|
||
.venv-eng/bin/python 5-Applications/scripts/semi_jack_vibration.py
|
||
.venv-eng/bin/python 5-Applications/scripts/semi_jack_vibration.py --json 5-Applications/out/sovereign_jenga/quantum_annealed/merkle_tree_nspace.json
|
||
.venv-eng/bin/python 5-Applications/scripts/semi_jack_vibration.py --no-atm # vacuum comparison
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import math
|
||
import sys
|
||
from pathlib import Path
|
||
from typing import List, Set, Tuple
|
||
|
||
import sys
|
||
import os
|
||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
||
from math_harness_compat import xp, AnyArray
|
||
from scipy.linalg import eigh
|
||
|
||
# ── Physical constants ─────────────────────────────────────────────────────────
|
||
|
||
ATM_PRESSURE_PA = 101_325.0 # Pa — standard atmosphere (ISO 2533)
|
||
G_MS2 = 9.80665 # m/s² — standard gravity
|
||
|
||
# ── Material: SLS PA12 (SI units throughout) ──────────────────────────────────
|
||
|
||
E_PA12_PA = 1_700e6 # Young's modulus, Pa
|
||
RHO_PA12_KG_M3 = 1_010.0 # density, kg/m³
|
||
|
||
|
||
# ── Geometry loader ────────────────────────────────────────────────────────────
|
||
|
||
def load_geometry(path: Path):
|
||
data = json.loads(path.read_text())
|
||
nodes = {n["id"]: n for n in data["nodes"]}
|
||
edges = [(e[0], e[1]) for e in data["edges"]]
|
||
|
||
# Identify leaf nodes (appear only as children, never as parents)
|
||
parents = {e[0] for e in edges}
|
||
all_ids = set(nodes.keys())
|
||
leaves = all_ids - parents
|
||
|
||
return nodes, edges, leaves
|
||
|
||
|
||
# ── Element matrices (3-DOF pin-jointed truss element) ────────────────────────
|
||
# DOFs per node: [ux, uy, uz]. Element connects node i (DOFs 0-2) to j (DOFs 3-5).
|
||
|
||
def direction_cosines(ni: dict, nj: dict) -> Tuple[float, float, float, float]:
|
||
dx = (nj["x"] - ni["x"]) * 1e-3 # mm → m
|
||
dy = (nj["y"] - ni["y"]) * 1e-3
|
||
dz = (nj["z"] - ni["z"]) * 1e-3
|
||
L = math.sqrt(dx**2 + dy**2 + dz**2)
|
||
if L < 1e-12:
|
||
return 0.0, 0.0, 0.0, 0.0
|
||
return dx/L, dy/L, dz/L, L
|
||
|
||
|
||
def element_stiffness(
|
||
ni: dict, nj: dict, radius_m: float
|
||
) -> Tuple[AnyArray, float, float]:
|
||
"""
|
||
6×6 elastic stiffness matrix in global coords.
|
||
Returns (k_global, L_m, A_m2).
|
||
"""
|
||
lx, ly, lz, L = direction_cosines(ni, nj)
|
||
if L < 1e-12:
|
||
return xp.zeros((6,6)), 0.0, 0.0
|
||
|
||
A = math.pi * radius_m**2
|
||
EA_over_L = E_PA12_PA * A / L
|
||
|
||
# Direction cosine vector
|
||
d = xp.array([lx, ly, lz])
|
||
|
||
# Local-to-global: k = EA/L * [d; -d] ⊗ [d; -d]
|
||
dext = xp.concatenate([d, -d])
|
||
k_global = EA_over_L * xp.outer(dext, dext)
|
||
return k_global, L, A
|
||
|
||
|
||
def element_geo_stiffness(
|
||
ni: dict, nj: dict, radius_m: float, axial_force_N: float
|
||
) -> AnyArray:
|
||
"""
|
||
6×6 geometric stiffness matrix for axial pre-load P.
|
||
For a truss element: k_geo = P/L * [I -I; -I I] projected onto
|
||
the transverse plane (perpendicular to strut axis).
|
||
Negative P (compression) → negative k_geo → softens structure.
|
||
"""
|
||
lx, ly, lz, L = direction_cosines(ni, nj)
|
||
if L < 1e-12:
|
||
return xp.zeros((6,6))
|
||
|
||
d = xp.array([lx, ly, lz])
|
||
I3 = xp.eye(3)
|
||
# Transverse projector: I - d⊗d
|
||
Pt = I3 - xp.outer(d, d)
|
||
|
||
k_geo_3 = (axial_force_N / L) * Pt
|
||
k_geo = xp.zeros((6, 6))
|
||
k_geo[0:3, 0:3] = k_geo_3
|
||
k_geo[3:6, 3:6] = k_geo_3
|
||
k_geo[0:3, 3:6] = -k_geo_3
|
||
k_geo[3:6, 0:3] = -k_geo_3
|
||
return k_geo
|
||
|
||
|
||
def element_mass(
|
||
ni: dict, nj: dict, radius_m: float
|
||
) -> AnyArray:
|
||
"""
|
||
6×6 consistent mass matrix in global coords.
|
||
m = ρAL/6 * [2I I; I 2I] (consistent formulation for truss)
|
||
"""
|
||
_, L, A = element_stiffness(ni, nj, radius_m)
|
||
if L < 1e-12:
|
||
return xp.zeros((6,6))
|
||
|
||
m_total = RHO_PA12_KG_M3 * A * L
|
||
I3 = xp.eye(3)
|
||
m = xp.zeros((6, 6))
|
||
m[0:3, 0:3] = 2/6 * m_total * I3
|
||
m[3:6, 3:6] = 2/6 * m_total * I3
|
||
m[0:3, 3:6] = 1/6 * m_total * I3
|
||
m[3:6, 0:3] = 1/6 * m_total * I3
|
||
return m
|
||
|
||
|
||
# ── Global assembly ────────────────────────────────────────────────────────────
|
||
|
||
def assemble(
|
||
nodes: dict,
|
||
edges: List[Tuple[int,int]],
|
||
leaves: Set[int],
|
||
radius_mm: float,
|
||
swl_N: float,
|
||
with_atm: bool,
|
||
) -> Tuple[AnyArray, AnyArray, List[int], dict]:
|
||
"""
|
||
Assemble global K, K_geo, M.
|
||
Returns (K_total, M, free_dofs, diagnostics).
|
||
"""
|
||
node_ids = sorted(nodes.keys())
|
||
id_to_idx = {nid: i for i, nid in enumerate(node_ids)}
|
||
n_nodes = len(node_ids)
|
||
n_dofs = n_nodes * 3
|
||
|
||
radius_m = radius_mm * 1e-3
|
||
A_m2 = math.pi * radius_m**2
|
||
|
||
# Atmospheric pressure compressive force on each strut end
|
||
# F_atm = P_atm × A_cross (acts inward = compression = negative)
|
||
atm_force_N = -ATM_PRESSURE_PA * A_m2 if with_atm else 0.0
|
||
|
||
K = xp.zeros((n_dofs, n_dofs))
|
||
M = xp.zeros((n_dofs, n_dofs))
|
||
|
||
diag = {
|
||
"atm_force_per_strut_N": abs(atm_force_N),
|
||
"n_struts": len(edges),
|
||
"radius_mm": radius_mm,
|
||
"A_mm2": A_m2 * 1e6,
|
||
}
|
||
|
||
# Identify root (no parent in edge list)
|
||
child_ids = {e[1] for e in edges}
|
||
root_id = next(nid for nid in node_ids if nid not in child_ids)
|
||
|
||
# Root force distribution: each direct child carries swl_N * load_frac
|
||
root_force = nodes[root_id].get("F", swl_N)
|
||
|
||
for p_id, c_id in edges:
|
||
ni = nodes[p_id]
|
||
nj = nodes[c_id]
|
||
|
||
k_el, L_m, _area = element_stiffness(ni, nj, radius_m)
|
||
|
||
# Structural axial force in strut (from applied SWL)
|
||
child_F = nj.get("F", 0.0)
|
||
frac = child_F / max(root_force, 1e-9)
|
||
strut_V = swl_N * frac # vertical component
|
||
lx, ly, lz, _L = direction_cosines(ni, nj)
|
||
cos_ang = abs(lz) if L_m > 1e-12 else 1.0
|
||
strut_axial = strut_V / max(cos_ang, 1e-6) # along strut axis
|
||
|
||
# Atmospheric compressive pre-load
|
||
total_axial_N = -strut_axial + atm_force_N # compression = negative
|
||
|
||
k_geo = element_geo_stiffness(ni, nj, radius_m, total_axial_N)
|
||
m_el = element_mass(ni, nj, radius_m)
|
||
|
||
# DOF indices
|
||
ri = id_to_idx[p_id] * 3
|
||
rj = id_to_idx[c_id] * 3
|
||
idx = [ri, ri+1, ri+2, rj, rj+1, rj+2]
|
||
|
||
for a, ga in enumerate(idx):
|
||
for b, gb in enumerate(idx):
|
||
K[ga, gb] += k_el[a, b] + k_geo[a, b]
|
||
M[ga, gb] += m_el[a, b]
|
||
|
||
# ── Boundary conditions: fix leaf nodes ───────────────────────────────────
|
||
fixed_dofs: Set[int] = set()
|
||
for leaf_id in leaves:
|
||
base = id_to_idx[leaf_id] * 3
|
||
fixed_dofs.update([base, base+1, base+2])
|
||
|
||
all_dofs = list(range(n_dofs))
|
||
free_dofs = [d for d in all_dofs if d not in fixed_dofs]
|
||
|
||
return K, M, free_dofs, diag
|
||
|
||
|
||
# ── Eigenvalue solve ───────────────────────────────────────────────────────────
|
||
|
||
def natural_frequencies(
|
||
K: AnyArray,
|
||
M: AnyArray,
|
||
free_dofs: List[int],
|
||
n_modes: int = 10,
|
||
) -> AnyArray:
|
||
"""
|
||
Solve generalised eigenvalue problem on free DOFs.
|
||
Returns natural frequencies in Hz.
|
||
"""
|
||
Kf = K[xp.ix_(free_dofs, free_dofs)]
|
||
Mf = M[xp.ix_(free_dofs, free_dofs)]
|
||
|
||
if Kf.shape[0] == 0:
|
||
return xp.array([])
|
||
|
||
# Regularise: add small diagonal to M to avoid singularity
|
||
Mf += xp.eye(Mf.shape[0]) * 1e-18
|
||
|
||
n_req = min(n_modes, Kf.shape[0] - 1)
|
||
if n_req < 1:
|
||
return xp.array([])
|
||
|
||
try:
|
||
# eigh for symmetric matrices → real eigenvalues
|
||
eigenvalues, _ = eigh(Kf, Mf, subset_by_index=[0, n_req-1])
|
||
# ω² = eigenvalue, clip negatives (rigid-body / numerical noise)
|
||
omega2 = xp.clip(eigenvalues, 0, None)
|
||
return xp.sqrt(omega2) / (2 * math.pi) # Hz
|
||
except Exception as e:
|
||
print(f" [eigensolve warning] {e}", file=sys.stderr)
|
||
return xp.array([])
|
||
|
||
|
||
# ── Report ─────────────────────────────────────────────────────────────────────
|
||
|
||
def run(
|
||
json_path: Path,
|
||
radius_mm: float,
|
||
swl_N: float,
|
||
n_modes: int,
|
||
with_atm: bool,
|
||
):
|
||
nodes, edges, leaves = load_geometry(json_path)
|
||
print(f"\n {'='*66}")
|
||
print(f" Semi-Jack Vibration Matrix Analysis")
|
||
print(f" {'='*66}")
|
||
print(f" Geometry : {json_path.name}")
|
||
print(f" Nodes : {len(nodes)} Edges: {len(edges)} Leaves: {len(leaves)}")
|
||
print(f" Radius : {radius_mm} mm SWL: {swl_N:.1f} N ({swl_N/G_MS2:.2f} kg)")
|
||
print(f" Atm P : {'101325 Pa (standard atmosphere)' if with_atm else 'OFF (vacuum)'}")
|
||
print(f" Material : SLS PA12 E={E_PA12_PA/1e6:.0f} MPa ρ={RHO_PA12_KG_M3} kg/m³")
|
||
print()
|
||
|
||
K, M, free_dofs, diag = assemble(
|
||
nodes, edges, leaves, radius_mm, swl_N, with_atm
|
||
)
|
||
|
||
print(f" System size : {K.shape[0]} DOFs ({len(free_dofs)} free after BCs)")
|
||
print(f" Strut area : {diag['A_mm2']:.3f} mm²")
|
||
if with_atm:
|
||
print(f" Atm load/strut: {diag['atm_force_per_strut_N']:.4f} N compressive")
|
||
print()
|
||
|
||
freqs = natural_frequencies(K, M, free_dofs, n_modes)
|
||
|
||
if len(freqs) == 0:
|
||
print(" No free DOFs — structure is fully constrained.")
|
||
return freqs
|
||
|
||
print(f" NATURAL FREQUENCIES (first {len(freqs)} modes)")
|
||
print(f" {'-'*50}")
|
||
for i, f in enumerate(freqs):
|
||
label = ""
|
||
if i == 0: label = " ← fundamental"
|
||
if f < 20: label += " ⚠ near audible resonance"
|
||
print(f" Mode {i+1:3d}: {f:12.2f} Hz{label}")
|
||
|
||
print()
|
||
print(f" Fundamental period : {1/freqs[0]*1000:.3f} ms" if freqs[0] > 0 else "")
|
||
return freqs
|
||
|
||
|
||
def compare(json_path: Path, radius_mm: float, swl_N: float, n_modes: int):
|
||
"""Run with and without atmosphere, show delta."""
|
||
print("\n Running vacuum baseline...")
|
||
f_vac = run(json_path, radius_mm, swl_N, n_modes, with_atm=False)
|
||
|
||
print("\n Running with standard atmosphere (101325 Pa)...")
|
||
f_atm = run(json_path, radius_mm, swl_N, n_modes, with_atm=True)
|
||
|
||
if len(f_vac) == 0 or len(f_atm) == 0:
|
||
return
|
||
|
||
n = min(len(f_vac), len(f_atm))
|
||
print(f"\n {'='*66}")
|
||
print(f" ATMOSPHERIC PRESSURE EFFECT ON VIBRATION MODES")
|
||
print(f" {'='*66}")
|
||
print(f" {'Mode':6s} {'Vacuum Hz':>12s} {'Atm Hz':>12s} {'Delta Hz':>12s} {'Delta %':>10s}")
|
||
print(f" {'-'*56}")
|
||
for i in range(n):
|
||
delta = f_atm[i] - f_vac[i]
|
||
delta_pc = 100.0 * delta / max(f_vac[i], 1e-9)
|
||
flag = " ↓ softened" if delta < -0.01 * f_vac[i] else ""
|
||
print(
|
||
f" {i+1:6d} {f_vac[i]:12.2f} {f_atm[i]:12.2f} "
|
||
f"{delta:12.2f} {delta_pc:10.3f}%{flag}"
|
||
)
|
||
print(f" {'='*66}\n")
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser(description="Semi-Jack vibration matrix analysis")
|
||
ap.add_argument("--json", default="5-Applications/out/sovereign_jenga/quantum_annealed/merkle_tree_nspace.json")
|
||
ap.add_argument("--radius", type=float, default=4.52, help="Tubule radius mm")
|
||
ap.add_argument("--swl", type=float, default=104.0, help="SWL in Newtons")
|
||
ap.add_argument("--modes", type=int, default=10, help="Number of modes")
|
||
ap.add_argument("--no-atm", action="store_true", help="Vacuum only (no atmosphere)")
|
||
ap.add_argument("--no-compare", action="store_true", help="Skip side-by-side comparison")
|
||
args = ap.parse_args()
|
||
|
||
path = Path(args.json)
|
||
if not path.exists():
|
||
path = Path(__file__).parent.parent / args.json
|
||
if not path.exists():
|
||
print(f"ERROR: {args.json} not found", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
if args.no_atm:
|
||
run(path, args.radius, args.swl, args.modes, with_atm=False)
|
||
elif args.no_compare:
|
||
run(path, args.radius, args.swl, args.modes, with_atm=True)
|
||
else:
|
||
compare(path, args.radius, args.swl, args.modes)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|