Files
Atomizer/docs/plans/atomizer_fast_solver_technologies.md
Anto01 c061146a77 docs: Final documentation polish and consistency fixes
- Update README.md with LLM assistant section
- Create optimization_memory JSONL structure
- Move implementation plans from skills/modules to docs/plans
- Verify all imports work correctly

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-07 09:07:44 -05:00

34 KiB
Raw Permalink Blame History

Atomizer Future Development: Fast Solver Technologies

Document Information

  • Created: December 2024
  • Updated: December 28, 2025 (L-BFGS implementation)
  • Purpose: Technical reference for integrating fast solver technologies into Atomizer
  • Status: Partially Implemented (L-BFGS complete, ROM/meshfree planned)
  • Priority: Phase 2+ (after core parametric optimization is stable)

Executive Summary

This document captures research on alternative solver technologies that could dramatically accelerate Atomizer's optimization capabilities. Two main approaches are explored:

  1. Meshfree/Immersed FEA - Eliminates meshing bottleneck entirely (Intact Solutions, Gridap.jl)
  2. Reduced Order Models (ROM/POD) - Accelerates existing Nastran by 1000x for parametric problems

Recommendation:

  • Short-term: Implement ROM/POD for parametric optimization (2-4 weeks, 1000x speedup)
  • Medium-term: Prototype with Gridap.jl for topology optimization capability (2-3 months)
  • Long-term: Evaluate Intact licensing or productionize custom meshfree solver

Part 1: Meshfree / Immersed FEA Methods

1.1 The Problem with Traditional FEA

Traditional FEA requires body-fitted meshes that conform to geometry:

Traditional FEA Pipeline:
                                                
CAD Geometry → Clean/Heal → Defeature → Mesh Generation → Solve → Post-process
                                              ↑
                                         BOTTLENECK
                                         - 80% of time
                                         - Breaks on complex geometry
                                         - Must remesh for topology opt

Pain points:

  • Meshing complex geometry (lattices, AM parts) often fails
  • Small features require fine meshes → huge DOF count
  • CAD errors (gaps, overlaps) break meshing
  • Topology optimization requires remeshing every iteration
  • Automation breaks when meshing fails

1.2 The Meshfree Solution: Immersed Methods

Core idea: Don't mesh the geometry. Mesh the SPACE.

Immersed FEA Pipeline:

CAD Geometry → Convert to Implicit → Immerse in Grid → Solve → Post-process
              (any format works)     (regular grid)    (always works)

How it works:

Step 1: Background Grid              Step 2: Classify Cells
                                    
╔══╤══╤══╤══╤══╗                    ╔══╤══╤══╤══╤══╗
║  │  │  │  │  ║                    ║○ │○ │○ │○ │○ ║   ○ = Outside
╠══╪══╪══╪══╪══╣                    ╠══╪══╪══╪══╪══╣   ● = Inside
║  │  │  │  │  ║    +  Geometry  →  ║○ │◐ │● │◐ │○ ║   ◐ = Cut by boundary
╠══╪══╪══╪══╪══╣       (implicit)   ╠══╪══╪══╪══╪══╣
║  │  │  │  │  ║                    ║○ │◐ │● │◐ │○ ║
╚══╧══╧══╧══╧══╝                    ╚══╧══╧══╧══╧══╝

Step 3: Special Integration for Cut Cells

    Full cell: Standard Gauss quadrature
    
    Cut cell:  Moment-fitting quadrature
               (custom weights that integrate correctly
                over the irregular "inside" portion)

1.3 Implicit Geometry Representation

Signed Distance Function (SDF):

  • φ(x) < 0 → inside geometry
  • φ(x) > 0 → outside geometry
  • φ(x) = 0 → on boundary (the surface)
Example: Plate with hole

        φ > 0 (outside)
           ↓
 ─────────────────────
│    ·    │    ·      │
│  -0.3   │  +0.2     │     Numbers show signed distance
│─────────┼───────────│  ←  φ = 0 (surface)
│  -0.5   │  -0.1     │
│    ·    │    ·      │
 ─────────────────────
           ↑
        φ < 0 (inside)

