Files
Atomizer/docs/plans/UNIFIED_CONFIGURATION_ARCHITECTURE.md
Anto01 ea437d360e docs: Major documentation overhaul - restructure folders, update tagline, add Getting Started guide
- Restructure docs/ folder (remove numeric prefixes):
  - 04_USER_GUIDES -> guides/
  - 05_API_REFERENCE -> api/
  - 06_PHYSICS -> physics/
  - 07_DEVELOPMENT -> development/
  - 08_ARCHIVE -> archive/
  - 09_DIAGRAMS -> diagrams/

- Replace tagline 'Talk, don't click' with 'LLM-driven optimization framework' in 9 files

- Create comprehensive docs/GETTING_STARTED.md:
  - Prerequisites and quick setup
  - Project structure overview
  - First study tutorial (Claude or manual)
  - Dashboard usage guide
  - Neural acceleration introduction

- Rewrite docs/00_INDEX.md with correct paths and modern structure

- Archive obsolete files:
  - 01_PROTOCOLS.md -> archive/historical/01_PROTOCOLS_legacy.md
  - 03_GETTING_STARTED.md -> archive/historical/
  - ATOMIZER_PODCAST_BRIEFING.md -> archive/marketing/

- Update timestamps to 2026-01-20 across all key files

- Update .gitignore to exclude docs/generated/

- Version bump: ATOMIZER_CONTEXT v1.8 -> v2.0
2026-01-20 10:03:45 -05:00

1698 lines
58 KiB
Markdown

