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

478 lines
18 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_geometry_search.py — n-space geometry search for the Semi-Jack
A "perfect mass in n-space" is the geometry where stress is perfectly
uniform across every node in the Merkle tree — the attractor state where
no node is over-loaded and none is under-utilised. Every strut operates
at the same fraction of its capacity simultaneously.
The parameter space (n-space) for a depth-D tree:
branch_angle[0..D-1] — angle from vertical per level (degrees)
az_offset[0..D-1] — azimuthal rotation between levels (degrees)
branching_factor — children per node (2..6)
tubule_radius — strut cross-section radius (mm)
Total: 2D + 2 dimensions. Default D=4 → 10D search space.
Fitness = stress_variance across all edges + constraint penalties.
Perfect geometry → fitness = 0.
Parallel iteration: each worker evaluates one candidate independently.
The main loop keeps the top-K survivors, perturbs them, and re-evaluates.
No shared state between workers — pure function evaluation.
Usage:
python 5-Applications/scripts/semi_jack_geometry_search.py
python 5-Applications/scripts/semi_jack_geometry_search.py --depth 4 --pop 64 --iters 50
python 5-Applications/scripts/semi_jack_geometry_search.py --depth 4 --workers 8 --out best.json
"""
from __future__ import annotations
import argparse
import json
import math
import random
import sys
from concurrent.futures import ProcessPoolExecutor, as_completed
from dataclasses import dataclass, field
from pathlib import Path
from typing import Dict, List, Optional, Tuple
# ── Constants (mirror constraint_model) ───────────────────────────────────────
PROOF_LOAD_FACTOR = 2.0
DESIGN_FAIL_FACTOR = 3.0
# Default material: SLS PA12 (strut structural limits)
MAT_YIELD_MPA = 70.0
MAT_ULT_MPA = 95.0
# Search space bounds
ANGLE_MIN, ANGLE_MAX = 5.0, 60.0 # branch angle from vertical (deg)
AZ_MIN, AZ_MAX = 0.0, 180.0 # azimuthal offset (deg)
BF_MIN, BF_MAX = 2, 6 # branching factor
RADIUS_MIN, RADIUS_MAX = 0.3, 5.0 # tubule radius (mm)
# ── Parametric tree geometry ───────────────────────────────────────────────────
@dataclass
class GNode:
id: int
x: float
y: float
z: float
load_frac: float # fraction of root SWL at this node
parent: Optional[int] = None
depth: int = 0
@dataclass
class Candidate:
branch_angles: List[float] # one per depth level
az_offsets: List[float] # azimuthal rotation per level (deg)
branching_factor: int
tubule_radius: float
fitness: float = float("inf")
nodes: List[GNode] = field(default_factory=list)
edges: List[Tuple[int,int]] = field(default_factory=list)
def generate_tree(
depth: int,
branch_angles: List[float],
az_offsets: List[float],
branching_factor: int,
height_per_level: float = 6.0, # mm per level
) -> Tuple[List[GNode], List[Tuple[int,int]]]:
"""
Build a rooted tree in 3D.
- Root at (0, 0, 0).
- At each level, children are placed symmetrically around the parent,
at branch_angle[level] from vertical, spaced evenly in azimuth,
with az_offsets[level] rotating the whole fan at that level.
- load_frac halves (for binary) or divides by branching_factor at each step.
"""
nodes: List[GNode] = [GNode(id=0, x=0.0, y=0.0, z=0.0, load_frac=1.0, depth=0)]
edges: List[Tuple[int,int]] = []
node_id = 1
current_level = [0] # indices of nodes at current depth
for lv in range(depth):
angle_deg = branch_angles[min(lv, len(branch_angles)-1)]
angle_rad = math.radians(angle_deg)
az_base = az_offsets[min(lv, len(az_offsets)-1)]
next_level = []
for pid in current_level:
parent = nodes[pid]
child_frac = parent.load_frac / branching_factor
step_z = height_per_level * math.cos(angle_rad)
step_r = height_per_level * math.sin(angle_rad)
for k in range(branching_factor):
az_deg = az_base + k * (360.0 / branching_factor)
az_rad = math.radians(az_deg)
cx = parent.x + step_r * math.cos(az_rad)
cy = parent.y + step_r * math.sin(az_rad)
cz = parent.z - step_z # growing downward
child = GNode(
id=node_id,
x=cx, y=cy, z=cz,
load_frac=child_frac,
parent=pid,
depth=lv+1,
)
nodes.append(child)
edges.append((pid, node_id))
next_level.append(node_id)
node_id += 1
current_level = next_level
return nodes, edges
# ── Stress calculation ─────────────────────────────────────────────────────────
ATM_PRESSURE_PA = 101_325.0 # Pa — standard atmosphere
def edge_von_mises(
parent: GNode, child: GNode,
swl_N: float, radius_mm: float,
with_atm: bool = True,
) -> float:
"""
Von Mises stress (MPa) in one strut.
Includes atmospheric pressure as additional axial compression when with_atm=True.
ATM adds: P_atm × A / A = P_atm (MPa) directly to axial stress.
"""
dx = child.x - parent.x
dy = child.y - parent.y
dz = child.z - parent.z
length = math.sqrt(dx**2 + dy**2 + dz**2)
if length < 1e-9:
return 0.0
horiz = math.sqrt(dx**2 + dy**2)
vert = abs(dz)
angle_rad = math.atan2(horiz, max(vert, 1e-9))
branch_force = swl_N * child.load_frac
axial = branch_force / max(math.cos(angle_rad), 1e-6)
lateral = axial * math.sin(angle_rad)
area = math.pi * radius_mm**2
sa = axial / area
ss = lateral / area
# Atmospheric pressure adds uniform compressive stress on all strut faces
atm_stress = (ATM_PRESSURE_PA * 1e-6) if with_atm else 0.0 # Pa → MPa
sa_total = sa + atm_stress
return math.sqrt(sa_total**2 + 3 * ss**2)
# ── Fitness function ───────────────────────────────────────────────────────────
def fitness(
nodes: List[GNode],
edges: List[Tuple[int,int]],
radius: float,
swl_N: float,
with_atm: bool = True,
) -> Tuple[float, List[float]]:
"""
Fitness = variance of normalised stresses + heavy penalties.
Perfect geometry: all stresses equal → variance = 0.
Constraint penalties:
- proof stress > yield → +1000 per violating edge
- fail stress < ult → +1000 per edge that fails too early
- base/height ratio → +500 if unstable
- planarity → +500 if all Y ≈ 0
"""
node_map = {n.id: n for n in nodes}
stresses: List[float] = []
proof_violations = 0
fail_violations = 0
for p_id, c_id in edges:
p = node_map[p_id]
c = node_map[c_id]
vm = edge_von_mises(p, c, swl_N, radius, with_atm)
stresses.append(vm)
if vm * PROOF_LOAD_FACTOR > MAT_YIELD_MPA: proof_violations += 1
if vm * DESIGN_FAIL_FACTOR > MAT_ULT_MPA: fail_violations += 1
if not stresses:
return float("inf"), []
# Normalise stresses to [0,1] relative to yield
norm = [s / max(MAT_YIELD_MPA, 1e-9) for s in stresses]
mean = sum(norm) / len(norm)
variance = sum((s - mean)**2 for s in norm) / len(norm)
# Penalties
penalty = 0.0
penalty += 1000.0 * proof_violations
penalty += 1000.0 * fail_violations
# Stability: base span vs height
ys = [n.y for n in nodes]
xs = [n.x for n in nodes]
y_span = max(ys) - min(ys)
x_span = max(xs) - min(xs)
base = math.sqrt(x_span**2 + y_span**2)
height = abs(min(n.z for n in nodes) - max(n.z for n in nodes))
if height > 1e-9 and base / height < 0.5:
penalty += 500.0 * (0.5 - base/height)
# Planarity: all Y ≈ 0 → add heavy penalty
if y_span < 1e-3:
penalty += 500.0
return variance + penalty, stresses
# ── Worker (runs in separate process) ─────────────────────────────────────────
def evaluate_candidate(args_tuple) -> Tuple[float, dict]:
"""
Top-level function (picklable) for ProcessPoolExecutor.
Returns (fitness_score, serialisable candidate dict).
"""
(branch_angles, az_offsets, branching_factor,
tubule_radius, depth, height_per_level, swl_N, with_atm) = args_tuple
nodes, edges = generate_tree(
depth, branch_angles, az_offsets,
branching_factor, height_per_level,
)
score, stresses = fitness(nodes, edges, tubule_radius, swl_N, with_atm)
return score, {
"branch_angles": branch_angles,
"az_offsets": az_offsets,
"branching_factor": branching_factor,
"tubule_radius": tubule_radius,
"fitness": score,
"stress_mean_MPa": sum(stresses)/len(stresses) if stresses else 0.0,
"stress_var": score, # approximate — includes penalties
"nodes": [
{"id": n.id, "x": n.x, "y": n.y, "z": n.z,
"load_frac": n.load_frac, "depth": n.depth}
for n in nodes
],
"edges": [[p, c] for p, c in edges],
}
# ── Parameter sampling & perturbation ─────────────────────────────────────────
def random_params(depth: int, rng: random.Random) -> tuple:
angles = [rng.uniform(ANGLE_MIN, ANGLE_MAX) for _ in range(depth)]
az = [rng.uniform(AZ_MIN, AZ_MAX) for _ in range(depth)]
bf = rng.randint(BF_MIN, BF_MAX)
radius = rng.uniform(RADIUS_MIN, RADIUS_MAX)
return angles, az, bf, radius
def perturb(params: tuple, depth: int, rng: random.Random, scale: float = 0.15) -> tuple:
angles, az, bf, radius = params
new_angles = [
max(ANGLE_MIN, min(ANGLE_MAX, a + rng.gauss(0, scale*(ANGLE_MAX-ANGLE_MIN))))
for a in angles
]
new_az = [
(a + rng.gauss(0, scale*(AZ_MAX-AZ_MIN))) % 360.0
for a in az
]
new_bf = max(BF_MIN, min(BF_MAX, bf + rng.randint(-1, 1)))
new_r = max(RADIUS_MIN, min(RADIUS_MAX, radius + rng.gauss(0, scale*(RADIUS_MAX-RADIUS_MIN))))
return new_angles, new_az, new_bf, new_r
# ── Main search loop ───────────────────────────────────────────────────────────
def search(
depth: int = 4,
population: int = 64,
iterations: int = 40,
survivors: int = 8,
workers: int = 4,
swl_N: float = 104.0,
height_per_level: float = 6.0,
seed: int = 42,
with_atm: bool = True,
) -> dict:
rng = random.Random(seed)
# Initial population
pop: List[tuple] = [random_params(depth, rng) for _ in range(population)]
best_score = float("inf")
best_result = None
for iteration in range(iterations):
# Build args for workers
work = [
(p[0], p[1], p[2], p[3], depth, height_per_level, swl_N, with_atm)
for p in pop
]
results: List[Tuple[float, dict]] = []
with ProcessPoolExecutor(max_workers=workers) as ex:
futs = {ex.submit(evaluate_candidate, w): i for i, w in enumerate(work)}
for fut in as_completed(futs):
try:
results.append(fut.result())
except Exception as e:
results.append((float("inf"), {}))
results.sort(key=lambda r: r[0])
iter_best = results[0][0]
if iter_best < best_score:
best_score = iter_best
best_result = results[0][1]
print(
f" iter {iteration+1:3d}/{iterations} "
f"best={iter_best:.6f} "
f"pop_best={results[0][1].get('stress_mean_MPa', 0):.3f}MPa mean "
f"bf={results[0][1].get('branching_factor',0)} "
f"r={results[0][1].get('tubule_radius',0):.2f}mm "
f"angles={[round(a,1) for a in results[0][1].get('branch_angles',[])]}"
)
if best_score < 1e-6:
print(" [converged]")
break
# Breed next generation from survivors
survivors_params = []
for score, res in results[:survivors]:
if res:
p = (
res["branch_angles"], res["az_offsets"],
res["branching_factor"], res["tubule_radius"],
)
survivors_params.append(p)
pop = list(survivors_params)
# Fill rest with perturbations of survivors
while len(pop) < population:
parent = rng.choice(survivors_params)
pop.append(perturb(parent, depth, rng))
return best_result or {}
# ── Output ─────────────────────────────────────────────────────────────────────
def to_merkle_json(result: dict, swl_N: float) -> dict:
"""Convert search result to merkle_tree.json-compatible format."""
nodes_out = []
for n in result.get("nodes", []):
nodes_out.append({
"id": n["id"],
"x": round(n["x"], 4),
"y": round(n["y"], 4),
"z": round(n["z"], 4),
"F": round(swl_N * n["load_frac"], 4),
})
return {
"nodes": nodes_out,
"edges": result.get("edges", []),
"metadata": {
"description": "Semi-Jack n-space geometry search result",
"branch_angles": result.get("branch_angles"),
"az_offsets": result.get("az_offsets"),
"branching_factor": result.get("branching_factor"),
"tubule_radius_mm": result.get("tubule_radius"),
"fitness": result.get("fitness"),
"stress_mean_MPa": result.get("stress_mean_MPa"),
"swl_N": swl_N,
"material": "SLS_PA12",
}
}
def main():
ap = argparse.ArgumentParser(description="Semi-Jack n-space geometry search")
ap.add_argument("--depth", type=int, default=4, help="Tree depth (default 4)")
ap.add_argument("--pop", type=int, default=64, help="Population size (default 64)")
ap.add_argument("--iters", type=int, default=40, help="Iterations (default 40)")
ap.add_argument("--survivors", type=int, default=8, help="Survivors per iteration (default 8)")
ap.add_argument("--workers", type=int, default=4, help="Parallel workers (default 4)")
ap.add_argument("--swl", type=float, default=104.0,
help="SWL in Newtons (default 104N = 22mm cube at water density)")
ap.add_argument("--hlevel", type=float, default=6.0,
help="Height per tree level in mm (default 6mm → 24mm total for depth=4)")
ap.add_argument("--seed", type=int, default=42, help="RNG seed")
ap.add_argument("--no-atm", action="store_true",
help="Search in vacuum (no atmospheric pressure). Default: include atm.")
ap.add_argument("--out", default="5-Applications/out/sovereign_jenga/quantum_annealed/merkle_tree_nspace.json",
help="Output JSON path")
args = ap.parse_args()
with_atm = not args.no_atm
print(f"\n Semi-Jack n-space geometry search")
print(f" depth={args.depth} pop={args.pop} iters={args.iters}")
print(f" workers={args.workers} swl={args.swl}N ({args.swl/9.81:.2f}kg)")
print(f" atmosphere: {'101325 Pa (standard)' if with_atm else 'OFF (vacuum)'}")
print(f" search space: {2*args.depth + 2}D")
print()
best = search(
depth=args.depth,
population=args.pop,
iterations=args.iters,
survivors=args.survivors,
workers=args.workers,
swl_N=args.swl,
height_per_level=args.hlevel,
seed=args.seed,
with_atm=with_atm,
)
if not best:
print("ERROR: no result", file=sys.stderr)
sys.exit(1)
print(f"\n BEST GEOMETRY FOUND")
print(f" fitness : {best.get('fitness', '?'):.8f}")
print(f" branching factor : {best.get('branching_factor')}")
print(f" tubule radius : {best.get('tubule_radius', 0):.3f} mm")
print(f" branch angles : {[round(a,2) for a in best.get('branch_angles', [])]}")
print(f" az offsets : {[round(a,2) for a in best.get('az_offsets', [])]}")
print(f" stress mean : {best.get('stress_mean_MPa', 0):.3f} MPa")
print(f" nodes : {len(best.get('nodes', []))}")
print(f" edges : {len(best.get('edges', []))}")
out_path = Path(args.out)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_json = to_merkle_json(best, args.swl)
out_path.write_text(json.dumps(out_json, indent=2))
print(f"\n Saved → {out_path}")
print(f" Feed into constraint_model: ")
print(f" python 5-Applications/scripts/semi_jack_constraint_model.py \\")
print(f" --json {out_path} \\")
print(f" --radius {best.get('tubule_radius', 1.0):.3f} \\")
print(f" --swl {args.swl}")
print()
if __name__ == "__main__":
main()