Why implicit is powerful:

  • Boolean operations are trivial: union = min(φ_A, φ_B), intersection = max(φ_A, φ_B)
  • Works with ANY input: CAD, STL, CT scans, procedural geometry
  • Resolution-independent
  • VDB format stores efficiently (sparse, only near surface)

1.4 Key Technical Challenges

Challenge 1: Cut Cell Integration

When boundary cuts through a cell, standard Gauss quadrature doesn't work.

Solution: Moment Fitting

Find quadrature weights w_i such that polynomial integrals are exact:

∫ xᵃ yᵇ zᶜ dV = Σ wᵢ · xᵢᵃ yᵢᵇ zᵢᶜ   for all a+b+c ≤ p
Ω_cut           i

Challenge 2: Boundary Condition Enforcement

Grid doesn't have nodes ON the boundary.

Solution: Nitsche's Method (Weak Enforcement)

Add penalty term to variational form:

a(u,v) = ∫ ε(u):σ(v) dΩ 
       + ∫ β/h (u-g)·(v) dΓ      ← penalty term
       - ∫ σ(u)·n·v dΓ           ← consistency
       - ∫ σ(v)·n·u dΓ           ← symmetry

Challenge 3: Small Cut Cells (Ill-Conditioning)

Tiny slivers cause numerical instability.

Solution: Cell Aggregation (AgFEM)

Merge small cuts with neighboring full cells:

Before:                    After (aggregated):
┌───┬───┐                  ┌───┬───┐
│   │░░░│ ← tiny sliver    │   │▓▓▓│ ← merged with neighbor
└───┴───┘                  └───┴───┘

1.5 Intact Solutions: Commercial Implementation

Company: Intact Solutions, Inc. (Madison, WI) Technology: Immersed Method of Moments (IMM) Funding: DARPA, NSF, NIST History: 10+ years (started with Scan&Solve for Rhino)

Products:

Product Platform Use Case
Intact.Simulation for Grasshopper Rhino Design exploration
Intact.Simulation for nTop nTopology Lattice/AM parts
Intact.Simulation for Synera Synera Automotive
Intact.Simulation for Automation Headless/API Batch processing
PyIntact Python API Custom workflows
LevelOpt All platforms Topology optimization

Capabilities:

  • Physics: Linear static, thermal, modal
  • Geometry: STL, VDB, BREP, CT scans, G-code, hybrid
  • Materials: Isotropic, orthotropic
  • Multi-load case support
  • Assembly handling (bonded)

Performance (from their case study):

Steering knuckle topology optimization:
- 5,600 simulations
- 1,400 design iterations
- 4 load cases
- ~200,000 DOF each
- Total time: 45 hours
- Per simulation: ~30 seconds
- No meshing, no manual intervention

Limitations (inferred):

  • No nonlinear mechanics (yet?)
  • No dynamic response (frequency response, random vib)
  • Not certified for aerospace (Nastran still required for certification)
  • Proprietary (black box)

1.6 Open-Source Alternative: Gridap.jl

Language: Julia Repository: github.com/gridap/Gridap.jl, github.com/gridap/GridapEmbedded.jl License: MIT Maturity: Research+ (active development, used in publications)

Why Gridap:

  • Native AgFEM support (handles cut cells properly)
  • Level set geometry built-in
  • Clean mathematical API
  • Good performance (Julia compiles to native code)
  • Active community

Installation:

using Pkg
Pkg.add("Gridap")
Pkg.add("GridapEmbedded")

Basic Example:

using Gridap
using GridapEmbedded

# 1. Background mesh
domain = (-1, 1, -1, 1, -1, 1)
partition = (20, 20, 20)
bgmodel = CartesianDiscreteModel(domain, partition)

# 2. Implicit geometry (level set)
R = 0.7
geo = sphere(R)  # Built-in primitive

# 3. Cut the mesh
cutgeo = cut(bgmodel, geo)
Ω = Triangulation(cutgeo, PHYSICAL)  # Inside domain
Γ = EmbeddedBoundary(cutgeo)          # Boundary

