Research-Stack/5-Applications/text-to-cad/models/physics_test_merkle_jack.py

404 lines
16 KiB
Python

"""
Physics testing for Merkle Jack structure under common strain conditions.
This script loads the merkle jack geometry and applies various strain conditions
to analyze structural integrity, stress distribution, and failure modes.
"""
import json
import numpy as np
from typing import List, Tuple, Dict, Any
import math
class StrainCondition:
"""Define a strain condition for testing."""
def __init__(self, name: str, description: str, load_type: str,
magnitude: float, direction: Tuple[float, float, float] = None):
self.name = name
self.description = description
self.load_type = load_type # 'compression', 'tension', 'shear', 'torsion'
self.magnitude = magnitude
self.direction = direction or (0, 0, 1)
class MerkleJackPhysics:
"""Physics analysis for Merkle Jack structure."""
def __init__(self, geometry_file: str):
"""Load geometry data from JSON file."""
with open(geometry_file, 'r') as f:
data = json.load(f)
self.nodes = data['nodes']
self.edges = data['edges']
self.params = data['parameters']
# Build node lookup
self.node_map = {n['id']: n for n in self.nodes}
# Material properties (assumed: steel-like)
self.youngs_modulus = 200e9 # Pa (200 GPa)
self.shear_modulus = 79.3e9 # Pa (79.3 GPa)
self.yield_strength = 250e6 # Pa (250 MPa)
self.ultimate_strength = 400e6 # Pa (400 MPa)
# Convert parameters from mm to meters
self.tubule_radius = self.params['tubule_radius'] / 1000.0 # m
self.cross_sectional_area = math.pi * self.tubule_radius**2
self.moment_of_inertia = (math.pi * self.tubule_radius**4) / 4
def calculate_edge_length(self, edge: Tuple[int, int]) -> float:
"""Calculate length of an edge in meters."""
p_id, c_id = edge
parent = self.node_map[p_id]
child = self.node_map[c_id]
dx = (child['x'] - parent['x']) / 1000.0 # mm to m
dy = (child['y'] - parent['y']) / 1000.0
dz = (child['z'] - parent['z']) / 1000.0
return math.sqrt(dx**2 + dy**2 + dz**2)
def calculate_edge_vector(self, edge: Tuple[int, int]) -> Tuple[float, float, float]:
"""Calculate unit vector along an edge."""
p_id, c_id = edge
parent = self.node_map[p_id]
child = self.node_map[c_id]
dx = (child['x'] - parent['x']) / 1000.0
dy = (child['y'] - parent['y']) / 1000.0
dz = (child['z'] - parent['z']) / 1000.0
length = math.sqrt(dx**2 + dy**2 + dz**2)
if length < 1e-9:
return (0, 0, 1)
return (dx/length, dy/length, dz/length)
def apply_compression(self, magnitude: float) -> Dict[int, float]:
"""Apply compressive load and calculate axial stress in each edge."""
edge_stresses = {}
# Load applied at top nodes (highest z)
max_z = max(n['z'] for n in self.nodes)
top_nodes = [n for n in self.nodes if abs(n['z'] - max_z) < 1e-6]
total_load = magnitude # Newtons
load_per_node = total_load / len(top_nodes) if top_nodes else 0
# Distribute load through tree structure
node_loads = {n['id']: 0.0 for n in self.nodes}
for node in top_nodes:
node_loads[node['id']] = load_per_node
# Propagate loads down the tree
for node in sorted(self.nodes, key=lambda n: -n['z']):
if node['parent'] is not None:
node_loads[node['parent']] += node_loads[node['id']]
# Calculate stress in each edge
for edge in self.edges:
p_id, c_id = edge
load = node_loads[c_id] # Load carried by this edge
stress = load / self.cross_sectional_area
edge_stresses[tuple(edge)] = stress
return edge_stresses
def apply_tension(self, magnitude: float) -> Dict[int, float]:
"""Apply tensile load (upward force at root)."""
edge_stresses = {}
# Load applied at root (node 0)
total_load = magnitude # Newtons
# Load propagates up through branching structure
node_loads = {n['id']: 0.0 for n in self.nodes}
node_loads[0] = total_load
# Distribute load to children based on load_frac
for node in sorted(self.nodes, key=lambda n: n['z']):
if node['parent'] is not None:
parent = self.node_map[node['parent']]
node_loads[node['id']] = node_loads[node['parent']] * node['load_frac']
# Calculate stress in each edge
for edge in self.edges:
p_id, c_id = edge
load = node_loads[c_id]
stress = load / self.cross_sectional_area
edge_stresses[tuple(edge)] = stress
return edge_stresses
def apply_shear(self, magnitude: float, direction: Tuple[float, float, float]) -> Dict[int, float]:
"""Apply shear load and calculate shear stress."""
edge_stresses = {}
# Shear stress = Force / Area
# For simplicity, assume shear distributed to all edges
total_load = magnitude
load_per_edge = total_load / len(self.edges) if self.edges else 0
for edge in self.edges:
# Calculate angle between edge and shear direction
edge_vec = self.calculate_edge_vector(edge)
shear_dir = np.array(direction) / np.linalg.norm(direction)
edge_dir = np.array(edge_vec)
# Shear stress depends on angle
cos_theta = abs(np.dot(shear_dir, edge_dir))
effective_load = load_per_edge * (1 - cos_theta) # Maximum when perpendicular
stress = effective_load / self.cross_sectional_area
edge_stresses[tuple(edge)] = stress
return edge_stresses
def apply_torsion(self, magnitude: float) -> Dict[int, float]:
"""Apply torsional load and calculate shear stress."""
edge_stresses = {}
# Torsional stress = T * r / J
# T = torque, r = radius, J = polar moment of inertia
torque = magnitude # N·m
polar_moment = (math.pi * self.tubule_radius**4) / 2
# For simplicity, distribute torsion through structure
# Edges farther from center carry more torsional stress
max_r = max(math.sqrt(n['x']**2 + n['y']**2) for n in self.nodes) / 1000.0
for edge in self.edges:
p_id, c_id = edge
child = self.node_map[c_id]
# Distance from center axis
r = math.sqrt(child['x']**2 + child['y']**2) / 1000.0
stress = (torque * r) / polar_moment
edge_stresses[tuple(edge)] = stress
return edge_stresses
def calculate_von_mises(self, axial_stress: float, shear_stress: float = 0) -> float:
"""Calculate Von Mises stress from axial and shear components."""
return math.sqrt(axial_stress**2 + 3 * shear_stress**2)
def analyze_stress_distribution(self, edge_stresses: Dict[Tuple[int, int], float]) -> Dict[str, Any]:
"""Analyze stress distribution across the structure."""
stresses = list(edge_stresses.values())
if not stresses:
return {
'max_stress': 0,
'min_stress': 0,
'mean_stress': 0,
'std_stress': 0,
'max_edge': None,
'safety_factor': float('inf'),
'failure_edges': []
}
max_stress = max(stresses)
min_stress = min(stresses)
mean_stress = np.mean(stresses)
std_stress = np.std(stresses)
# Find edge with maximum stress
max_edge = max(edge_stresses.items(), key=lambda x: x[1])[0] if edge_stresses else None
# Calculate safety factor
safety_factor = self.yield_strength / max_stress if max_stress > 0 else float('inf')
# Identify edges exceeding yield strength
failure_edges = [edge for edge, stress in edge_stresses.items()
if stress > self.yield_strength]
return {
'max_stress': max_stress,
'min_stress': min_stress,
'mean_stress': mean_stress,
'std_stress': std_stress,
'max_edge': max_edge,
'safety_factor': safety_factor,
'failure_edges': failure_edges,
'failure_count': len(failure_edges)
}
def run_strain_test(self, condition: StrainCondition) -> Dict[str, Any]:
"""Run a single strain test condition."""
print(f"\n{'='*60}")
print(f"Testing: {condition.name}")
print(f"Description: {condition.description}")
print(f"Load Type: {condition.load_type}")
print(f"Magnitude: {condition.magnitude} N")
print(f"{'='*60}")
# Apply load based on type
if condition.load_type == 'compression':
edge_stresses = self.apply_compression(condition.magnitude)
elif condition.load_type == 'tension':
edge_stresses = self.apply_tension(condition.magnitude)
elif condition.load_type == 'shear':
edge_stresses = self.apply_shear(condition.magnitude, condition.direction)
elif condition.load_type == 'torsion':
edge_stresses = self.apply_torsion(condition.magnitude)
else:
raise ValueError(f"Unknown load type: {condition.load_type}")
# Analyze stress distribution
analysis = self.analyze_stress_distribution(edge_stresses)
# Add condition info
analysis['condition_name'] = condition.name
analysis['condition_description'] = condition.description
analysis['load_type'] = condition.load_type
analysis['load_magnitude'] = condition.magnitude
# Print results
print(f"Maximum Stress: {analysis['max_stress']/1e6:.2f} MPa")
print(f"Minimum Stress: {analysis['min_stress']/1e6:.2f} MPa")
print(f"Mean Stress: {analysis['mean_stress']/1e6:.2f} MPa")
print(f"Std Dev: {analysis['std_stress']/1e6:.2f} MPa")
print(f"Safety Factor: {analysis['safety_factor']:.2f}")
print(f"Failure Edges: {analysis['failure_count']}/{len(self.edges)}")
if analysis['failure_edges']:
print(f"⚠️ FAILURE DETECTED at edges: {analysis['failure_edges'][:5]}...")
elif analysis['safety_factor'] < 1.5:
print(f"⚠️ LOW SAFETY MARGIN (< 1.5)")
else:
print(f"✅ STRUCTURE SAFE")
return analysis
def run_all_tests(self) -> List[Dict[str, Any]]:
"""Run all common strain condition tests."""
results = []
# Define test conditions
conditions = [
StrainCondition(
"Compression - Moderate",
"Compressive load equal to structure weight",
"compression",
1000.0 # 1 kN
),
StrainCondition(
"Compression - Heavy",
"Heavy compressive load (10x structure weight)",
"compression",
10000.0 # 10 kN
),
StrainCondition(
"Tension - Uplift",
"Upward tensile load at root",
"tension",
5000.0 # 5 kN
),
StrainCondition(
"Shear - Lateral",
"Lateral shear load in X direction",
"shear",
2000.0,
(1, 0, 0)
),
StrainCondition(
"Shear - Wind",
"Wind load simulation (lateral shear)",
"shear",
3000.0,
(0, 1, 0)
),
StrainCondition(
"Torsion - Twist",
"Torsional load about vertical axis",
"torsion",
500.0 # 500 N·m
),
]
for condition in conditions:
result = self.run_strain_test(condition)
results.append(result)
return results
def generate_report(self, results: List[Dict[str, Any]], output_file: str):
"""Generate a physics test report."""
with open(output_file, 'w') as f:
f.write("# Merkle Jack Physics Test Report\n\n")
f.write(f"Generated: {__import__('datetime').datetime.now()}\n\n")
f.write("## Geometry Parameters\n\n")
f.write(f"- Depth: {self.params['depth']}\n")
f.write(f"- Branching Factor: {self.params['branching_factor']}\n")
f.write(f"- Branch Angles: {self.params['branch_angles']}\n")
f.write(f"- Azimuthal Offsets: {self.params['az_offsets']}\n")
f.write(f"- Tubule Radius: {self.params['tubule_radius']} mm\n")
f.write(f"- Height per Level: {self.params['height_per_level']} mm\n")
f.write(f"- Total Nodes: {len(self.nodes)}\n")
f.write(f"- Total Edges: {len(self.edges)}\n\n")
f.write("## Material Properties\n\n")
f.write(f"- Young's Modulus: {self.youngs_modulus/1e9:.1f} GPa\n")
f.write(f"- Shear Modulus: {self.shear_modulus/1e9:.1f} GPa\n")
f.write(f"- Yield Strength: {self.yield_strength/1e6:.1f} MPa\n")
f.write(f"- Ultimate Strength: {self.ultimate_strength/1e6:.1f} MPa\n\n")
f.write("## Test Results\n\n")
for result in results:
f.write(f"### {result['condition_name']}\n\n")
f.write(f"**Description:** {result['condition_description']}\n\n")
f.write(f"**Load Type:** {result['load_type']}\n")
f.write(f"**Magnitude:** {result['load_magnitude']} N\n\n")
f.write(f"**Maximum Stress:** {result['max_stress']/1e6:.2f} MPa\n")
f.write(f"**Minimum Stress:** {result['min_stress']/1e6:.2f} MPa\n")
f.write(f"**Mean Stress:** {result['mean_stress']/1e6:.2f} MPa\n")
f.write(f"**Standard Deviation:** {result['std_stress']/1e6:.2f} MPa\n")
f.write(f"**Safety Factor:** {result['safety_factor']:.2f}\n")
f.write(f"**Failure Edges:** {result['failure_count']}/{len(self.edges)}\n\n")
if result['failure_edges']:
f.write(f"⚠️ **FAILURE DETECTED** at edges: {result['failure_edges'][:5]}\n\n")
elif result['safety_factor'] < 1.5:
f.write(f"⚠️ **LOW SAFETY MARGIN** (< 1.5)\n\n")
else:
f.write(f"✅ **STRUCTURE SAFE**\n\n")
f.write("## Summary\n\n")
safe_tests = sum(1 for r in results if r['failure_count'] == 0 and r['safety_factor'] >= 1.5)
warning_tests = sum(1 for r in results if r['failure_count'] == 0 and r['safety_factor'] < 1.5)
failure_tests = sum(1 for r in results if r['failure_count'] > 0)
f.write(f"- Safe Tests: {safe_tests}/{len(results)}\n")
f.write(f"- Low Margin Tests: {warning_tests}/{len(results)}\n")
f.write(f"- Failure Tests: {failure_tests}/{len(results)}\n\n")
if failure_tests > 0:
f.write("⚠️ **CRITICAL:** Structure fails under some load conditions.\n\n")
elif warning_tests > 0:
f.write("⚠️ **WARNING:** Structure has low safety margins under some conditions.\n\n")
else:
f.write("✅ **PASS:** Structure is safe under all tested conditions.\n\n")
print(f"\nReport generated: {output_file}")
if __name__ == "__main__":
# Load geometry and run physics tests
geometry_file = "/home/allaun/Documents/Research Stack/5-Applications/text-to-cad/models/merkle_jack.json"
output_report = "/home/allaun/Documents/Research Stack/5-Applications/text-to-cad/models/physics_test_report.md"
print("Loading Merkle Jack geometry...")
physics = MerkleJackPhysics(geometry_file)
print(f"Loaded {len(physics.nodes)} nodes and {len(physics.edges)} edges")
print("\nRunning physics strain tests...")
results = physics.run_all_tests()
print("\nGenerating test report...")
physics.generate_report(results, output_report)
print("\nPhysics testing complete!")