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

566 lines
27 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.

"""
Material-Bounded Merkle Jack - Realistic structure using Research Stack mathematics.
This model uses FAMM frustration physics, manifold-generalized Bernoulli equations,
and String-Star Manifold concepts to find OPTIMAL geometry, not impossible dynamic adaptation.
Key differences from adaptive model:
- No instantaneous geometry changes (physically impossible)
- Elastic deformation only (Hooke's Law)
- Yield strength enforcement
- Fatigue life analysis
- Manufacturing feasibility constraints
- Optimization-based design instead of adaptation
Mathematical Frameworks:
- FAMM: Minimize frustration in optimal geometry design
- Manifold-generalized Bernoulli: Optimal load distribution in design phase
- String-Star Manifold: Curvature-aware geometry optimization
- Scale Space: Multi-scale optimization for manufacturing
"""
import json
import numpy as np
import math
from typing import List, Tuple, Dict, Any, Optional
from dataclasses import dataclass
@dataclass
class MaterialProperties:
"""Real material properties with physical constraints.
SLS Nylon PA12 (Selective Laser Sintering):
- Lower modulus and strength than steel
- Anisotropic due to layer orientation
- Porosity affects properties
"""
youngs_modulus: float = 1.7e9 # Pa (SLS PA12 nylon)
yield_strength: float = 48e6 # Pa (SLS PA12)
ultimate_strength: float = 52e6 # Pa (SLS PA12)
shear_modulus: float = 0.6e9 # Pa (SLS PA12)
poisson_ratio: float = 0.4
density: float = 930 # kg/m³ (SLS PA12)
fatigue_strength_coefficient: float = 0.3 # S-N curve coefficient (lower for polymers)
fatigue_exponent: float = -0.12 # S-N curve exponent
porosity: float = 0.03 # 3% porosity typical for SLS
anisotropy_factor: float = 0.8 # Strength reduction in weak direction
@dataclass
class ManufacturingConstraints:
"""Manufacturing feasibility constraints for SLS."""
min_tubule_radius: float = 0.8e-3 # 0.8 mm minimum (SLS powder size limit)
max_tubule_radius: float = 10.0e-3 # 10 mm maximum (SLS build volume)
min_branch_angle: float = 45.0 # degrees (SLS overhang limit ~45°)
max_branch_angle: float = 60.0 # degrees
min_feature_size: float = 0.6e-3 # 0.6 mm (SLS powder size ~60µm)
max_aspect_ratio: float = 5.0 # length/radius ratio (SLS support limited)
layer_thickness: float = 0.1e-3 # 0.1 mm SLS layer thickness
surface_roughness: float = 15e-6 # 15 µm Ra (SLS typical)
min_wall_thickness: float = 0.8e-3 # 0.8 mm (SLS minimum)
build_direction: str = "vertical" # SLS build orientation
@dataclass
class LoadCondition:
"""Expected load condition for design optimization."""
name: str
load_type: str # 'compression', 'tension', 'shear', 'torsion'
magnitude: float # N or N·m
direction: Tuple[float, float, float]
probability: float # Probability of occurrence (0-1)
cycles: int # Expected load cycles for fatigue
class MaterialBoundedMerkleJack:
"""Material-bounded Merkle Jack with realistic physics constraints."""
def __init__(self, geometry_file: str):
"""Load initial geometry."""
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
self.material = MaterialProperties()
# Manufacturing constraints
self.manufacturing = ManufacturingConstraints()
# Convert to meters
self.tubule_radius = self.params['tubule_radius'] / 1000.0
self.cross_sectional_area = math.pi * self.tubule_radius**2
# Elastic deformation state
self.elastic_deformation = {}
self.residual_stress = {}
# Fatigue damage accumulation
self.fatigue_damage = {}
def calculate_elastic_deformation(self, stress: float, edge_length: float) -> float:
"""
Calculate elastic deformation using Hooke's Law.
σ = E * ε → ε = σ / E
ΔL = ε * L = (σ / E) * L
"""
strain = stress / self.material.youngs_modulus
deformation = strain * edge_length
return deformation
def check_yield_criterion(self, stress: float) -> bool:
"""Check if stress exceeds yield strength."""
return stress <= self.material.yield_strength
def calculate_von_mises_stress(self, axial_stress: float, shear_stress: float = 0) -> float:
"""Calculate Von Mises stress."""
return math.sqrt(axial_stress**2 + 3 * shear_stress**2)
def calculate_fatigue_life(self, stress_amplitude: float, mean_stress: float = 0) -> int:
"""
Calculate fatigue life using S-N curve with Goodman correction.
N = (σ_e / σ_a)^(1/b) with Goodman mean stress correction
"""
# Endurance limit (typical for steel: 0.5 * ultimate strength)
endurance_limit = self.material.fatigue_strength_coefficient * self.material.ultimate_strength
# Goodman mean stress correction
stress_ratio = mean_stress / self.material.ultimate_strength if self.material.ultimate_strength > 0 else 0
effective_amplitude = stress_amplitude / (1 - stress_ratio)
# S-N curve: N = (σ_e / σ_a)^(1/b)
if effective_amplitude > 0 and effective_amplitude < endurance_limit:
cycles = int((endurance_limit / effective_amplitude) ** (1 / abs(self.material.fatigue_exponent)))
else:
cycles = 0 # Immediate failure
return cycles
def check_manufacturing_feasibility(self, radius: float, angle_deg: float) -> Tuple[bool, List[str]]:
"""Check if geometry is manufacturable."""
issues = []
# Check radius constraints
if radius < self.manufacturing.min_tubule_radius:
issues.append(f"Radius {radius*1000:.2f} mm below minimum {self.manufacturing.min_tubule_radius*1000:.2f} mm")
if radius > self.manufacturing.max_tubule_radius:
issues.append(f"Radius {radius*1000:.2f} mm above maximum {self.manufacturing.max_tubule_radius*1000:.2f} mm")
# Check angle constraints
if angle_deg < self.manufacturing.min_branch_angle:
issues.append(f"Branch angle {angle_deg:.1f}° below minimum {self.manufacturing.min_branch_angle:.1f}°")
if angle_deg > self.manufacturing.max_branch_angle:
issues.append(f"Branch angle {angle_deg:.1f}° above maximum {self.manufacturing.max_branch_angle:.1f}°")
# Check aspect ratio
edge_lengths = [self.calculate_edge_length(edge) for edge in self.edges]
for length in edge_lengths:
aspect_ratio = length / radius if radius > 0 else float('inf')
if aspect_ratio > self.manufacturing.max_aspect_ratio:
issues.append(f"Aspect ratio {aspect_ratio:.1f} exceeds maximum {self.manufacturing.max_aspect_ratio}")
return len(issues) == 0, issues
def calculate_edge_length(self, edge: Tuple[int, int]) -> float:
"""Calculate edge length 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
dy = (child['y'] - parent['y']) / 1000.0
dz = (child['z'] - parent['z']) / 1000.0
return math.sqrt(dx**2 + dy**2 + dz**2)
def optimize_geometry_for_loads(self, load_conditions: List[LoadCondition]) -> Dict[str, Any]:
"""
Optimize geometry for multiple load conditions using FAMM and manifold math.
This finds the BEST INITIAL geometry, not dynamic adaptation.
"""
print(f"\n{'='*70}")
print(f"OPTIMIZING GEOMETRY FOR {len(load_conditions)} LOAD CONDITIONS")
print(f"{'='*70}")
# Current geometry evaluation
current_evaluation = self.evaluate_all_loads(load_conditions)
print(f"\nCurrent Geometry Performance:")
print(f" Max stress: {current_evaluation['max_stress']/1e6:.2f} MPa")
print(f" Safety factor: {current_evaluation['min_safety_factor']:.2f}")
print(f" Fatigue life: {min(current_evaluation['fatigue_lives']) if current_evaluation['fatigue_lives'] else 'N/A'} cycles")
print(f" Manufacturing feasible: {current_evaluation['manufacturing_feasible']}")
if not current_evaluation['manufacturing_feasible']:
print(f" Manufacturing issues: {len(current_evaluation['manufacturing_issues'])}")
for issue in current_evaluation['manufacturing_issues'][:3]:
print(f" - {issue}")
# Optimization using FAMM frustration minimization
# Instead of changing geometry dynamically, we find optimal static geometry
optimal_angles = self.optimize_branch_angles_famm(load_conditions)
optimal_radius = self.optimize_tubule_radius_manifold(load_conditions)
print(f"\nOptimized Geometry Parameters:")
print(f" Branch angles: {[f'{a:.1f}°' for a in optimal_angles]}")
print(f" Tubule radius: {optimal_radius*1000:.2f} mm")
# Evaluate optimized geometry
old_params = self.params['branch_angles'][:]
old_radius = self.tubule_radius
self.params['branch_angles'] = optimal_angles
self.tubule_radius = optimal_radius
self.cross_sectional_area = math.pi * optimal_radius**2
optimized_evaluation = self.evaluate_all_loads(load_conditions)
print(f"\nOptimized Geometry Performance:")
print(f" Max stress: {optimized_evaluation['max_stress']/1e6:.2f} MPa")
print(f" Safety factor: {optimized_evaluation['min_safety_factor']:.2f}")
print(f" Fatigue life: {min(optimized_evaluation['fatigue_lives']) if optimized_evaluation['fatigue_lives'] else 'N/A'} cycles")
print(f" Manufacturing feasible: {optimized_evaluation['manufacturing_feasible']}")
if not optimized_evaluation['manufacturing_feasible']:
print(f" Manufacturing issues: {len(optimized_evaluation['manufacturing_issues'])}")
# Calculate improvement
stress_reduction = (current_evaluation['max_stress'] - optimized_evaluation['max_stress']) / current_evaluation['max_stress']
sf_improvement = (optimized_evaluation['min_safety_factor'] - current_evaluation['min_safety_factor']) / current_evaluation['min_safety_factor']
print(f"\nOptimization Results:")
print(f" Stress reduction: {stress_reduction*100:.1f}%")
print(f" Safety factor improvement: {sf_improvement*100:.1f}%")
# Restore original parameters
self.params['branch_angles'] = old_params
self.tubule_radius = old_radius
self.cross_sectional_area = math.pi * old_radius**2
return {
'current': current_evaluation,
'optimized': optimized_evaluation,
'optimal_angles': optimal_angles,
'optimal_radius': optimal_radius,
'improvement': {
'stress_reduction': stress_reduction,
'sf_improvement': sf_improvement
}
}
def optimize_branch_angles_famm(self, load_conditions: List[LoadCondition]) -> List[float]:
"""
Optimize branch angles using FAMM frustration minimization.
Find angles that minimize frustration across all expected loads.
"""
# Current angles
angles = list(self.params['branch_angles'])
# Simple gradient descent on angle space
best_angles = angles[:]
best_frustration = float('inf')
# Search space: ±15 degrees around current angles
search_range = 15.0 # degrees
step_size = 5.0 # degrees
for i in range(len(angles)):
test_angles = angles[:]
for delta in np.arange(-search_range, search_range + step_size, step_size):
test_angle = angles[i] + delta
# Clamp to manufacturing constraints
test_angle = max(self.manufacturing.min_branch_angle,
min(self.manufacturing.max_branch_angle, test_angle))
test_angles[i] = test_angle
# Calculate frustration for this configuration
frustration = self.calculate_total_frustration(test_angles, load_conditions)
if frustration < best_frustration:
best_frustration = frustration
best_angles[i] = test_angle
return best_angles
def calculate_total_frustration(self, angles: List[float], load_conditions: List[LoadCondition]) -> float:
"""Calculate total FAMM frustration across all load conditions."""
total_frustration = 0.0
# Temporarily set angles
old_angles = self.params['branch_angles'][:]
self.params['branch_angles'] = angles
for load in load_conditions:
# Calculate stress distribution for this load
stresses = self.calculate_stress_for_load(load)
# Calculate frustration
if stresses:
mean_stress = np.mean(list(stresses.values()))
if mean_stress > 0:
for stress in stresses.values():
total_frustration += abs(stress - mean_stress) / mean_stress
# Restore angles
self.params['branch_angles'] = old_angles
return total_frustration / len(load_conditions) if load_conditions else 0
def optimize_tubule_radius_manifold(self, load_conditions: List[LoadCondition]) -> float:
"""
Optimize tubule radius using manifold-generalized Bernoulli.
Find radius that balances stress across manifold curvature.
"""
# Current radius
radius = self.tubule_radius
# Search space: ±50% around current radius
search_min = max(self.manufacturing.min_tubule_radius, radius * 0.5)
search_max = min(self.manufacturing.max_tubule_radius, radius * 1.5)
best_radius = radius
best_stress = float('inf')
# Test different radii
for test_radius in np.linspace(search_min, search_max, 20):
# Temporarily set radius
old_radius = self.tubule_radius
old_area = self.cross_sectional_area
self.tubule_radius = test_radius
self.cross_sectional_area = math.pi * test_radius**2
# Calculate max stress across all loads
max_stress = 0
for load in load_conditions:
stresses = self.calculate_stress_for_load(load)
if stresses:
max_stress = max(max_stress, max(stresses.values()))
if max_stress < best_stress:
best_stress = max_stress
best_radius = test_radius
# Restore radius
self.tubule_radius = old_radius
self.cross_sectional_area = old_area
return best_radius
def calculate_merkle_reinforcement_factor(self, edge: Tuple[int, int]) -> float:
"""
Calculate strain reinforcement factor from merkle tree topology.
The merkle tree provides strain reinforcement through frustration physics:
- When an edge is loaded, strain propagates through the tree
- Sibling edges share load due to frustration minimization
- Deeper nodes benefit from more load sharing paths
- Branching factor determines reinforcement strength
Reinforcement factor R = 1 + (branching_factor - 1) * (depth / max_depth)
"""
p_id, c_id = edge
child = self.node_map[c_id]
depth = child['depth']
max_depth = self.params['depth']
branching_factor = self.params['branching_factor']
# Base reinforcement from branching
# More branches = more load sharing = higher reinforcement
base_reinforcement = 1 + (branching_factor - 1) * 0.5
# Depth-dependent reinforcement
# Deeper nodes have more load sharing paths through the tree
depth_factor = depth / max_depth if max_depth > 0 else 0
# Total reinforcement factor
# R = base * depth_factor + 1 (minimum reinforcement of 1)
reinforcement = 1 + base_reinforcement * depth_factor * 0.3
return reinforcement
def calculate_stress_for_load(self, load: LoadCondition) -> Dict[Tuple[int, int], float]:
"""Calculate stress distribution for a specific load condition with SLS effects and merkle reinforcement."""
stresses = {}
# Effective area accounting for porosity
effective_area = self.cross_sectional_area * (1 - self.material.porosity)
# Anisotropy factor based on build direction
anisotropy = self.material.anisotropy_factor
# Calculate stress for each edge with merkle reinforcement
for edge in self.edges:
base_stress = 0
if load.load_type == 'tension':
if edge[0] == 0:
base_stress = load.magnitude / effective_area
else:
base_stress = load.magnitude * 0.5 / effective_area
# Tension is sensitive to anisotropy
base_stress /= anisotropy
elif load.load_type == 'compression':
base_stress = load.magnitude * 0.1 / effective_area
# Compression less sensitive to anisotropy
base_stress /= (anisotropy * 0.9 + 0.1)
elif load.load_type == 'shear':
base_stress = load.magnitude * 0.2 / effective_area
# Shear highly sensitive to anisotropy
base_stress /= anisotropy
elif load.load_type == 'torsion':
p_id, c_id = edge
child = self.node_map[c_id]
r = math.sqrt(child['x']**2 + child['y']**2) / 1000.0
base_stress = load.magnitude * r / effective_area
# Torsion sensitive to anisotropy
base_stress /= anisotropy
# Apply merkle topology strain reinforcement
# The tree structure provides load sharing through frustration physics
reinforcement = self.calculate_merkle_reinforcement_factor(edge)
base_stress /= reinforcement
stresses[tuple(edge)] = base_stress
return stresses
def evaluate_all_loads(self, load_conditions: List[LoadCondition]) -> Dict[str, Any]:
"""Evaluate geometry under all load conditions."""
max_stress = 0
min_safety_factor = float('inf')
fatigue_lives = []
for load in load_conditions:
stresses = self.calculate_stress_for_load(load)
if stresses:
load_max_stress = max(stresses.values())
max_stress = max(max_stress, load_max_stress)
safety_factor = self.material.yield_strength / load_max_stress
min_safety_factor = min(min_safety_factor, safety_factor)
# Calculate fatigue life
fatigue_life = self.calculate_fatigue_life(load_max_stress * 0.5, load_max_stress * 0.1)
fatigue_lives.append(fatigue_life)
# Check manufacturing feasibility
feasible, issues = self.check_manufacturing_feasibility(self.tubule_radius,
self.params['branch_angles'][0])
return {
'max_stress': max_stress,
'min_safety_factor': min_safety_factor,
'fatigue_lives': fatigue_lives,
'manufacturing_feasible': feasible,
'manufacturing_issues': issues
}
def generate_design_report(self, optimization_result: Dict[str, Any], output_file: str):
"""Generate design optimization report."""
with open(output_file, 'w') as f:
f.write("# Material-Bounded Merkle Jack Design Report\n\n")
f.write(f"Generated: {__import__('datetime').datetime.now()}\n\n")
f.write("## Material Properties\n\n")
f.write(f"- Material: SLS Nylon PA12 (Selective Laser Sintering)\n")
f.write(f"- Young's Modulus: {self.material.youngs_modulus/1e9:.1f} GPa\n")
f.write(f"- Yield Strength: {self.material.yield_strength/1e6:.1f} MPa\n")
f.write(f"- Ultimate Strength: {self.material.ultimate_strength/1e6:.1f} MPa\n")
f.write(f"- Density: {self.material.density} kg/m³\n")
f.write(f"- Porosity: {self.material.porosity*100:.1f}%\n")
f.write(f"- Anisotropy Factor: {self.material.anisotropy_factor}\n\n")
f.write("## Manufacturing Constraints\n\n")
f.write(f"- Min/Max Tubule Radius: {self.manufacturing.min_tubule_radius*1000:.1f} / {self.manufacturing.max_tubule_radius*1000:.1f} mm\n")
f.write(f"- Min/Max Branch Angle: {self.manufacturing.min_branch_angle:.1f}° / {self.manufacturing.max_branch_angle:.1f}°\n")
f.write(f"- Max Aspect Ratio: {self.manufacturing.max_aspect_ratio}\n\n")
f.write("## Optimization Results\n\n")
f.write(f"**Stress Reduction:** {optimization_result['improvement']['stress_reduction']*100:.1f}%\n")
f.write(f"**Safety Factor Improvement:** {optimization_result['improvement']['sf_improvement']*100:.1f}%\n\n")
f.write("### Recommended Geometry\n\n")
f.write(f"- Branch Angles: {[f'{a:.1f}°' for a in optimization_result['optimal_angles']]}\n")
f.write(f"- Tubule Radius: {optimization_result['optimal_radius']*1000:.2f} mm\n\n")
f.write("### Performance Comparison\n\n")
f.write("| Metric | Current | Optimized |\n")
f.write("|--------|---------|----------|\n")
f.write(f"| Max Stress | {optimization_result['current']['max_stress']/1e6:.2f} MPa | {optimization_result['optimized']['max_stress']/1e6:.2f} MPa |\n")
f.write(f"| Safety Factor | {optimization_result['current']['min_safety_factor']:.2f} | {optimization_result['optimized']['min_safety_factor']:.2f} |\n")
f.write(f"| Fatigue Life | {min(optimization_result['current']['fatigue_lives']) if optimization_result['current']['fatigue_lives'] else 'N/A'} | {min(optimization_result['optimized']['fatigue_lives']) if optimization_result['optimized']['fatigue_lives'] else 'N/A'} |\n")
f.write(f"| Manufacturing Feasible | {'Yes' if optimization_result['current']['manufacturing_feasible'] else 'No'} | {'Yes' if optimization_result['optimized']['manufacturing_feasible'] else 'No'} |\n\n")
if not optimization_result['optimized']['manufacturing_feasible']:
f.write("### Manufacturing Issues\n\n")
for issue in optimization_result['optimized']['manufacturing_issues']:
f.write(f"- {issue}\n")
f.write("## Key Differences from Adaptive Model\n\n")
f.write("- **No instantaneous geometry changes** - uses optimal static geometry\n")
f.write("- **Elastic deformation only** - obeys Hooke's Law\n")
f.write("- **Yield strength enforcement** - prevents plastic deformation\n")
f.write("- **Fatigue life analysis** - accounts for cyclic loading\n")
f.write("- **Manufacturing constraints** - realistic production limits\n")
f.write("- **Optimization-based design** - finds best initial configuration\n")
f.write("- **Merkle topology strain reinforcement** - load sharing through frustration physics\n\n")
f.write("## Merkle Topology Strain Reinforcement\n\n")
f.write("The merkle tree structure provides strain reinforcement through FAMM frustration physics:\n")
f.write("- When an edge is loaded, strain propagates through the tree\n")
f.write("- Sibling edges share load due to frustration minimization\n")
f.write("- Deeper nodes benefit from more load sharing paths\n")
f.write("- Branching factor determines reinforcement strength\n\n")
f.write(f"- Branching Factor: {self.params['branching_factor']}\n")
f.write(f"- Tree Depth: {self.params['depth']}\n")
f.write(f"- Max Reinforcement Factor: {1 + (self.params['branching_factor'] - 1) * 0.5 * 0.3:.2f}x\n\n")
if optimization_result['optimized']['min_safety_factor'] >= 1.5:
f.write("✅ **DESIGN SAFE** - Meets safety requirements\n")
elif optimization_result['optimized']['min_safety_factor'] >= 1.0:
f.write("⚠️ **DESIGN MARGINAL** - Low safety margin\n")
else:
f.write("❌ **DESIGN UNSAFE** - Exceeds yield strength\n")
print(f"\nDesign report generated: {output_file}")
if __name__ == "__main__":
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/material_bounded_design_report.md"
print("Initializing Material-Bounded Merkle Jack...")
jack = MaterialBoundedMerkleJack(geometry_file)
print(f"Loaded {len(jack.nodes)} nodes and {len(jack.edges)} edges")
print("Using material physics constraints: Hooke's Law, yield strength, fatigue, manufacturing limits")
# Define expected load conditions
load_conditions = [
LoadCondition("Compression - Static", "compression", 10000.0, (0, 0, -1), 0.5, 1000),
LoadCondition("Tension - Uplift", "tension", 5000.0, (0, 0, 1), 0.3, 10000),
LoadCondition("Shear - Wind", "shear", 3000.0, (0, 1, 0), 0.4, 5000),
LoadCondition("Torsion - Twist", "torsion", 500.0, (0, 0, 1), 0.1, 1000),
]
print(f"\nOptimizing for {len(load_conditions)} expected load conditions...")
# Optimize geometry
optimization_result = jack.optimize_geometry_for_loads(load_conditions)
# Generate report
jack.generate_design_report(optimization_result, output_report)
print("\nMaterial-bounded design optimization complete!")
print("This model uses realistic material physics and manufacturing constraints.")