# 4. FE space with aggregation
reffe = ReferenceFE(lagrangian, VectorValue{3,Float64}, 2)
V = TestFESpace(Ω, reffe, conformity=:H1)
U = TrialFESpace(V)

# 5. Weak form (linear elasticity)
const E, ν = 210e9, 0.3
const λ = E*ν / ((1+ν)*(1-2ν))
const μ = E / (2*(1+ν))

ε(u) = 0.5 * ((u) + transpose((u)))
σ(u) = λ * tr(ε(u)) * one(ε(u)) + 2*μ * ε(u)

a(u,v) = (ε(v)  σ(u)) * 
l(v) = (v  VectorValue(0,0,-1e6)) * 

# 6. Solve
op = AffineFEOperator(a, l, U, V)
uh = solve(op)

# 7. Export
writevtk(Ω, "results", cellfields=["u"=>uh])

Parameterized Solver for Optimization:

function solve_bracket(hole_radius, thickness, load)
    # Background mesh
    domain = (0, 1, 0, 0.5, 0, thickness)
    partition = (40, 20, 4)
    bgmodel = CartesianDiscreteModel(domain, partition)
    
    # Parameterized geometry
    function φ(x)
        d_box = max(max(-x[1], x[1]-1), max(-x[2], x[2]-0.5), max(-x[3], x[3]-thickness))
        d_hole = sqrt((x[1]-0.5)^2 + (x[2]-0.25)^2) - hole_radius
        return max(d_box, -d_hole)
    end
    
    geo = AnalyticalGeometry(φ)
    cutgeo = cut(bgmodel, geo)
    Ω = Triangulation(cutgeo, PHYSICAL)
    
    # ... FE setup and solve ...
    
    return (max_stress=σ_max, max_disp=u_max, mass=m)
end

# Use in optimization loop
for r in 0.05:0.01:0.2
    result = solve_bracket(r, 0.1, 1e6)
    println("r=$r, σ_max=$(result.max_stress/1e6) MPa")
end

1.7 Other Open-Source Options

Library Language Immersed Support Notes
FEniCSx Python/C++ Experimental Most mature FEM, but CutFEM needs work
MFEM C++ Manual Very fast, DOE-backed, shifted boundary
deal.II C++ Partial German powerhouse, some cut cell support
scikit-fem Python DIY Simple, hackable, but build from scratch

Recommendation: Start with Gridap.jl for prototyping. It has the best out-of-box immersed support.


Part 2: Reduced Order Models (ROM/POD)

2.1 Concept

Problem: FEA is slow because we solve huge systems (100k+ DOFs).

Insight: For parametric variations, solutions live in a low-dimensional subspace.

Solution: Find that subspace, solve there instead.

Full Order Model:               Reduced Order Model:

K u = F                         K_r u_r = F_r
                                
K: 100,000 × 100,000           K_r: 20 × 20
u: 100,000 unknowns            u_r: 20 unknowns

Solve time: minutes             Solve time: milliseconds

2.2 How POD Works

Step 1: Collect Snapshots (Offline)

Run N full FEA simulations with different parameters:

snapshots = []
for params in sample_parameter_space(n=100):
    result = nastran_solve(geometry(params), loads, bcs)
    snapshots.append(result.displacement_field)

U = np.array(snapshots)  # Shape: (n_samples, n_dofs)

Step 2: SVD to Find Basis (Offline)

from scipy.linalg import svd

U_centered = U - U.mean(axis=0)
Phi, S, Vt = svd(U_centered, full_matrices=False)

# Keep top k modes
k = 20  # Usually 10-50 captures 99%+ variance
basis = Vt[:k, :]  # Shape: (k, n_dofs)

The singular values decay rapidly - most "information" is in first few modes:

Singular Value Spectrum (typical):

    S │
  100 │██
      │██
   50 │██░░
      │██░░░░
   10 │██░░░░░░░░░░░░░░░░░░░░░░
      └─────────────────────────
           1   5   10  15  20  ...
                Mode index

Step 3: Project and Solve (Online - FAST)

