Research-Stack/5-Applications/tools-scripts/simulation/sovereign_jenga_physics_test.py

696 lines
23 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.
# ==============================================================================
"""
Sovereign Jenga Physics Test Suite
FEA + Rigid Body Simulation for N-Space Manifold Verification
This module provides physics validation for the Sovereign Jenga demonstration system.
It interfaces with external FEA/rigid body engines to test:
- Stress distribution across lattice structures
- Global vibration modes
- Load path redundancy
- "Impossible" configuration stability
External Dependencies (choose one or more):
- pyCalculix (CalculiX FEA, free/open-source)
- FEniCS (FEM, free/open-source)
- PyAnsys (ANSYS, commercial)
- pybullet (Rigid body dynamics, free)
- mujoco (Rigid body dynamics, free for research)
"""
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 pathlib import Path
from typing import Dict, List, Tuple, Optional
from dataclasses import dataclass
import json
REPO_ROOT = Path(os.getenv("RESEARCH_STACK_ROOT") or Path(__file__).resolve().parents[1])
# Try to import optional physics engines
try:
import calculix
CALCULIX_AVAILABLE = True
except ImportError:
CALCULIX_AVAILABLE = False
try:
import fenics
FENICS_AVAILABLE = True
except ImportError:
FENICS_AVAILABLE = False
try:
import pybullet as p
PYBULLET_AVAILABLE = True
except ImportError:
PYBULLET_AVAILABLE = False
try:
import trimesh
TRIMESH_AVAILABLE = True
except ImportError:
TRIMESH_AVAILABLE = False
# ============================================================================
# Material Properties (Aluminum 6061-T6)
# ============================================================================
@dataclass
class MaterialProperties:
"""First-principles material properties for Aluminum 6061-T6"""
name: str = "Aluminum 6061-T6"
# Atomic Properties
atomic_mass: float = 26.98 # g/mol
crystal_structure: str = "FCC"
lattice_parameter: float = 4.05e-10 # m (4.05 Å)
# Elastic Properties (Isotropic)
youngs_modulus: float = 69e9 # Pa (69 GPa)
shear_modulus: float = 26e9 # Pa (26 GPa)
bulk_modulus: float = 76e9 # Pa (76 GPa)
poisson_ratio: float = 0.33
# Density
density: float = 2700 # kg/m³
# Strength
yield_strength: float = 276e6 # Pa (276 MPa)
ultimate_strength: float = 310e6 # Pa (310 MPa)
# Wave Propagation
longitudinal_wave_speed: float = 6400 # m/s
shear_wave_speed: float = 3100 # m/s
# Compressibility
compressibility: float = 1.32e-11 # Pa⁻¹
# Damping
damping_ratio: float = 0.001 # Structural damping
# ============================================================================
# Stress Tensor Operations (6D "N-Space")
# ============================================================================
class StressTensor:
"""
6D stress tensor operations for "N-space" mechanics.
This is the real physics behind the "N-dimensional stress space" metaphor.
"""
@staticmethod
def from_components(sx: float, sy: float, sz: float,
txy: float, txz: float, tyz: float) -> AnyArray:
"""Create stress tensor from 6 independent components"""
return xp.array([
[sx, txy, txz],
[txy, sy, tyz],
[txz, tyz, sz]
])
@staticmethod
def to_vector(tensor: AnyArray) -> AnyArray:
"""Convert 3x3 tensor to 6D vector (Voigt notation)"""
return xp.array([
tensor[0, 0], # σx
tensor[1, 1], # σy
tensor[2, 2], # σz
tensor[0, 1], # τxy
tensor[0, 2], # τxz
tensor[1, 2] # τyz
])
@staticmethod
def principal_stresses(tensor: AnyArray) -> Tuple[AnyArray, AnyArray]:
"""
Compute principal stresses and directions.
Returns:
eigenvalues: Principal stresses (σ1, σ2, σ3)
eigenvectors: Principal directions
"""
eigenvalues, eigenvectors = xp.linalg.eigh(tensor)
# Sort by magnitude
idx = xp.abs(eigenvalues).argsort()[::-1]
return eigenvalues[idx], eigenvectors[:, idx]
@staticmethod
def von_mises(tensor: AnyArray) -> float:
"""
Compute von Mises stress (yield criterion).
σ_vm = √(0.5 * [(σ1-σ2)² + (σ2-σ3)² + (σ3-σ1)²])
"""
principal, _ = StressTensor.principal_stresses(tensor)
s1, s2, s3 = principal
von_mises = xp.sqrt(0.5 * (
(s1 - s2)**2 +
(s2 - s3)**2 +
(s3 - s1)**2
))
return von_mises
@staticmethod
def equilibrium_residual(stress_field: AnyArray,
body_forces: AnyArray) -> AnyArray:
"""
Check equilibrium: ∇ · σ + f = 0
Returns residual (should be ~0 for equilibrium)
"""
# Simplified: compute divergence numerically
div_sigma = xp.gradient(stress_field, axis=0)
residual = div_sigma + body_forces
return residual
# ============================================================================
# Lattice Structure Generator (Octet Truss)
# ============================================================================
class OctetTrussGenerator:
"""
Generate octet truss lattice structures for Sovereign Jenga blocks.
"""
def __init__(self,
unit_cell_size: float = 0.01, # 10mm
strut_diameter: float = 0.001, # 1mm
relative_density: float = 0.2):
self.unit_cell_size = unit_cell_size
self.strut_diameter = strut_diameter
self.relative_density = relative_density
def generate_unit_cell(self) -> Dict:
"""
Generate single octet truss unit cell.
Returns dict with:
- nodes: Nx3 array of node positions
- elements: Mx2 array of element connectivity
"""
# Octet truss has 14 nodes per unit cell
# (8 corner + 6 face-center)
a = self.unit_cell_size
# Node positions
nodes = xp.array([
# Corners
[0, 0, 0],
[a, 0, 0],
[a, a, 0],
[0, a, 0],
[0, 0, a],
[a, 0, a],
[a, a, a],
[0, a, a],
# Face centers
[a/2, a/2, 0],
[a/2, a/2, a],
[a/2, 0, a/2],
[a/2, a, a/2],
[0, a/2, a/2],
[a, a/2, a/2],
])
# Element connectivity (tetrahedra + octahedra)
elements = [
# Bottom tetrahedra
[0, 1, 3, 8],
[1, 2, 3, 8],
# Top tetrahedra
[4, 5, 7, 9],
[5, 6, 7, 9],
# Vertical struts
[0, 4], [1, 5], [2, 6], [3, 7],
# Face diagonals
[8, 10], [8, 11], [8, 12], [8, 13],
[9, 10], [9, 11], [9, 12], [9, 13],
# Additional connections for full octet
[10, 12], [10, 13],
[11, 12], [11, 13],
]
return {
'nodes': nodes,
'elements': elements,
'num_nodes': len(nodes),
'num_elements': len(elements)
}
def generate_block(self,
length: float,
width: float,
height: float) -> Dict:
"""
Generate full block with internal octet lattice.
"""
# Calculate number of unit cells
nx = int(length / self.unit_cell_size)
ny = int(width / self.unit_cell_size)
nz = int(height / self.unit_cell_size)
# Generate lattice
all_nodes = []
all_elements = []
node_offset = 0
for i in range(nx):
for j in range(ny):
for k in range(nz):
cell = self.generate_unit_cell()
# Translate nodes
translation = xp.array([
i * self.unit_cell_size,
j * self.unit_cell_size,
k * self.unit_cell_size
])
cell_nodes = cell['nodes'] + translation
all_nodes.append(cell_nodes)
# Update element connectivity
cell_elements = cell['elements'] + node_offset
all_elements.extend(cell_elements)
node_offset += cell['num_nodes']
return {
'nodes': xp.vstack(all_nodes),
'elements': all_elements,
'dimensions': (length, width, height),
'num_cells': (nx, ny, nz)
}
# ============================================================================
# FEA Solver Interface
# ============================================================================
class FEASolver:
"""
Interface to external FEA engines for stress analysis.
"""
def __init__(self, engine: str = 'calculix'):
self.engine = engine
self.available_engines = []
if CALCULIX_AVAILABLE:
self.available_engines.append('calculix')
if FENICS_AVAILABLE:
self.available_engines.append('fenics')
if engine not in self.available_engines:
print(f"Warning: {engine} not available. Available: {self.available_engines}")
def solve_static(self,
geometry: Dict,
material: MaterialProperties,
boundary_conditions: Dict,
loads: Dict) -> Dict:
"""
Solve static equilibrium problem.
Returns:
stress_field: Stress tensor at each node
displacement_field: Displacement at each node
von_mises_field: Von Mises stress at each node
"""
if self.engine == 'calculix' and CALCULIX_AVAILABLE:
return self._solve_with_calculix(geometry, material, boundary_conditions, loads)
elif self.engine == 'fenics' and FENICS_AVAILABLE:
return self._solve_with_fenics(geometry, material, boundary_conditions, loads)
else:
# Fallback: simplified analytical solution
return self._solve_analytical(geometry, material, boundary_conditions, loads)
def _solve_with_calculix(self, geometry, material, bc, loads):
"""Interface to CalculiX FEA solver.
Requires: CalculiX (ccx) installed and on PATH.
Workflow: write .inp → run ccx → parse .frd output.
"""
raise NotImplementedError(
"CalculiX (ccx) required. Install: apt install calculix-ccx or build from source."
)
def _solve_with_fenics(self, geometry, material, bc, loads):
"""Interface to FEniCS FEM solver.
Requires: FEniCS (dolfin) Python package.
"""
raise NotImplementedError(
"FEniCS required. Install: pip install fenics or use conda install -c conda-forge fenics."
)
def _solve_analytical(self,
geometry: Dict,
material: MaterialProperties,
bc: Dict,
loads: Dict) -> Dict:
"""
Simplified analytical solution for quick testing.
Uses beam theory + truss analysis for approximate results.
"""
nodes = geometry['nodes']
elements = geometry['elements']
num_nodes = len(nodes)
# Initialize fields
displacement_field = xp.zeros((num_nodes, 3))
stress_field = xp.zeros((num_nodes, 3, 3))
von_mises_field = xp.zeros(num_nodes)
# Simplified: assume uniform stress distribution
total_load = loads.get('magnitude', 1000) # N
load_area = len([e for e in elements if len(e) > 2]) # Approximate
avg_stress = total_load / (load_area * 1e-6) # Pa (rough estimate)
# Fill stress field (simplified)
for i in range(num_nodes):
stress_tensor = StressTensor.from_components(
avg_stress, 0, 0, 0, 0, 0
)
stress_field[i] = stress_tensor
von_mises_field[i] = StressTensor.von_mises(stress_tensor)
# Simplified displacement (Hooke's law)
strain = avg_stress / material.youngs_modulus
displacement_field[:, 2] = strain * geometry['dimensions'][2]
return {
'stress_field': stress_field,
'displacement_field': displacement_field,
'von_mises_field': von_mises_field,
'max_stress': xp.max(von_mises_field),
'max_displacement': xp.max(xp.abs(displacement_field))
}
# ============================================================================
# Verification Tests (Pass/Fail Criteria)
# ============================================================================
class SovereignJengaVerifier:
"""
Run verification tests for Sovereign Jenga demonstration system.
"""
def __init__(self, material: MaterialProperties):
self.material = material
self.results = {}
def test_centerless_tower(self,
block_geometry: Dict,
num_blocks: int = 18) -> Dict:
"""
Test: Remove all central blocks, leave outer skeleton.
Tower should stand under own weight.
"""
print("\n=== Test: Center-less Tower ===")
# Setup: Create hollow tower (only outer ring of blocks)
# Simplified: check if stress < yield with center removed
solver = FEASolver()
# Apply gravity load
loads = {
'type': 'gravity',
'magnitude': self.material.density * 9.81 * block_geometry['dimensions'][0]
}
# Fixed base
bc = {'fixed': 'bottom'}
# Solve
result = solver.solve_static(block_geometry, self.material, bc, loads)
# Check: max stress < yield strength
max_stress = result['max_stress']
passed = max_stress < self.material.yield_strength
self.results['centerless_tower'] = {
'passed': passed,
'max_stress_MPa': max_stress / 1e6,
'yield_strength_MPa': self.material.yield_strength / 1e6,
'safety_factor': self.material.yield_strength / max_stress
}
status = "✓ PASS" if passed else "✗ FAIL"
print(f"Result: {status}")
print(f" Max stress: {max_stress/1e6:.2f} MPa")
print(f" Yield strength: {self.material.yield_strength/1e6:.2f} MPa")
print(f" Safety factor: {self.results['centerless_tower']['safety_factor']:.2f}")
return self.results['centerless_tower']
def test_45deg_overhang(self,
block_geometry: Dict) -> Dict:
"""
Test: Tower leans at 45° without support.
Stability via torsional-to-compressive conversion.
"""
print("\n=== Test: 45° Overhang ===")
# This test requires rigid body dynamics (not just static FEA)
# For now, simplified check: center of mass within support polygon
# Simplified: check if geometry can support 45° lean
# Real test would use PyBullet/MuJoCo
# Placeholder result
self.results['overhang_45deg'] = {
'passed': False, # Requires rigid body simulation
'note': 'Requires PyBullet/MuJoCo integration'
}
print(f"Result: ⚠ NOT YET IMPLEMENTED")
print(f" Note: Requires rigid body dynamics engine")
return self.results['overhang_45deg']
def test_vibration_distribution(self,
block_geometry: Dict) -> Dict:
"""
Test: Strike one block, entire structure responds.
Global vibration modes (not localized).
"""
print("\n=== Test: Vibration Distribution ===")
# Compute eigenmodes of stiffness matrix
# Global modes = eigenvectors span entire structure
solver = FEASolver()
# Simplified: check if structure has delocalized modes
# Real test would compute full modal analysis
# Placeholder
self.results['vibration_distribution'] = {
'passed': False, # Requires modal analysis
'note': 'Requires eigenmode computation'
}
print(f"Result: ⚠ NOT YET IMPLEMENTED")
print(f" Note: Requires modal analysis (eigenmode computation)")
return self.results['vibration_distribution']
def test_load_redistribution(self,
block_geometry: Dict,
remove_fraction: float = 0.3) -> Dict:
"""
Test: Remove 30% of blocks randomly.
Tower should still support 10kg top load.
"""
print("\n=== Test: Load Redistribution ===")
# Setup: Remove random blocks, apply 10kg load
# Check: stress < yield, displacement < threshold
solver = FEASolver()
loads = {
'type': 'point',
'magnitude': 10 * 9.81 # 10kg
}
bc = {'fixed': 'bottom'}
result = solver.solve_static(block_geometry, self.material, bc, loads)
# Check criteria
max_stress = result['max_stress']
max_disp = result['max_displacement']
stress_ok = max_stress < self.material.yield_strength
disp_ok = max_disp < 0.01 # 1cm threshold
passed = stress_ok and disp_ok
self.results['load_redistribution'] = {
'passed': passed,
'max_stress_MPa': max_stress / 1e6,
'max_displacement_mm': max_disp * 1000,
'stress_ok': stress_ok,
'displacement_ok': disp_ok
}
status = "✓ PASS" if passed else "✗ FAIL"
print(f"Result: {status}")
print(f" Max stress: {max_stress/1e6:.2f} MPa")
print(f" Max displacement: {max_disp*1000:.2f} mm")
return self.results['load_redistribution']
def test_semi_truck_lift(self,
block_geometry: Dict) -> Dict:
"""
Test: Use as jack base.
Lifts 10,000 lbs (one corner of semi-truck).
"""
print("\n=== Test: Semi-Truck Lift (10,000 lbs) ===")
# 10,000 lbs = 4536 kg = 44.5 kN
solver = FEASolver()
loads = {
'type': 'point',
'magnitude': 44500 # N (10,000 lbs)
}
bc = {'fixed': 'bottom'}
result = solver.solve_static(block_geometry, self.material, bc, loads)
max_stress = result['max_stress']
passed = max_stress < self.material.yield_strength
self.results['semi_truck_lift'] = {
'passed': passed,
'load_N': 44500,
'load_lbs': 10000,
'max_stress_MPa': max_stress / 1e6,
'yield_strength_MPa': self.material.yield_strength / 1e6,
'safety_factor': self.material.yield_strength / max_stress if max_stress > 0 else float('inf')
}
status = "✓ PASS" if passed else "✗ FAIL"
print(f"Result: {status}")
print(f" Load: 44,500 N (10,000 lbs)")
print(f" Max stress: {max_stress/1e6:.2f} MPa")
print(f" Safety factor: {self.results['semi_truck_lift']['safety_factor']:.2f}")
return self.results['semi_truck_lift']
def run_all_tests(self, block_geometry: Dict) -> Dict:
"""Run complete verification suite"""
print("\n" + "="*60)
print("SOVEREIGN JENGA VERIFICATION SUITE")
print("="*60)
self.test_centerless_tower(block_geometry)
self.test_45deg_overhang(block_geometry)
self.test_vibration_distribution(block_geometry)
self.test_load_redistribution(block_geometry)
self.test_semi_truck_lift(block_geometry)
# Summary
print("\n" + "="*60)
print("SUMMARY")
print("="*60)
passed = sum(1 for r in self.results.values() if r.get('passed', False))
total = len([r for r in self.results.values() if 'passed' in r])
not_implemented = len([r for r in self.results.values() if 'note' in r])
print(f"Passed: {passed}/{total}")
print(f"Not yet implemented: {not_implemented}")
return {
'passed': passed,
'total': total,
'not_implemented': not_implemented,
'details': self.results
}
# ============================================================================
# Main Entry Point
# ============================================================================
def main():
"""Run Sovereign Jenga physics verification"""
# Material
material = MaterialProperties()
print(f"Material: {material.name}")
print(f" Young's modulus: {material.youngs_modulus/1e9:.1f} GPa")
print(f" Yield strength: {material.yield_strength/1e6:.1f} MPa")
print(f" Density: {material.density} kg/m³")
# Generate block geometry
generator = OctetTrussGenerator(
unit_cell_size=0.01, # 10mm
strut_diameter=0.001, # 1mm
relative_density=0.2
)
block_geometry = generator.generate_block(
length=0.06, # 60mm
width=0.02, # 20mm
height=0.02 # 20mm
)
print(f"\nBlock geometry:")
print(f" Dimensions: {block_geometry['dimensions']}")
print(f" Num nodes: {len(block_geometry['nodes'])}")
print(f" Num elements: {len(block_geometry['elements'])}")
# Run verification
verifier = SovereignJengaVerifier(material)
results = verifier.run_all_tests(block_geometry)
# Save results
output_path = REPO_ROOT / "out" / "sovereign_jenga_verification.json"
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, 'w') as f:
json.dump(results, f, indent=2, default=str)
print(f"\n[+] Results saved to: {output_path}")
return results
if __name__ == "__main__":
main()