# Atomizer Unified Configuration Architecture
## Master Design Document
**Version**: 1.0
**Date**: January 2026
**Status**: Proposed Architecture
**Author**: Atomizer Architecture Review
---
## Executive Summary
This document presents a comprehensive analysis of Atomizer's current configuration architecture and proposes a **Unified Configuration System** that establishes a single source of truth for optimization studies. The new architecture enables:
1. **Single Source of Truth**: One canonical JSON schema (`AtomizerSpec`) used everywhere
2. **Bidirectional Sync**: Canvas ↔ Config ↔ Backend with zero data loss
3. **Intelligent Manipulation**: Claude can dynamically modify configs, add custom functions, and validate changes
4. **Real-time Collaboration**: WebSocket-driven updates between Dashboard, Canvas, and Claude Assistant
---
## Table of Contents
1. [Current State Analysis](#1-current-state-analysis)
2. [Problem Statement](#2-problem-statement)
3. [Proposed Architecture](#3-proposed-architecture)
4. [AtomizerSpec Schema](#4-atomizerspec-schema)
5. [Component Architecture](#5-component-architecture)
6. [Intelligent Assistant Integration](#6-intelligent-assistant-integration)
7. [API Design](#7-api-design)
8. [Migration Strategy](#8-migration-strategy)
9. [Implementation Roadmap](#9-implementation-roadmap)
---
## 1. Current State Analysis
### 1.1 Architecture Overview
The current system has **four distinct configuration representations** that don't fully align:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ CURRENT ARCHITECTURE (FRAGMENTED) │
└─────────────────────────────────────────────────────────────────────────────┘
Canvas UI Backend Optimization
┌────────────────┐ ┌────────────────┐ ┌────────────────┐
│ ReactFlow │ │ FastAPI │ │ Python Engine │
│ Nodes/Edges │ │ Routes │ │ ConfigManager │
│ │ │ │ │ │
│ ┌──────────┐ │ │ ┌──────────┐ │ │ ┌──────────┐ │
│ │ Canvas │──┼──LOSSY──┼─▶│ Intent │──┼──LOSSY──┼─▶│ Config │ │
│ │ State │ │ │ │ JSON │ │ │ │ .json │ │
│ └──────────┘ │ │ └──────────┘ │ │ └──────────┘ │
│ ▲ │ │ │ │ │ │ │
│ │ LOSSY │ │ ▼ │ │ ▼ │
│ │ │ │ ┌──────────┐ │ │ ┌──────────┐ │
│ Load from │◀─LOSSY──┼──│ Claude │ │ │ │ Optuna │ │
│ config │ │ │ Assistant│ │ │ │ Database │ │
└────────────────┘ └────────────────┘ └────────────────┘
```
### 1.2 Current Config Formats (4+ Variants)
| Variant | Used By | Key Differences |
|---------|---------|-----------------|
| **Mirror/Zernike** | m1_mirror studies | `extractor_config`, `zernike_settings`, subcases |
| **Drone/Structural** | drone_gimbal | `extraction.{action, domain, params}`, `bounds[]` |
| **Canvas Intent** | Canvas UI | Simplified, loses metadata |
| **Legacy Schema** | optimization_config_schema.json | `parameter`, `bounds[]`, `goal` |
### 1.3 Field Naming Chaos
| Concept | Canvas Intent | Mirror Config | Drone Config | Schema |
|---------|---------------|---------------|--------------|--------|
| DV bounds | `min`, `max` | `min`, `max` | `bounds: [min, max]` | `bounds: [min, max]` |
| DV name | `name` | `expression_name` | `parameter` | `parameter` |
| Objective dir | `direction` | `direction` | `goal` | `goal` |
| Extractor | `extractor: "E5"` | `extractor_config: {}` | `extraction: {}` | `extraction: {}` |
| Trials | `max_trials` | `n_trials` | `n_trials` | `n_trials` |
### 1.4 Data Flow Problems
```
PROBLEM 1: Information Loss During Canvas → Config Conversion
═══════════════════════════════════════════════════════════════
Canvas Node: Intent: Config:
┌─────────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Extractor E8 │ │ extractors: [{ │ │ objectives: [{ │
│ - nModes: 40 │───────▶│ id: "E8", │───────▶│ extractor: │
│ - innerRadius: 58 │ LOST │ name: "..." │ LOST │ "E8" │
│ - subcases: [1,2,3] │───────▶│ }] │───────▶│ }] │
│ - filterLowOrders: 6│ │ │ │ │
└─────────────────────┘ └─────────────────┘ └─────────────────┘
│ │
│ MISSING: │ MISSING:
│ - nModes │ - All config
│ - innerRadius │ - zernike_settings
│ - subcases │ - extraction_method
│ - filterLowOrders │
PROBLEM 2: Config → Canvas Loading is Incomplete
═══════════════════════════════════════════════════════════════
Real Config (m1_mirror): Canvas Display:
┌───────────────────────────────┐ ┌─────────────────────────┐
│ design_variables: [ │ │ 11 DesignVar nodes │ ✓
│ { name, min, max, ... } │ │ │
│ ] │ │ │
│ │ │ │
│ objectives: [ │ │ 3 Objective nodes │ ✓
│ { extractor_config: { │ │ (but extractor_config │
│ target_subcase: "3", │ │ NOT displayed) │ ✗
│ metric: "rel_filtered_rms"│ │ │
│ }} │ │ │
│ ] │ │ │
│ │ │ │
│ hard_constraints: [ │ │ Converted to objectives │ ✗
│ { name, limit, penalty } │ │ (loses constraint type) │
│ ] │ │ │
│ │ │ │
│ sat_settings: { │ │ NOT LOADED AT ALL │ ✗
│ n_ensemble_models: 10, │ │ │
│ hidden_dims: [256, 128] │ │ │
│ } │ │ │
└───────────────────────────────┘ └─────────────────────────┘
PROBLEM 3: No Bidirectional Sync
═══════════════════════════════════════════════════════════════
User edits config.json manually
Canvas doesn't know about change
User refreshes canvas → loses their canvas-only edits
Conflict between what config says and what canvas shows
```
### 1.5 Missing Backend Endpoints
| Required Endpoint | Status | Impact |
|-------------------|--------|--------|
| `POST /studies/create-from-intent` | Missing | Canvas can't create studies |
| `POST /studies/{id}/validate-config` | Missing | No schema validation |
| `PUT /studies/{id}/config` (atomic) | Partial | No conflict detection |
| `POST /canvas/apply-modification` | Missing | Claude can't modify canvas |
| `WebSocket /studies/{id}/sync` | Missing | No real-time updates |
---
## 2. Problem Statement
### 2.1 Core Issues
1. **No Single Source of Truth**: Multiple config formats mean the same study looks different depending on where you view it
2. **Lossy Conversions**: Converting Canvas → Intent → Config loses information; converting Config → Canvas is incomplete
3. **No Real-time Sync**: Changes in one place (config file, canvas, Claude) don't propagate to others
4. **Claude Can't Modify Canvas Directly**: MCP tools return JSON but nothing applies changes to the frontend
5. **No Intelligent Extensibility**: User can't ask Claude to "add a custom extraction function" because there's no plugin system
### 2.2 User Pain Points
| Scenario | Current Experience | Desired Experience |
|----------|-------------------|-------------------|
| "Load my existing study into canvas" | Partial load, loses extractor settings | Complete bidirectional load |
| "Claude, change the mass objective weight to 5" | Claude returns JSON, user must manually apply | Canvas updates in real-time |
| "I need a custom Zernike RMS calculation" | Must edit Python code directly | Claude adds function, canvas shows new node |
| "Show me what the config looks like" | Multiple representations exist | One canonical view everywhere |
---
## 3. Proposed Architecture
### 3.1 Unified Architecture Overview
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ PROPOSED ARCHITECTURE (UNIFIED) │
└─────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────┐
│ AtomizerSpec │
│ (Single Source │
│ of Truth) │
│ │
│ atomizer_spec.json │
└──────────┬──────────┘
┌──────────────────────────┼──────────────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Canvas UI │ │ Backend API │ │ Claude Assistant│
│ │ │ │ │ │
│ ReactFlow + │◀─────▶│ FastAPI + │◀─────▶│ MCP Tools + │
│ SpecRenderer │ WS │ SpecManager │ WS │ SpecModifier │
│ │ │ │ │ │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
│ WebSocket Real-time Sync Bus │
└──────────────────────────┴──────────────────────────┘
┌─────────────────────┐
│ Optimization │
│ Engine │
│ │
│ Uses AtomizerSpec │
│ directly │
└─────────────────────┘
```
### 3.2 Key Principles
1. **AtomizerSpec is THE config**: One JSON schema, versioned, validated everywhere
2. **No Intermediate Formats**: Canvas renders AtomizerSpec directly; no "Intent" conversion
3. **Changes Flow Through Backend**: All modifications go through SpecManager
4. **WebSocket Sync**: All clients receive real-time updates
5. **Extensible Functions**: Custom extractors/functions defined IN the spec
### 3.3 Data Flow (New)
```
AtomizerSpec (atomizer_spec.json)
│ Read by all
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
Canvas Renderer Backend Loader Claude Reader
│ │ │
│ User edits node │ │ "Change weight"
│ │ │
▼ ▼ ▼
PATCH /spec/nodes/{id} (validates) MCP: spec_modify
│ │ │
└─────────────────────────┼─────────────────────────┘
SpecManager
┌─────────────────────────┐
│ 1. Validate change │
│ 2. Apply to spec │
│ 3. Persist to disk │
│ 4. Broadcast via WS │
└─────────────────────────┘
│ WebSocket: spec_updated
┌─────────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
Canvas re-renders Other clients Claude sees new state
```
---
## 4. AtomizerSpec Schema
### 4.1 Schema Design Philosophy
- **Self-describing**: Schema version and capabilities in the file
- **Complete**: ALL information needed for optimization, canvas, and reporting
- **Extensible**: Custom functions, extractors, and plugins declared in-spec
- **Validated**: JSON Schema validation at every boundary
### 4.2 AtomizerSpec v2.0 Schema
```json
{
"$schema": "https://atomizer.io/schemas/atomizer_spec_v2.json",
"$id": "atomizer_spec",
"meta": {
"version": "2.0",
"created": "2026-01-17T12:00:00Z",
"modified": "2026-01-17T14:30:00Z",
"created_by": "canvas",
"modified_by": "claude",
"study_name": "m1_mirror_optimization_v15",
"description": "Multi-objective mirror optimization with Zernike WFE",
"tags": ["mirror", "zernike", "multi-objective"]
},
"model": {
"nx_part": {
"path": "C:/Studies/m1_mirror/model/M1_Mirror.prt",
"hash": "sha256:abc123...",
"idealized_part": "M1_Mirror_fem1_i.prt"
},
"fem": {
"path": "model/M1_Mirror_fem1.fem",
"element_count": 45000,
"node_count": 12000
},
"sim": {
"path": "model/M1_Mirror_sim1.sim",
"solver": "nastran",
"solution_type": "SOL101",
"subcases": [
{ "id": 1, "name": "Gravity 0deg", "type": "static" },
{ "id": 2, "name": "Gravity 20deg", "type": "static" },
{ "id": 3, "name": "Gravity 40deg", "type": "static" }
]
}
},
"design_variables": [
{
"id": "dv_001",
"name": "Lateral Rib Count",
"expression_name": "n_lateral_ribs",
"type": "integer",
"bounds": { "min": 6, "max": 12 },
"baseline": 9,
"units": "count",
"enabled": true,
"description": "Number of lateral support ribs",
"canvas_position": { "x": 50, "y": 100 }
},
{
"id": "dv_002",
"name": "Facesheet Thickness",
"expression_name": "facesheet_t",
"type": "continuous",
"bounds": { "min": 2.0, "max": 8.0 },
"baseline": 4.5,
"units": "mm",
"enabled": true,
"step": 0.1,
"canvas_position": { "x": 50, "y": 200 }
}
],
"extractors": [
{
"id": "ext_001",
"name": "Zernike WFE Extractor",
"type": "zernike_opd",
"builtin": true,
"config": {
"inner_radius_mm": 58.0,
"outer_radius_mm": 330.0,
"n_modes": 40,
"filter_low_orders": 6,
"displacement_unit": "mm",
"reference_subcase": 1
},
"outputs": [
{ "name": "wfe_rms_20deg", "subcase": 2, "metric": "filtered_rms_nm" },
{ "name": "wfe_rms_40deg", "subcase": 3, "metric": "filtered_rms_nm" },
{ "name": "wfe_p2v_40deg", "subcase": 3, "metric": "pv_nm" }
],
"canvas_position": { "x": 740, "y": 100 }
},
{
"id": "ext_002",
"name": "Mass Extractor",
"type": "mass",
"builtin": true,
"config": {
"source": "expression",
"expression_name": "total_mass_kg"
},
"outputs": [
{ "name": "mass_kg", "metric": "total" }
],
"canvas_position": { "x": 740, "y": 300 }
},
{
"id": "ext_003",
"name": "Custom Manufacturability Score",
"type": "custom_function",
"builtin": false,
"function": {
"name": "calc_manufacturability",
"module": "custom_extractors.manufacturability",
"signature": "(design_vars: dict, fem_results: dict) -> float",
"source_code": "def calc_manufacturability(design_vars, fem_results):\n # Custom logic here\n rib_count = design_vars['n_lateral_ribs']\n thickness = design_vars['facesheet_t']\n score = 100 - (rib_count * 5) - (10 - thickness) * 3\n return max(0, min(100, score))"
},
"outputs": [
{ "name": "mfg_score", "metric": "score" }
],
"canvas_position": { "x": 740, "y": 500 }
}
],
"objectives": [
{
"id": "obj_001",
"name": "Minimize WFE at 40deg",
"direction": "minimize",
"weight": 5.0,
"source": {
"extractor_id": "ext_001",
"output_name": "wfe_rms_40deg"
},
"target": 5.0,
"units": "nm",
"canvas_position": { "x": 1020, "y": 100 }
},
{
"id": "obj_002",
"name": "Minimize Mass",
"direction": "minimize",
"weight": 1.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"target": 100.0,
"units": "kg",
"canvas_position": { "x": 1020, "y": 200 }
}
],
"constraints": [
{
"id": "con_001",
"name": "Max Mass Limit",
"type": "hard",
"operator": "<=",
"threshold": 120.0,
"source": {
"extractor_id": "ext_002",
"output_name": "mass_kg"
},
"penalty_config": {
"method": "quadratic",
"weight": 1000.0,
"margin": 5.0
},
"canvas_position": { "x": 1020, "y": 400 }
},
{
"id": "con_002",
"name": "Minimum Manufacturability",
"type": "soft",
"operator": ">=",
"threshold": 60.0,
"source": {
"extractor_id": "ext_003",
"output_name": "mfg_score"
},
"penalty_config": {
"method": "linear",
"weight": 10.0
},
"canvas_position": { "x": 1020, "y": 500 }
}
],
"optimization": {
"algorithm": {
"type": "NSGA-II",
"config": {
"population_size": 50,
"n_generations": 100,
"mutation_prob": null,
"crossover_prob": 0.9,
"seed": 42
}
},
"budget": {
"max_trials": 500,
"max_time_hours": 48,
"convergence_patience": 50
},
"surrogate": {
"enabled": true,
"type": "ensemble",
"config": {
"n_models": 10,
"architecture": [256, 128, 64],
"train_every_n_trials": 20,
"min_training_samples": 30,
"acquisition_candidates": 10000,
"fea_validations_per_round": 5
}
},
"canvas_position": { "x": 1300, "y": 150 }
},
"workflow": {
"stages": [
{
"id": "stage_exploration",
"name": "Design Space Exploration",
"algorithm": "RandomSearch",
"trials": 30,
"purpose": "Build initial training data"
},
{
"id": "stage_optimization",
"name": "Surrogate-Assisted Optimization",
"algorithm": "SAT_v3",
"trials": 470,
"purpose": "Efficient optimization with neural acceleration"
}
],
"transitions": [
{
"from": "stage_exploration",
"to": "stage_optimization",
"condition": "trial_count >= 30"
}
]
},
"reporting": {
"auto_report": true,
"report_triggers": ["new_pareto", "every_50_trials", "convergence"],
"insights": [
{
"type": "zernike_visualization",
"for_trials": "pareto_front",
"config": { "include_html": true }
},
{
"type": "convergence_plot",
"config": { "show_pareto_evolution": true }
}
]
},
"canvas": {
"layout_version": "2.0",
"viewport": { "x": 0, "y": 0, "zoom": 1.0 },
"edges": [
{ "source": "dv_001", "target": "model" },
{ "source": "dv_002", "target": "model" },
{ "source": "model", "target": "solver" },
{ "source": "solver", "target": "ext_001" },
{ "source": "solver", "target": "ext_002" },
{ "source": "ext_001", "target": "obj_001" },
{ "source": "ext_002", "target": "obj_002" },
{ "source": "ext_002", "target": "con_001" },
{ "source": "obj_001", "target": "optimization" },
{ "source": "obj_002", "target": "optimization" },
{ "source": "con_001", "target": "optimization" }
],
"groups": [
{ "id": "grp_inputs", "name": "Design Inputs", "node_ids": ["dv_001", "dv_002"] },
{ "id": "grp_physics", "name": "Physics Extraction", "node_ids": ["ext_001", "ext_002"] }
]
}
}
```
### 4.3 Schema Versioning
| Version | Status | Key Changes |
|---------|--------|-------------|
| 1.0 | Legacy | Original `optimization_config.json` |
| 2.0 | **Proposed** | Unified spec with canvas, custom functions, workflow |
| 2.1 | Future | Multi-fidelity, parallel execution |
### 4.4 JSON Schema Definition
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://atomizer.io/schemas/atomizer_spec_v2.json",
"title": "AtomizerSpec v2.0",
"type": "object",
"required": ["meta", "model", "design_variables", "extractors", "objectives", "optimization"],
"properties": {
"meta": {
"type": "object",
"required": ["version", "study_name"],
"properties": {
"version": { "type": "string", "pattern": "^2\\.\\d+$" },
"study_name": { "type": "string", "pattern": "^[a-z0-9_]+$" },
"created": { "type": "string", "format": "date-time" },
"modified": { "type": "string", "format": "date-time" },
"created_by": { "enum": ["canvas", "claude", "api", "migration"] },
"modified_by": { "type": "string" },
"description": { "type": "string" },
"tags": { "type": "array", "items": { "type": "string" } }
}
},
"design_variables": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/design_variable"
}
},
"extractors": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/extractor"
}
},
"objectives": {
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/definitions/objective"
}
}
},
"definitions": {
"design_variable": {
"type": "object",
"required": ["id", "name", "expression_name", "type", "bounds"],
"properties": {
"id": { "type": "string", "pattern": "^dv_\\d{3}$" },
"name": { "type": "string" },
"expression_name": { "type": "string" },
"type": { "enum": ["continuous", "integer", "categorical"] },
"bounds": {
"type": "object",
"required": ["min", "max"],
"properties": {
"min": { "type": "number" },
"max": { "type": "number" }
}
},
"baseline": { "type": "number" },
"units": { "type": "string" },
"enabled": { "type": "boolean", "default": true },
"canvas_position": { "$ref": "#/definitions/position" }
}
},
"extractor": {
"type": "object",
"required": ["id", "name", "type", "outputs"],
"properties": {
"id": { "type": "string", "pattern": "^ext_\\d{3}$" },
"name": { "type": "string" },
"type": { "type": "string" },
"builtin": { "type": "boolean" },
"config": { "type": "object" },
"function": {
"type": "object",
"properties": {
"name": { "type": "string" },
"module": { "type": "string" },
"source_code": { "type": "string" }
}
},
"outputs": {
"type": "array",
"items": {
"type": "object",
"required": ["name"],
"properties": {
"name": { "type": "string" },
"metric": { "type": "string" },
"subcase": { "type": "integer" }
}
}
}
}
},
"objective": {
"type": "object",
"required": ["id", "name", "direction", "source"],
"properties": {
"id": { "type": "string", "pattern": "^obj_\\d{3}$" },
"name": { "type": "string" },
"direction": { "enum": ["minimize", "maximize"] },
"weight": { "type": "number", "minimum": 0 },
"source": {
"type": "object",
"required": ["extractor_id", "output_name"],
"properties": {
"extractor_id": { "type": "string" },
"output_name": { "type": "string" }
}
},
"target": { "type": "number" },
"units": { "type": "string" }
}
},
"position": {
"type": "object",
"properties": {
"x": { "type": "number" },
"y": { "type": "number" }
}
}
}
}
```
---
## 5. Component Architecture
### 5.1 Backend: SpecManager Service
```python
# atomizer-dashboard/backend/api/services/spec_manager.py
from pathlib import Path
from datetime import datetime
import json
import hashlib
from typing import Optional, Dict, Any, List
from pydantic import BaseModel
import jsonschema
class SpecManager:
"""
Central service for managing AtomizerSpec.
All modifications go through this service.
"""
def __init__(self, study_path: Path):
self.study_path = study_path
self.spec_path = study_path / "atomizer_spec.json"
self.schema = self._load_schema()
self._subscribers: List[WebSocketConnection] = []
def load(self) -> Dict[str, Any]:
"""Load and validate the spec."""
with open(self.spec_path) as f:
spec = json.load(f)
self._validate(spec)
return spec
def save(self, spec: Dict[str, Any], modified_by: str = "api"):
"""Save spec with validation and broadcast."""
# Update metadata
spec["meta"]["modified"] = datetime.utcnow().isoformat() + "Z"
spec["meta"]["modified_by"] = modified_by
# Validate
self._validate(spec)
# Compute hash for conflict detection
spec_hash = self._compute_hash(spec)
# Atomic write
temp_path = self.spec_path.with_suffix(".tmp")
with open(temp_path, "w") as f:
json.dump(spec, f, indent=2)
temp_path.replace(self.spec_path)
# Broadcast to all subscribers
self._broadcast({
"type": "spec_updated",
"hash": spec_hash,
"modified_by": modified_by,
"timestamp": spec["meta"]["modified"]
})
return spec_hash
def patch(self, path: str, value: Any, modified_by: str = "api") -> Dict[str, Any]:
"""
Apply a JSON Patch-style modification.
path: JSONPath like "design_variables[0].bounds.max"
"""
spec = self.load()
self._apply_patch(spec, path, value)
self.save(spec, modified_by)
return spec
def add_node(self, node_type: str, data: Dict[str, Any], modified_by: str = "canvas"):
"""Add a new node (design var, extractor, objective, etc.)"""
spec = self.load()
# Generate ID
node_id = self._generate_id(node_type, spec)
data["id"] = node_id
# Add canvas position if not provided
if "canvas_position" not in data:
data["canvas_position"] = self._auto_position(node_type, spec)
# Add to appropriate section
section = self._get_section_for_type(node_type)
spec[section].append(data)
self.save(spec, modified_by)
return node_id
def remove_node(self, node_id: str, modified_by: str = "canvas"):
"""Remove a node and all edges referencing it."""
spec = self.load()
# Find and remove node
for section in ["design_variables", "extractors", "objectives", "constraints"]:
spec[section] = [n for n in spec.get(section, []) if n.get("id") != node_id]
# Remove edges referencing this node
if "canvas" in spec and "edges" in spec["canvas"]:
spec["canvas"]["edges"] = [
e for e in spec["canvas"]["edges"]
if e["source"] != node_id and e["target"] != node_id
]
self.save(spec, modified_by)
def add_custom_function(self, name: str, code: str, outputs: List[str], modified_by: str = "claude"):
"""
Add a custom extractor function.
Claude can call this to add new physics extraction logic.
"""
spec = self.load()
# Validate Python syntax
compile(code, f"<custom:{name}>", "exec")
extractor = {
"id": self._generate_id("ext", spec),
"name": name,
"type": "custom_function",
"builtin": False,
"function": {
"name": name,
"module": "custom_extractors.dynamic",
"source_code": code
},
"outputs": [{"name": o, "metric": "custom"} for o in outputs]
}
spec["extractors"].append(extractor)
self.save(spec, modified_by)
return extractor["id"]
def validate_and_report(self) -> Dict[str, Any]:
"""Run full validation and return detailed report."""
spec = self.load()
report = {
"valid": True,
"errors": [],
"warnings": [],
"summary": {}
}
# Schema validation
try:
self._validate(spec)
except jsonschema.ValidationError as e:
report["valid"] = False
report["errors"].append({
"type": "schema",
"path": list(e.absolute_path),
"message": e.message
})
# Semantic validation
self._validate_semantic(spec, report)
# Summary
report["summary"] = {
"design_variables": len(spec.get("design_variables", [])),
"extractors": len(spec.get("extractors", [])),
"objectives": len(spec.get("objectives", [])),
"constraints": len(spec.get("constraints", [])),
"custom_functions": sum(1 for e in spec.get("extractors", []) if not e.get("builtin", True))
}
return report
def subscribe(self, ws: WebSocketConnection):
"""Subscribe to spec changes."""
self._subscribers.append(ws)
def unsubscribe(self, ws: WebSocketConnection):
"""Unsubscribe from spec changes."""
self._subscribers.remove(ws)
def _broadcast(self, message: Dict[str, Any]):
"""Broadcast to all subscribers."""
for ws in self._subscribers:
ws.send_json(message)
```
### 5.2 Frontend: SpecRenderer Component
```typescript
// atomizer-dashboard/frontend/src/components/canvas/SpecRenderer.tsx
import React, { useCallback, useEffect } from 'react';
import ReactFlow, { Node, Edge, useNodesState, useEdgesState } from 'reactflow';
import { useSpecStore } from '../../hooks/useSpecStore';
import { useWebSocket } from '../../hooks/useWebSocket';
interface AtomizerSpec {
meta: SpecMeta;
design_variables: DesignVariable[];
extractors: Extractor[];
objectives: Objective[];
constraints: Constraint[];
optimization: OptimizationConfig;
canvas: CanvasConfig;
}
export function SpecRenderer({ studyId }: { studyId: string }) {
const { spec, setSpec, updateNode, addNode, removeNode } = useSpecStore();
const { subscribe, send } = useWebSocket(`/api/studies/${studyId}/sync`);
// Convert spec to ReactFlow nodes
const [nodes, setNodes, onNodesChange] = useNodesState(
specToNodes(spec)
);
// Convert spec edges to ReactFlow edges
const [edges, setEdges, onEdgesChange] = useEdgesState(
spec?.canvas?.edges?.map(edgeToReactFlow) || []
);
// Subscribe to real-time updates
useEffect(() => {
const unsubscribe = subscribe('spec_updated', (data) => {
// Reload spec from server
fetchSpec(studyId).then(setSpec);
});
return unsubscribe;
}, [studyId]);
// When spec changes, update nodes
useEffect(() => {
if (spec) {
setNodes(specToNodes(spec));
setEdges(spec.canvas?.edges?.map(edgeToReactFlow) || []);
}
}, [spec]);
// Handle node property changes
const onNodeDataChange = useCallback((nodeId: string, data: Partial<NodeData>) => {
// Send PATCH to backend
send({
type: 'patch_node',
node_id: nodeId,
data
});
}, [send]);
// Handle node position changes (for canvas layout)
const onNodeDragStop = useCallback((event: React.MouseEvent, node: Node) => {
send({
type: 'update_position',
node_id: node.id,
position: node.position
});
}, [send]);
return (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeDragStop={onNodeDragStop}
nodeTypes={customNodeTypes}
fitView
>
{/* Toolbars, panels, etc. */}
</ReactFlow>
);
}
// Convert AtomizerSpec to ReactFlow nodes
function specToNodes(spec: AtomizerSpec | null): Node[] {
if (!spec) return [];
const nodes: Node[] = [];
// Model node (synthetic)
nodes.push({
id: 'model',
type: 'model',
position: { x: 280, y: 50 },
data: {
label: spec.meta.study_name,
filePath: spec.model.sim.path,
configured: true
}
});
// Solver node (synthetic)
nodes.push({
id: 'solver',
type: 'solver',
position: { x: 510, y: 50 },
data: {
label: spec.model.sim.solution_type,
solverType: spec.model.sim.solution_type,
configured: true
}
});
// Design variables
for (const dv of spec.design_variables) {
nodes.push({
id: dv.id,
type: 'designVar',
position: dv.canvas_position || { x: 50, y: nodes.length * 100 },
data: {
label: dv.name,
expressionName: dv.expression_name,
minValue: dv.bounds.min,
maxValue: dv.bounds.max,
baseline: dv.baseline,
unit: dv.units,
enabled: dv.enabled,
configured: true
}
});
}
// Extractors
for (const ext of spec.extractors) {
nodes.push({
id: ext.id,
type: 'extractor',
position: ext.canvas_position || { x: 740, y: nodes.length * 100 },
data: {
label: ext.name,
extractorId: ext.id,
extractorType: ext.type,
builtin: ext.builtin,
config: ext.config,
outputs: ext.outputs,
hasCustomCode: !ext.builtin,
configured: true
}
});
}
// Objectives
for (const obj of spec.objectives) {
nodes.push({
id: obj.id,
type: 'objective',
position: obj.canvas_position || { x: 1020, y: nodes.length * 100 },
data: {
label: obj.name,
name: obj.name,
direction: obj.direction,
weight: obj.weight,
target: obj.target,
units: obj.units,
source: obj.source,
configured: true
}
});
}
// Constraints
for (const con of spec.constraints || []) {
nodes.push({
id: con.id,
type: 'constraint',
position: con.canvas_position || { x: 1020, y: nodes.length * 100 },
data: {
label: con.name,
name: con.name,
operator: con.operator,
threshold: con.threshold,
constraintType: con.type,
source: con.source,
configured: true
}
});
}
// Optimization node
nodes.push({
id: 'optimization',
type: 'algorithm',
position: spec.optimization.canvas_position || { x: 1300, y: 150 },
data: {
label: spec.optimization.algorithm.type,
method: spec.optimization.algorithm.type,
maxTrials: spec.optimization.budget.max_trials,
config: spec.optimization.algorithm.config,
surrogate: spec.optimization.surrogate,
configured: true
}
});
return nodes;
}
```
### 5.3 MCP Tools for Claude
```typescript
// mcp-server/atomizer-tools/src/tools/spec_tools.ts
export const specTools: AtomizerTool[] = [
{
definition: {
name: "spec_get",
description: "Get the current AtomizerSpec for a study. Returns the complete configuration.",
inputSchema: {
type: "object",
properties: {
study_name: { type: "string", description: "Study name" }
},
required: ["study_name"]
}
},
handler: async (args) => {
const spec = await specManager.load(args.study_name);
return { content: [{ type: "text", text: JSON.stringify(spec, null, 2) }] };
}
},
{
definition: {
name: "spec_modify",
description: "Modify part of an AtomizerSpec. Changes are validated and broadcast to all clients.",
inputSchema: {
type: "object",
properties: {
study_name: { type: "string" },
modifications: {
type: "array",
items: {
type: "object",
properties: {
operation: { enum: ["set", "add", "remove"] },
path: { type: "string", description: "JSONPath to the field" },
value: { description: "New value (for set/add)" }
}
}
}
},
required: ["study_name", "modifications"]
}
},
handler: async (args) => {
const results = [];
for (const mod of args.modifications) {
switch (mod.operation) {
case "set":
await specManager.patch(mod.path, mod.value, "claude");
results.push(`Set ${mod.path}`);
break;
case "add":
const id = await specManager.addNode(mod.path, mod.value, "claude");
results.push(`Added ${id} at ${mod.path}`);
break;
case "remove":
await specManager.removeNode(mod.path, "claude");
results.push(`Removed ${mod.path}`);
break;
}
}
return {
content: [{
type: "text",
text: JSON.stringify({ success: true, applied: results }, null, 2)
}]
};
}
},
{
definition: {
name: "spec_add_custom_extractor",
description: "Add a custom Python function as an extractor. The function will be available in the optimization workflow.",
inputSchema: {
type: "object",
properties: {
study_name: { type: "string" },
function_name: { type: "string", description: "Name of the function" },
description: { type: "string" },
code: { type: "string", description: "Python source code" },
outputs: {
type: "array",
items: { type: "string" },
description: "List of output names this function produces"
},
dependencies: {
type: "array",
items: { type: "string" },
description: "Python packages required (must be installed)"
}
},
required: ["study_name", "function_name", "code", "outputs"]
}
},
handler: async (args) => {
// Validate code syntax
const validation = await validatePythonCode(args.code);
if (!validation.valid) {
return {
content: [{ type: "text", text: JSON.stringify({ error: validation.error }) }],
isError: true
};
}
const extractorId = await specManager.addCustomFunction(
args.function_name,
args.code,
args.outputs,
"claude"
);
return {
content: [{
type: "text",
text: JSON.stringify({
success: true,
extractor_id: extractorId,
message: `Custom extractor "${args.function_name}" added. Canvas will update automatically.`,
outputs: args.outputs
}, null, 2)
}]
};
}
},
{
definition: {
name: "spec_validate",
description: "Validate an AtomizerSpec and return detailed report.",
inputSchema: {
type: "object",
properties: {
study_name: { type: "string" }
},
required: ["study_name"]
}
},
handler: async (args) => {
const report = await specManager.validateAndReport(args.study_name);
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
}
},
{
definition: {
name: "spec_create_from_description",
description: "Create a new AtomizerSpec from a natural language description. Claude interprets the description and generates a complete spec.",
inputSchema: {
type: "object",
properties: {
study_name: { type: "string" },
description: { type: "string", description: "Natural language description of the optimization" },
model_path: { type: "string", description: "Path to the NX model" }
},
required: ["study_name", "description", "model_path"]
}
},
handler: async (args) => {
// This would invoke the StudyCreator with Claude interpretation
// ...
}
}
];
```
---
## 6. Intelligent Assistant Integration
### 6.1 Claude Capabilities
With the unified spec, Claude can:
| Capability | Example User Request | Claude Action |
|------------|---------------------|---------------|
| **Modify Parameters** | "Set mass weight to 3" | `spec_modify` on `objectives[1].weight` |
| **Add Custom Logic** | "Add a manufacturing cost function based on rib count" | `spec_add_custom_extractor` with Python code |
| **Restructure Workflow** | "Change from single-objective to multi-objective" | Modify `optimization.algorithm.type` and restructure objectives |
| **Validate Setup** | "Check if my config is valid" | `spec_validate` and explain issues |
| **Explain Canvas** | "What does this optimization do?" | Read spec, generate natural language summary |
### 6.2 Example Interaction
```
User: "Claude, I want to add a manufacturability score that penalizes designs with more than 10 ribs"
Claude: I'll add a custom extractor for manufacturability scoring. Let me create the function:
[spec_add_custom_extractor]
function_name: "manufacturability_score"
code: """
def manufacturability_score(design_vars, fem_results):
'''
Calculate manufacturability score based on rib count.
Higher score = easier to manufacture.
'''
rib_count = design_vars.get('n_lateral_ribs', 0)
if rib_count <= 8:
score = 100 # Easy
elif rib_count <= 10:
score = 80 - (rib_count - 8) * 10 # Moderate
else:
score = 60 - (rib_count - 10) * 15 # Difficult, penalized
return max(0, score)
"""
outputs: ["mfg_score"]
Done! I've added a manufacturability extractor that:
- Returns 100 for 8 or fewer ribs (easy to manufacture)
- Returns 60-80 for 9-10 ribs (moderate)
- Returns <60 for 11+ ribs (penalized)
The canvas has been updated with a new "Manufacturability Score" node.
Would you like me to:
1. Add this as a soft constraint (mfg_score >= 60)?
2. Add it as an objective to maximize?
```
### 6.3 Real-time Canvas Updates
```
┌─────────────────────────────────────────────────────────────────┐
│ Real-time Update Flow │
└─────────────────────────────────────────────────────────────────┘
User types: "Change mass weight to 5"
┌─────────────────┐
│ Claude receives │
│ message via WS │
└────────┬────────┘
┌─────────────────┐
│ Claude calls │
│ spec_modify │
│ path: objectives│
│ [1].weight = 5 │
└────────┬────────┘
┌─────────────────┐
│ SpecManager │
│ - Validates │
│ - Saves spec │
│ - Broadcasts │
└────────┬────────┘
├────────────────────────────────────┐
│ │
▼ ▼
┌─────────────────┐ ┌─────────────────┐
│ Canvas receives │ │ Claude confirms │
│ spec_updated │ │ "Weight updated │
│ WebSocket msg │ │ to 5" │
└────────┬────────┘ └─────────────────┘
┌─────────────────┐
│ Canvas re- │
│ renders node │
│ with new weight │
│ "Mass (w=5)" │
└─────────────────┘
```
---
## 7. API Design
### 7.1 REST Endpoints
```yaml
# OpenAPI 3.0 specification (partial)
paths:
/api/studies/{study_id}/spec:
get:
summary: Get AtomizerSpec
responses:
200:
content:
application/json:
schema:
$ref: '#/components/schemas/AtomizerSpec'
put:
summary: Replace entire AtomizerSpec
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/AtomizerSpec'
patch:
summary: Partial update to spec
requestBody:
content:
application/json:
schema:
type: object
properties:
path:
type: string
description: JSONPath to field
value:
description: New value
modified_by:
type: string
/api/studies/{study_id}/spec/nodes:
post:
summary: Add a new node
requestBody:
content:
application/json:
schema:
type: object
properties:
type:
enum: [designVar, extractor, objective, constraint]
data:
type: object
/api/studies/{study_id}/spec/nodes/{node_id}:
patch:
summary: Update node properties
delete:
summary: Remove node
/api/studies/{study_id}/spec/validate:
post:
summary: Validate spec and return report
/api/studies/{study_id}/spec/custom-functions:
post:
summary: Add custom extractor function
requestBody:
content:
application/json:
schema:
type: object
required: [name, code, outputs]
properties:
name:
type: string
code:
type: string
outputs:
type: array
items:
type: string
```
### 7.2 WebSocket Protocol
```typescript
// WebSocket Messages
// Client -> Server
interface ClientMessage {
type: 'subscribe' | 'patch_node' | 'add_node' | 'remove_node' | 'update_position';
study_id: string;
// ... type-specific fields
}
// Server -> Client
interface ServerMessage {
type: 'spec_updated' | 'validation_error' | 'node_added' | 'node_removed' | 'connection_ack';
timestamp: string;
// ... type-specific fields
}
// Example: spec_updated
{
"type": "spec_updated",
"timestamp": "2026-01-17T14:30:00Z",
"modified_by": "claude",
"hash": "sha256:abc123...",
"changes": [
{ "path": "objectives[1].weight", "old": 1, "new": 5 }
]
}
```
---
## 8. Migration Strategy
### 8.1 Backward Compatibility
```python
# optimization_engine/config/migrator.py
class SpecMigrator:
"""
Migrate old optimization_config.json to AtomizerSpec v2.
"""
MIGRATION_MAP = {
# Old field -> New field
"design_variables.*.parameter": "design_variables.*.expression_name",
"design_variables.*.bounds": lambda dv: {"min": dv["bounds"][0], "max": dv["bounds"][1]},
"objectives.*.goal": "objectives.*.direction",
"optimization_settings.n_trials": "optimization.budget.max_trials",
"optimization_settings.sampler": lambda s: {"type": s.replace("Sampler", "")},
}
def migrate(self, old_config: Dict) -> Dict:
"""Convert old config to AtomizerSpec v2."""
spec = {
"meta": {
"version": "2.0",
"created_by": "migration",
"study_name": old_config.get("study_name", "migrated_study")
},
"design_variables": [],
"extractors": [],
"objectives": [],
"constraints": [],
"optimization": {},
"canvas": {"edges": []}
}
# Migrate design variables
for i, dv in enumerate(old_config.get("design_variables", [])):
spec["design_variables"].append(self._migrate_dv(dv, i))
# Migrate objectives
for i, obj in enumerate(old_config.get("objectives", [])):
extractor_id, extractor = self._infer_extractor(obj)
if extractor not in [e["id"] for e in spec["extractors"]]:
spec["extractors"].append(extractor)
spec["objectives"].append(self._migrate_objective(obj, i, extractor_id))
# ... continue for other sections
return spec
def _migrate_dv(self, dv: Dict, index: int) -> Dict:
"""Migrate a design variable."""
bounds = dv.get("bounds", [dv.get("min", 0), dv.get("max", 1)])
return {
"id": f"dv_{index:03d}",
"name": dv.get("name", dv.get("parameter", f"param_{index}")),
"expression_name": dv.get("expression_name", dv.get("parameter")),
"type": dv.get("type", "continuous"),
"bounds": {"min": bounds[0], "max": bounds[1]},
"baseline": dv.get("baseline"),
"units": dv.get("units", dv.get("description", "")),
"enabled": dv.get("enabled", True)
}
```
### 8.2 Migration Script
```bash
# CLI tool for batch migration
python -m optimization_engine.config.migrate \
--input studies/*/optimization_config.json \
--output-suffix "_spec_v2.json" \
--validate \
--dry-run
```
---
## 9. Implementation Roadmap
### Phase 1: Foundation (Weeks 1-3)
| Task | Priority | Effort |
|------|----------|--------|
| Define AtomizerSpec JSON Schema v2.0 | P0 | 2 days |
| Create SpecManager service (Python) | P0 | 3 days |
| Implement spec validation | P0 | 2 days |
| Add REST endpoints for spec CRUD | P0 | 3 days |
| Create migration script | P0 | 3 days |
| Update optimization engine to use spec | P1 | 4 days |
**Deliverable**: Backend can read/write/validate AtomizerSpec
### Phase 2: Frontend Integration (Weeks 4-6)
| Task | Priority | Effort |
|------|----------|--------|
| Create SpecRenderer component | P0 | 4 days |
| Replace useCanvasStore with useSpecStore | P0 | 3 days |
| Implement spec ↔ canvas conversion | P0 | 3 days |
| Add WebSocket sync | P1 | 3 days |
| Update node panels for full spec fields | P1 | 4 days |
| Test bidirectional editing | P0 | 3 days |
**Deliverable**: Canvas renders and edits AtomizerSpec directly
### Phase 3: Claude Integration (Weeks 7-9)
| Task | Priority | Effort |
|------|----------|--------|
| Implement spec_* MCP tools | P0 | 4 days |
| Add custom function support | P1 | 4 days |
| Test Claude → Canvas updates | P0 | 3 days |
| Add natural language study creation | P2 | 5 days |
| Create Claude assistant prompts | P1 | 2 days |
**Deliverable**: Claude can read, modify, and extend AtomizerSpec
### Phase 4: Polish & Testing (Weeks 10-12)
| Task | Priority | Effort |
|------|----------|--------|
| Migrate all existing studies | P0 | 3 days |
| Integration testing | P0 | 5 days |
| Documentation | P1 | 3 days |
| Performance optimization | P2 | 3 days |
| User acceptance testing | P0 | 4 days |
**Deliverable**: Production-ready unified configuration system
---
## Appendix A: Comparison Table
| Feature | Current | Proposed |
|---------|---------|----------|
| Config formats | 4+ variants | 1 (AtomizerSpec) |
| Canvas ↔ Config sync | Lossy, manual | Lossless, automatic |
| Claude modifications | Returns JSON for manual copy | Direct spec modification |
| Custom extractors | Edit Python code | Declare in spec |
| Real-time updates | None | WebSocket-driven |
| Validation | Partial Python | Full JSON Schema + semantic |
| Canvas layout | Lost on reload | Persisted in spec |
---
## Appendix B: Glossary
| Term | Definition |
|------|------------|
| **AtomizerSpec** | The unified JSON configuration format (v2.0) |
| **SpecManager** | Backend service for all spec operations |
| **SpecRenderer** | Frontend component that renders spec as canvas |
| **Custom Extractor** | User-defined Python function declared in spec |
| **Spec Patch** | Partial update to a single field in the spec |
---
## Appendix C: File Locations (Proposed)
```
atomizer-dashboard/
├── backend/api/
│ ├── services/
│ │ └── spec_manager.py # NEW: Core spec management
│ ├── routes/
│ │ └── spec.py # NEW: REST endpoints for spec
│ └── schemas/
│ └── atomizer_spec_v2.json # NEW: JSON Schema
├── frontend/src/
│ ├── hooks/
│ │ └── useSpecStore.ts # NEW: Spec state management
│ ├── components/canvas/
│ │ └── SpecRenderer.tsx # NEW: Spec-based canvas
│ └── lib/spec/
│ ├── types.ts # NEW: TypeScript types
│ └── converter.ts # NEW: Spec ↔ ReactFlow
mcp-server/atomizer-tools/
└── src/tools/
└── spec_tools.ts # NEW: MCP tools for spec
optimization_engine/
├── config/
│ ├── migrator.py # NEW: v1 → v2 migration
│ └── spec_loader.py # NEW: AtomizerSpec loader
└── schemas/
└── atomizer_spec_v2.json # Canonical schema
```
---
*This document is the master plan for implementing Atomizer's Unified Configuration Architecture. All implementation work should reference this document.*