def rom_solve(K, F, basis):
    """
    Solve in reduced space.
    
    Args:
        K: (n_dofs, n_dofs) stiffness matrix
        F: (n_dofs,) force vector
        basis: (k, n_dofs) POD modes
    
    Returns:
        u: (n_dofs,) approximate displacement
    """
    # Project to reduced space
    K_r = basis @ K @ basis.T  # (k, k)
    F_r = basis @ F            # (k,)
    
    # Solve tiny system
    u_r = np.linalg.solve(K_r, F_r)  # Instant!
    
    # Project back
    u = basis.T @ u_r  # (n_dofs,)
    
    return u

2.3 Complete ROM Implementation for Atomizer

# atomizer/solvers/rom_solver.py

import numpy as np
from scipy.linalg import svd
from scipy.sparse import issparse
from typing import List, Dict, Callable, Tuple
import pickle

class ROMSolver:
    """
    Reduced Order Model solver for fast parametric optimization.
    
    Usage:
        # Training (offline, one-time)
        rom = ROMSolver()
        rom.train(parameter_samples, nastran_solver, n_modes=20)
        rom.save("bracket_rom.pkl")
        
        # Inference (online, fast)
        rom = ROMSolver.load("bracket_rom.pkl")
        u, error_est = rom.solve(new_params)
    """
    
    def __init__(self, n_modes: int = 20):
        self.n_modes = n_modes
        self.basis = None           # (n_modes, n_dofs)
        self.singular_values = None # (n_modes,)
        self.mean_solution = None   # (n_dofs,)
        self.parameter_samples = None
        self.K_func = None          # Function to build stiffness
        self.F_func = None          # Function to build force
        
    def train(self, 
              parameter_samples: List[Dict],
              solver_func: Callable,
              K_func: Callable = None,
              F_func: Callable = None,
              verbose: bool = True):
        """
        Train ROM from full-order snapshots.
        
        Args:
            parameter_samples: List of parameter dictionaries
            solver_func: Function(params) -> displacement field (n_dofs,)
            K_func: Function(params) -> stiffness matrix (for online solve)
            F_func: Function(params) -> force vector (for online solve)
        """
        self.parameter_samples = parameter_samples
        self.K_func = K_func
        self.F_func = F_func
        
        # Collect snapshots
        if verbose:
            print(f"Collecting {len(parameter_samples)} snapshots...")
        
        snapshots = []
        for i, params in enumerate(parameter_samples):
            u = solver_func(params)
            snapshots.append(u)
            if verbose and (i+1) % 10 == 0:
                print(f"  Snapshot {i+1}/{len(parameter_samples)}")
        
        U = np.array(snapshots)  # (n_samples, n_dofs)
        
        # Center the data
        self.mean_solution = U.mean(axis=0)
        U_centered = U - self.mean_solution
        
        # SVD
        if verbose:
            print("Computing SVD...")
        
        _, S, Vt = svd(U_centered, full_matrices=False)
        
        # Keep top modes
        self.basis = Vt[:self.n_modes, :]
        self.singular_values = S[:self.n_modes]
        
        # Report energy captured
        total_energy = (S**2).sum()
        captured_energy = (self.singular_values**2).sum()
        if verbose:
            print(f"Captured {captured_energy/total_energy*100:.2f}% of variance "
                  f"with {self.n_modes} modes")
        
        return self
    
    def solve(self, params: Dict) -> Tuple[np.ndarray, float]:
        """
        Fast solve using ROM.
        
        Args:
            params: Parameter dictionary
            
        Returns:
            u: Approximate displacement field
            error_estimate: Estimated relative error
        """
        if self.K_func is None or self.F_func is None:
            raise ValueError("K_func and F_func required for online solve")
        
        # Build matrices for this parameter set
        K = self.K_func(params)
        F = self.F_func(params)
        
        # Convert sparse to dense if needed (for small reduced system)
        if issparse(K):
            # Project while sparse
            K_r = self.basis @ K @ self.basis.T
        else:
            K_r = self.basis @ K @ self.basis.T
        
        F_r = self.basis @ F
        
        # Solve reduced system
        u_r = np.linalg.solve(K_r, F_r)
        
        # Project back
        u = self.basis.T @ u_r + self.mean_solution
        
        # Error estimate (residual-based)
        residual = F - K @ u
        error_estimate = np.linalg.norm(residual) / np.linalg.norm(F)
        
        return u, error_estimate
    
    def solve_interpolated(self, params: Dict) -> np.ndarray:
        """
        Even faster: interpolate in coefficient space.
        Requires parameter_samples to be stored with coefficients.
        """
        # Find nearest neighbors in parameter space
        # Interpolate their ROM coefficients
        # This avoids even building K and F!
        raise NotImplementedError("TODO: Implement RBF interpolation")
    
    def estimate_error(self, u_rom: np.ndarray, K: np.ndarray, F: np.ndarray) -> float:
        """Residual-based error estimate."""
        residual = F - K @ u_rom
        return np.linalg.norm(residual) / np.linalg.norm(F)
    
    def should_fallback_to_full(self, error_estimate: float, threshold: float = 0.05) -> bool:
        """Check if ROM error is too high, suggesting full solve needed."""
        return error_estimate > threshold
    
    def save(self, filepath: str):
        """Save trained ROM to file."""
        data = {
            'n_modes': self.n_modes,
            'basis': self.basis,
            'singular_values': self.singular_values,
            'mean_solution': self.mean_solution,
        }
        with open(filepath, 'wb') as f:
            pickle.dump(data, f)
    
    @classmethod
    def load(cls, filepath: str) -> 'ROMSolver':
        """Load trained ROM from file."""
        with open(filepath, 'rb') as f:
            data = pickle.load(f)
        
        rom = cls(n_modes=data['n_modes'])
        rom.basis = data['basis']
        rom.singular_values = data['singular_values']
        rom.mean_solution = data['mean_solution']
        
        return rom


class AdaptiveROMSolver(ROMSolver):
    """
    ROM with automatic fallback to full solver when error is high.
    """
    
    def __init__(self, n_modes: int = 20, error_threshold: float = 0.05):
        super().__init__(n_modes)
        self.error_threshold = error_threshold
        self.full_solver = None
        
    def set_full_solver(self, solver_func: Callable):
        """Set the full-order solver for fallback."""
        self.full_solver = solver_func
        
    def solve_adaptive(self, params: Dict) -> Tuple[np.ndarray, float, bool]:
        """
        Solve with automatic fallback.
        
        Returns:
            u: Displacement field
            error: Error estimate (or 0 if full solve)
            used_full: Whether full solver was used
        """
        # Try ROM first
        u_rom, error = self.solve(params)
        
        if error < self.error_threshold:
            return u_rom, error, False
        
        # Fallback to full
        if self.full_solver is not None:
            u_full = self.full_solver(params)
            return u_full, 0.0, True
        else:
            # Return ROM result with warning
            print(f"Warning: ROM error {error:.3f} exceeds threshold, "
                  f"but no full solver available")
            return u_rom, error, False

2.4 Integration with Atomizer Optimization Loop

# atomizer/optimization/rom_optimization_runner.py

from atomizer.solvers.rom_solver import AdaptiveROMSolver
from atomizer.solvers.nastran_solver import NastranSolver
import optuna

class ROMOptimizationRunner:
    """
    Optimization using ROM for speed, Nastran for validation.
    """
    
    def __init__(self, problem_config: dict):
        self.config = problem_config
        self.rom = None
        self.nastran = NastranSolver()
        
    def train_rom(self, n_samples: int = 100, n_modes: int = 20):
        """Train ROM on problem-specific data."""
        
        # Sample parameter space
        samples = self._latin_hypercube_sample(n_samples)
        
        # Run Nastran for each sample
        def nastran_solve(params):
            result = self.nastran.solve(
                geometry=self._build_geometry(params),
                loads=self.config['loads'],
                bcs=self.config['bcs']
            )
            return result['displacement_field']
        
        # Train ROM
        self.rom = AdaptiveROMSolver(n_modes=n_modes)
        self.rom.train(samples, nastran_solve)
        self.rom.set_full_solver(nastran_solve)
        
        return self
    
    def optimize(self, n_trials: int = 500):
        """Run optimization using ROM."""
        
        def objective(trial):
            # Sample parameters
            params = {}
            for name, bounds in self.config['parameters'].items():
                params[name] = trial.suggest_float(name, bounds[0], bounds[1])
            
            # Fast ROM solve
            u, error, used_full = self.rom.solve_adaptive(params)
            
            # Compute objective (e.g., max stress)
            stress = self._compute_stress(u)
            max_stress = stress.max()
            
            # Log
            trial.set_user_attr('rom_error', error)
            trial.set_user_attr('used_full_solver', used_full)
            
            return max_stress
        
        # Run optimization
        study = optuna.create_study(direction='minimize')
        study.optimize(objective, n_trials=n_trials)
        
        # Validate best designs with full Nastran
        return self._validate_top_designs(study, n_top=10)
    
    def _validate_top_designs(self, study, n_top: int = 10):
        """Run full Nastran on top candidates."""
        
        top_trials = sorted(study.trials, key=lambda t: t.value)[:n_top]
        
        validated = []
        for trial in top_trials:
            result = self.nastran.solve(
                geometry=self._build_geometry(trial.params),
                loads=self.config['loads'],
                bcs=self.config['bcs']
            )
            validated.append({
                'params': trial.params,
                'rom_prediction': trial.value,
                'nastran_result': result['max_stress'],
                'error': abs(trial.value - result['max_stress']) / result['max_stress']
            })
        
        return validated

2.5 When ROM Works Well vs. Poorly

WORKS GREAT:                              WORKS POORLY:

✓ Parametric variations                   ✗ Topology changes
  (thickness, radii, positions)             (holes appear/disappear)

✓ Smooth parameter dependence             ✗ Discontinuous behavior
                                            (contact, buckling)

✓ Similar geometry family                 ✗ Wildly different geometries

✓ Linear problems                         ✗ Highly nonlinear

✓ Fixed mesh topology                     ✗ Mesh changes between solves

For Atomizer's current parametric optimization: ROM is ideal.


Part 3: Comparison of Approaches

3.1 Speed Comparison

Method Typical Solve Time Speedup vs Nastran Training Cost
Nastran (baseline) 10 min 1x None
Intact IMM 30 sec 20x None
ROM/POD 10 ms 60,000x 100 Nastran runs
Neural Surrogate (MLP) 10 ms 60,000x 50+ FEA runs
L-BFGS on Surrogate <1 sec (20-50 evals) 100-1000x vs TPE Trained surrogate

3.1.1 L-BFGS Gradient Optimization (IMPLEMENTED)

Status: ✓ Implemented in optimization_engine/gradient_optimizer.py

L-BFGS exploits the differentiability of trained MLP surrogates for ultra-fast local refinement:

from optimization_engine.gradient_optimizer import run_lbfgs_polish

# Polish top candidates from surrogate search
results = run_lbfgs_polish(study_dir, n_starts=20, n_iterations=100)

Performance:

Method Evaluations to Converge Time
TPE (surrogate) 200-500 ~30 min
CMA-ES (surrogate) 100-300 ~15 min
L-BFGS (surrogate) 20-50 <1 sec

When to use: After Turbo mode identifies promising regions, before FEA validation.

Full documentation: docs/protocols/system/SYS_14_NEURAL_ACCELERATION.md (Section: L-BFGS Gradient Optimizer)

3.2 Capability Comparison

Capability Nastran Intact ROM Neural
Parametric opt ✓ (slow) ✓✓ ✓✓
Topology opt ✓✓ ?
New geometry ✗ (retrain) ✗ (retrain)
Certification ✓✓
Nonlinear ? Limited Limited
Dynamic ? Limited Limited

3.3 Implementation Effort

Approach Effort Risk Payoff
ROM/POD 2-4 weeks Low Immediate 1000x speedup
Gridap prototype 2-3 months Medium Topology opt capability
Intact license $$ + 2-4 weeks Low Production meshfree
AtomizerField Ongoing Medium Field predictions

4.1 Three-Tier Fidelity Stack

┌─────────────────────────────────────────────────────────────────────────┐
│                      ATOMIZER SOLVER STACK                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│  TIER 1: AtomizerField / ROM  (Screening)                               │
│  ──────────────────────────────────────────                              │
│  Speed:     ~10 ms per evaluation                                        │
│  Accuracy:  ~90-95%                                                      │
│  Use case:  Design space exploration, optimization inner loop            │
│  Output:    Promising candidates                                         │
│                                                                          │
│  TIER 2: Intact IMM / Gridap  (Refinement)  [FUTURE]                    │
│  ──────────────────────────────────────────────────                      │
│  Speed:     ~30 sec per evaluation                                       │
│  Accuracy:  ~98%                                                         │
│  Use case:  Topology optimization, complex geometry                      │
│  Output:    Optimized designs                                            │
│                                                                          │
│  TIER 3: NX Nastran  (Validation)                                       │
│  ────────────────────────────────                                        │
│  Speed:     ~10 min per evaluation                                       │
│  Accuracy:  100% (reference)                                             │
│  Use case:  Final validation, certification                              │
│  Output:    Certified results                                            │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Workflow:

  Design Space ──▶ Tier 1 ──▶ Candidates ──▶ Tier 2 ──▶ Best ──▶ Tier 3
  (1M points)      (1 sec)    (1000 pts)    (8 hrs)    (10)    (2 hrs)
                                               │
                                               └── Skip if parametric only

**Current Implemented Workflow (Turbo + L-BFGS):**

┌─────────────────────────────────────────────────────────────────────────┐ │ 1. FEA Exploration │ 50-100 Nastran trials │ │ └─► Train MLP surrogate on FEA data │ ├─────────────────────────────────────────────────────────────────────────┤ │ 2. Turbo Mode │ 5000+ NN predictions (~10ms each) │ │ └─► Find promising design regions │ ├─────────────────────────────────────────────────────────────────────────┤ │ 3. L-BFGS Polish │ 20 multi-start runs (<1 sec total) [NEW] │ │ └─► Precise local optima via gradient descent │ ├─────────────────────────────────────────────────────────────────────────┤ │ 4. FEA Validation │ Top 3-5 candidates verified with Nastran │ │ └─► Final certified results │ └─────────────────────────────────────────────────────────────────────────┘


### 4.2 Implementation Phases

PHASE 1: NOW (Q4 2024 - Q1 2025) ──────────────────────────────── [✓] Core optimization engine (Optuna/CMA-ES/NSGA-II) [✓] NX/Nastran integration [✓] Dashboard [✓] MLP Surrogate + Turbo Mode [✓] L-BFGS Gradient Optimizer ← IMPLEMENTED 2025-12-28 [ ] ROM/POD implementation [ ] AtomizerField training pipeline

PHASE 2: MEDIUM-TERM (Q2-Q3 2025) ──────────────────────────────────── [ ] Gridap.jl prototype [ ] Evaluate Intact licensing [ ] Topology optimization capability [ ] Multi-fidelity optimization

PHASE 3: FUTURE (Q4 2025+) ────────────────────────── [ ] Production meshfree solver (Intact or custom) [ ] Full AtomizerField deployment [ ] Cloud/HPC scaling [ ] Certification workflow integration


---

## Part 5: Key Resources

### 5.1 Intact Solutions
- Website: https://intact-solutions.com
- PyIntact docs: https://intact-solutions.com/intact_docs/PyIntact.html
- Contact for pricing: info@intact-solutions.com

### 5.2 Gridap.jl
- Main repo: https://github.com/gridap/Gridap.jl
- Embedded: https://github.com/gridap/GridapEmbedded.jl
- Tutorials: https://gridap.github.io/Tutorials/stable/
- Paper: "Gridap: An extensible Finite Element toolbox in Julia" (2020)

### 5.3 ROM/POD References
- Benner et al., "Model Reduction and Approximation" (2017)
- Quarteroni et al., "Reduced Basis Methods for PDEs" (2016)
- pyMOR library: https://pymor.org (Python model order reduction)

### 5.4 Immersed FEM Theory
- Burman et al., "CutFEM: Discretizing geometry and PDEs" (2015)
- Schillinger & Ruess, "The Finite Cell Method: A Review" (2015)
- Main & Scovazzi, "The Shifted Boundary Method" (2018)

---

## Appendix A: Quick Reference - Immersed FEM Math

### Weak Form with Nitsche BCs

Standard elasticity:

Find u ∈ V such that: a(u,v) = l(v) ∀v ∈ V

a(u,v) = ∫_Ω ε(u):C:ε(v) dΩ l(v) = ∫_Ω f·v dΩ + ∫_Γ_N t·v dΓ


With Nitsche (weak Dirichlet on Γ_D):

a(u,v) = ∫_Ω ε(u):C:ε(v) dΩ - ∫_Γ_D (σ(u)·n)·v dΓ # Consistency - ∫_Γ_D (σ(v)·n)·u dΓ # Symmetry
+ ∫_Γ_D (β/h) u·v dΓ # Penalty

l(v) = ∫_Ω f·v dΩ + ∫_Γ_N t·v dΓ - ∫_Γ_D (σ(v)·n)·g dΓ # Consistency (RHS) + ∫_Γ_D (β/h) g·v dΓ # Penalty (RHS)

where β ~ E (penalty parameter), h = cell size, g = prescribed displacement


### Ghost Penalty Stabilization

For small cut cells, add:

j(u,v) = Σ_F γ h² ∫_F [∂ⁿu/∂nⁿ]·[∂ⁿv/∂nⁿ] dF

where F = interior faces, [·] = jump across face, γ = stabilization parameter


---

## Appendix B: Julia/Gridap Cheat Sheet

```julia
# Installation
using Pkg
Pkg.add(["Gridap", "GridapEmbedded", "GridapGmsh"])

# Basic imports
using Gridap
using GridapEmbedded
using GridapGmsh  # For external meshes

# Geometry primitives
sphere(r)                    # Sphere of radius r
disk(r)                      # 2D disk
cylinder(r, h)               # Cylinder
box(corner1, corner2)        # Box

# Boolean operations
union(geo1, geo2)
intersect(geo1, geo2)  
setdiff(geo1, geo2)          # geo1 - geo2

# Custom level set
geo = AnalyticalGeometry(x -> my_sdf(x))

# Cutting
cutgeo = cut(bgmodel, geo)
Ω = Triangulation(cutgeo, PHYSICAL)
Γ = EmbeddedBoundary(cutgeo)

# FE spaces
reffe = ReferenceFE(lagrangian, Float64, 2)           # Scalar, quadratic
reffe = ReferenceFE(lagrangian, VectorValue{3,Float64}, 2)  # 3D vector

# Measures
dΩ = Measure(Ω, 4)           # Quadrature degree 4
dΓ = Measure(Γ, 4)

# Weak form
a(u,v) = ∫(∇(u)⋅∇(v)) * dΩ
l(v) = ∫(f*v) * dΩ

# Solve
op = AffineFEOperator(a, l, U, V)
uh = solve(op)

# Export
writevtk(Ω, "output", cellfields=["u"=>uh])

Appendix C: ROM/POD Implementation Checklist

□ Data Collection
  □ Define parameter space bounds
  □ Latin hypercube sampling (or similar)
  □ Run Nastran for each sample
  □ Store displacement fields consistently (same mesh!)

□ Basis Construction
  □ Center snapshot matrix
  □ SVD decomposition
  □ Select number of modes (energy criterion: 99%+)
  □ Store basis and mean

□ Online Solver
  □ Stiffness matrix assembly for new params
  □ Force vector assembly for new params
  □ Project to reduced space
  □ Solve reduced system
  □ Project back to full space

□ Error Estimation
  □ Residual-based estimate
  □ Fallback threshold selection
  □ Adaptive solver wrapper

□ Integration
  □ Save/load trained ROM
  □ Plug into optimization loop
  □ Validation against full solver

Document maintained by: Atomizer Development Team Last updated: December 2024