Files
Atomizer/docs/reviews/ARCHITECTURE_REVIEW.md
Anto01 5c419e2358 fix(canvas): Multiple fixes for drag-drop, undo/redo, and code generation
Drag-drop fixes:
- Fix Objective default data: use nested 'source' object with extractor_id/output_name
- Fix Constraint default data: use 'type' field (not constraint_type), 'threshold' (not limit)

Undo/Redo fixes:
- Remove dependency on isDirty flag (which is always false due to auto-save)
- Record snapshots based on actual spec changes via deep comparison

Code generation improvements:
- Update system prompt to support multiple extractor types:
  * OP2-based extractors for FEA results (stress, displacement, frequency)
  * Expression-based extractors for NX model values (dimensions, volumes)
  * Computed extractors for derived values (no FEA needed)
- Claude will now choose appropriate signature based on user's description
2026-01-20 15:08:49 -05:00

731 lines
26 KiB
Markdown
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.
# Atomizer Architecture Review
**Date**: January 2026
**Version**: 2.0 (AtomizerSpec unified configuration)
**Author**: Architecture Review
---
## Executive Summary
Atomizer is a structural optimization platform that enables engineers to optimize FEA (Finite Element Analysis) designs through a visual canvas interface and AI-powered assistance. The architecture follows a **single source of truth** pattern where all configuration flows through `atomizer_spec.json`.
### Key Strengths
- **Unified Configuration**: One JSON file defines the entire optimization study
- **Type Safety**: Pydantic validation at every modification point
- **Real-time Collaboration**: WebSocket-based sync between all clients
- **Responsive UI**: Optimistic updates with background synchronization
- **AI Integration**: Claude can modify configurations in Power Mode
### Architecture Quality Score: **8.5/10**
| Aspect | Score | Notes |
| --------------- | ----- | ----------------------------------------------------- |
| Data Integrity | 9/10 | Single source of truth, hash-based conflict detection |
| Type Safety | 9/10 | Pydantic models throughout backend |
| Extensibility | 8/10 | Custom extractors, algorithms supported |
| Performance | 8/10 | Optimistic updates, WebSocket streaming |
| Maintainability | 8/10 | Clear separation of concerns |
| Documentation | 7/10 | Good inline docs, needs more high-level guides |
---
## 1. Configuration Layer
### 1.1 AtomizerSpec v2.0 - The Single Source of Truth
**Location**: `studies/{study_name}/atomizer_spec.json`
The AtomizerSpec is the heart of Atomizer's configuration. Every component reads from and writes to this single file.
```
atomizer_spec.json
├── meta # Study metadata
│ ├── version: "2.0"
│ ├── study_name
│ ├── created_by # canvas | claude | api | migration
│ └── modified_at
├── model # NX model files
│ ├── sim: { path, solver }
│ ├── nx_part: { path }
│ └── fem: { path }
├── design_variables[] # Parameters to optimize
│ ├── id: "dv_001"
│ ├── name, expression_name
│ ├── type: continuous | discrete
│ ├── bounds: { min, max }
│ └── canvas_position
├── extractors[] # Physics result extractors
│ ├── id: "ext_001"
│ ├── type: mass | displacement | stress | zernike | custom
│ ├── config: {}
│ └── outputs: [{ name, metric }]
├── objectives[] # Optimization goals
│ ├── id: "obj_001"
│ ├── direction: minimize | maximize
│ ├── weight
│ └── source: { extractor_id, output_key }
├── constraints[] # Hard/soft constraints
│ ├── id: "con_001"
│ ├── operator: <= | >= | ==
│ ├── threshold
│ └── source: { extractor_id, output_key }
├── optimization # Algorithm settings
│ ├── algorithm: { type, config }
│ ├── budget: { max_trials }
│ └── surrogate: { enabled, type }
└── canvas # UI layout state
└── edges[]
```
### 1.2 Node ID Convention
All configurable elements use unique IDs with prefixes:
| Prefix | Element Type | Example |
|--------|--------------|---------|
| `dv_` | Design Variable | `dv_001`, `dv_002` |
| `ext_` | Extractor | `ext_001`, `ext_002` |
| `obj_` | Objective | `obj_001`, `obj_002` |
| `con_` | Constraint | `con_001`, `con_002` |
IDs are auto-generated: `{prefix}{max_existing + 1:03d}`
### 1.3 Legacy Configuration
**File**: `optimization_config.json` (deprecated)
Legacy studies may have this file. The `SpecMigrator` automatically converts them to AtomizerSpec v2.0 on load.
```python
from optimization_engine.config.migrator import SpecMigrator
migrator = SpecMigrator(study_dir)
spec = migrator.migrate_file(legacy_path, spec_path)
```
---
## 2. Frontend Architecture
### 2.1 Technology Stack
| Layer | Technology | Purpose |
|-------|------------|---------|
| Build | Vite + TypeScript | Fast bundling, type safety |
| UI | React 18 + TailwindCSS | Component framework |
| State | Zustand | Lightweight global state |
| Canvas | ReactFlow | Graph visualization |
| Communication | Fetch + WebSocket | API + real-time sync |
### 2.2 Directory Structure
```
atomizer-dashboard/frontend/src/
├── components/
│ ├── canvas/ # Canvas components
│ │ ├── AtomizerCanvas # Main wrapper
│ │ ├── SpecRenderer # Spec → ReactFlow
│ │ ├── nodes/ # Node type components
│ │ └── panels/ # Side panels
│ └── chat/ # Claude chat UI
├── hooks/
│ ├── useSpecStore.ts # Central state (Zustand)
│ ├── useChat.ts # Claude integration
│ ├── useCanvasStore.ts # Local canvas state
│ └── useSpecWebSocket.ts # Real-time sync
├── lib/
│ └── spec/converter.ts # Spec ↔ ReactFlow
├── types/
│ └── atomizer-spec.ts # TypeScript definitions
└── pages/
├── CanvasView.tsx # Main canvas page
├── Home.tsx # Study selection
└── Setup.tsx # Study wizard
```
### 2.3 State Management Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ useSpecStore (Zustand) │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────────────┐ │
│ │ spec │ │ hash │ │ isDirty │ │ selectedNode │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └───────┬───────┘ │
└───────┼────────────┼────────────┼───────────────┼──────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐
│Canvas │ │Conflict │ │ Save │ │ NodeConfig │
│ Render │ │Detection│ │ Button │ │ Panel │
└─────────┘ └─────────┘ └─────────┘ └─────────────┘
```
**Key Actions**:
- `loadSpec(studyId)` - Fetch spec from backend
- `patchSpec(path, value)` - Update with conflict check
- `patchSpecOptimistic(path, value)` - Fire-and-forget update
- `addNode(type, data)` - Add design var/extractor/etc.
- `removeNode(nodeId)` - Delete with edge cleanup
### 2.4 Canvas Rendering Pipeline
```
AtomizerSpec JSON
specToNodes() [converter.ts]
├──► Model Node (synthetic)
├──► Solver Node (synthetic)
├──► DesignVar Nodes × N
├──► Extractor Nodes × N
├──► Objective Nodes × N
├──► Constraint Nodes × N
├──► Algorithm Node (synthetic)
└──► Surrogate Node (optional)
ReactFlow Component
Interactive Canvas
```
**Layout Constants**:
```
Design Variables: x = 50
Model: x = 280
Solver: x = 510
Extractors: x = 740
Objectives: x = 1020
Constraints: x = 1020 (offset y)
Algorithm: x = 1300
Surrogate: x = 1530
Row Height: 100px
```
---
## 3. Backend Architecture
### 3.1 Technology Stack
| Layer | Technology | Purpose |
|-------|------------|---------|
| Framework | FastAPI | Async REST + WebSocket |
| Validation | Pydantic | Schema enforcement |
| Database | SQLite | Trial storage (Optuna schema) |
| LLM | Anthropic Claude | AI assistance |
### 3.2 Directory Structure
```
atomizer-dashboard/backend/api/
├── main.py # FastAPI app
├── routes/
│ ├── spec.py # Spec CRUD + WebSocket
│ ├── optimization.py # Run management
│ ├── claude.py # Chat sessions
│ └── files.py # File operations
└── services/
├── spec_manager.py # Central spec management
├── claude_agent.py # Claude with tools
├── context_builder.py # System prompts
└── session_manager.py # WebSocket sessions
```
### 3.3 SpecManager Service
**The SpecManager is the gatekeeper for all spec modifications.**
```python
class SpecManager:
def __init__(self, study_path: Path):
self.study_path = study_path
self.spec_file = study_path / "atomizer_spec.json"
self.subscribers: List[WebSocket] = []
# Loading
def load_spec(self) -> AtomizerSpec
def load_raw(self) -> dict
# Validation
def validate(self, spec) -> ValidationReport
def validate_semantic(self, spec) -> ValidationReport
# Modifications
def patch_spec(self, path, value) -> dict
def add_node(self, type, data) -> str
def update_node(self, id, updates) -> None
def remove_node(self, id) -> None
# Persistence
def save_spec(self, spec) -> dict # Atomic write + hash
def compute_hash(self, spec) -> str
# Real-time sync
def subscribe(self, ws: WebSocket)
def broadcast(self, message: dict)
```
**Modification Flow**:
1. Load current spec
2. Apply modification
3. Validate with Pydantic
4. Atomic write to disk
5. Compute new hash
6. Broadcast to all WebSocket subscribers
7. Return hash + timestamp
### 3.4 REST API Endpoints
| Method | Endpoint | Purpose |
|--------|----------|---------|
| GET | `/api/studies/{id}/spec` | Load full spec |
| GET | `/api/studies/{id}/spec/hash` | Get current hash |
| PUT | `/api/studies/{id}/spec` | Replace entire spec |
| PATCH | `/api/studies/{id}/spec` | JSONPath patch |
| POST | `/api/studies/{id}/spec/nodes` | Add node |
| PATCH | `/api/studies/{id}/spec/nodes/{nid}` | Update node |
| DELETE | `/api/studies/{id}/spec/nodes/{nid}` | Remove node |
| POST | `/api/studies/{id}/spec/validate` | Validate spec |
| WS | `/api/studies/{id}/spec/sync` | Real-time sync |
### 3.5 Conflict Detection
Uses SHA256 hash of spec content:
```
Client A loads spec (hash: abc123)
Client B loads spec (hash: abc123)
Client A modifies → sends with hash abc123
Server: hash matches → apply → new hash: def456
Server: broadcast to all clients
Client B modifies → sends with hash abc123
Server: hash mismatch (expected def456)
Server: return 409 Conflict
Client B: reload latest spec
```
---
## 4. Optimization Engine
### 4.1 Directory Structure
```
optimization_engine/
├── config/ # Configuration management
│ ├── spec_models.py # Pydantic models
│ ├── spec_validator.py # Semantic validation
│ └── migrator.py # Legacy migration
├── extractors/ # Physics extractors
│ ├── extract_displacement.py
│ ├── extract_stress.py
│ ├── extract_mass_*.py
│ ├── extract_zernike*.py
│ └── custom_extractor_loader.py
├── core/ # Optimization algorithms
│ ├── runner.py # Main loop
│ ├── method_selector.py # Algorithm selection
│ └── intelligent_optimizer.py # IMSO
├── nx/ # NX integration
│ ├── solver.py # Nastran execution
│ └── updater.py # Parameter updates
├── study/ # Study management
│ ├── creator.py
│ └── state.py
└── utils/
├── dashboard_db.py # Optuna schema
└── trial_manager.py # Trial CRUD
```
### 4.2 Extractor Library
| ID | Type | Function | Inputs |
|----|------|----------|--------|
| E1 | Displacement | Max/RMS displacement | OP2, subcase |
| E2 | Frequency | Eigenvalue | OP2, mode |
| E3 | Stress | Von Mises, principal | OP2, element set |
| E4 | Mass (BDF) | Total mass | BDF file |
| E5 | Mass (Expr) | NX expression | NX session |
| E8-10 | Zernike | OPD polynomial fit | OP2, grid config |
**Custom Extractor Pattern**:
```python
def extract_volume(op2_path: str) -> Dict[str, float]:
from pyNastran.op2.op2 import OP2
op2 = OP2()
op2.read_op2(op2_path)
# ... calculation
return {"volume_mm3": calculated_volume}
```
### 4.3 Trial Storage
**Folder Structure**:
```
studies/{study}/2_iterations/
├── trial_0001/
│ ├── params.json # Input parameters
│ ├── results.json # Objectives/constraints
│ ├── _meta.json # Metadata
│ └── *.op2, *.fem # FEA outputs
├── trial_0002/
└── ...
```
**Database Schema** (Optuna-compatible SQLite):
```sql
trials (trial_id, study_id, number, state, created_at)
trial_params (trial_id, param_name, param_value)
trial_values (trial_id, objective_id, value)
trial_user_attributes (trial_id, key, value_json)
```
---
## 5. Claude Integration
### 5.1 Two Operation Modes
| Mode | Endpoint | Capabilities | Use Case |
|------|----------|--------------|----------|
| **User** | `/ws` | Read-only, MCP tools | Safe exploration |
| **Power** | `/ws/power` | Full write access | Canvas modification |
### 5.2 Power Mode Tools
```python
# claude_agent.py - Direct API tools
add_design_variable(name, min, max, baseline, units)
add_extractor(name, type, config, custom_code)
add_objective(name, direction, weight, extractor_id)
add_constraint(name, operator, threshold, extractor_id)
update_spec_field(path, value) # JSONPath update
remove_node(node_id)
```
### 5.3 Context Building
The `ContextBuilder` assembles rich system prompts:
```
# Atomizer Assistant
## Current Mode: POWER (full write access)
## Current Study: bracket_optimization
- Design Variables: 3 (thickness, angle, radius)
- Extractors: 2 (Displacement, Mass)
- Objectives: 2 (Min mass, Max stiffness)
- Constraints: 1 (mass <= 0.2 kg)
- Status: 47/100 trials complete
## Canvas State
8 nodes, 11 edges
[Node list with IDs and types...]
## Available Tools
- add_design_variable: Add a new design variable
- add_extractor: Add physics extractor
- add_objective: Add optimization objective
- add_constraint: Add constraint
- update_spec_field: Update any field by JSONPath
- remove_node: Remove element by ID
**ACT IMMEDIATELY** when asked to modify things.
```
---
## 6. Data Flow Diagrams
### 6.1 Canvas Edit Flow
```
User edits node property
┌─────────────────────────────┐
│ useSpecStore.patchSpec() │
│ ┌─────────────────────────┐ │
│ │ 1. Optimistic UI update │ │
│ └───────────┬─────────────┘ │
│ │ │
│ ┌───────────▼─────────────┐ │
│ │ 2. Async PATCH request │ │
│ └───────────┬─────────────┘ │
└─────────────┼───────────────┘
┌─────────────────────────────┐
│ Backend: SpecManager │
│ ┌─────────────────────────┐ │
│ │ 3. JSONPath parse │ │
│ │ 4. Apply modification │ │
│ │ 5. Pydantic validate │ │
│ │ 6. Atomic file write │ │
│ │ 7. Compute new hash │ │
│ │ 8. Broadcast to clients │ │
│ └───────────┬─────────────┘ │
└─────────────┼───────────────┘
┌─────────────────────────────┐
│ All WebSocket Clients │
│ ┌─────────────────────────┐ │
│ │ 9. Receive spec_updated │ │
│ │ 10. Update local hash │ │
│ │ 11. Re-render if needed │ │
│ └─────────────────────────┘ │
└─────────────────────────────┘
```
### 6.2 Optimization Run Flow
```
User clicks "Run"
┌─────────────────────────────┐
│ POST /api/optimization/start│
│ { study_id, trials, method }│
└─────────────┬───────────────┘
┌─────────────────────────────┐
│ Backend: Spawn runner │
│ ┌─────────────────────────┐ │
│ │ 1. Load spec │ │
│ │ 2. Initialize Optuna │ │
│ │ 3. Create trial folders │ │
│ └───────────┬─────────────┘ │
└─────────────┼───────────────┘
┌─────────┼─────────┐
│ │ │
▼ ▼ ▼
┌───────────────────────────────────────────────────────┐
│ For each trial (1 to N): │
│ ┌───────────────────────────────────────────────────┐ │
│ │ 4. Optuna suggests parameters │ │
│ │ 5. Update NX expressions │ │
│ │ 6. Run Nastran simulation │ │
│ │ 7. Extract physics results │ │
│ │ 8. Compute objectives/constraints │ │
│ │ 9. Save to trial folder + database │ │
│ │ 10. Send WebSocket update │ │
│ └─────────────────────────────────────────────────┘ │
└───────────────────────────────────────────────────────┘
┌─────────────────────────────┐
│ Frontend: Real-time updates │
│ ┌─────────────────────────┐ │
│ │ Update convergence plot │ │
│ │ Update trial table │ │
│ │ Show best design │ │
│ └─────────────────────────┘ │
└─────────────────────────────┘
```
### 6.3 Claude Canvas Modification Flow
```
User: "Add volume extractor with constraint <= 1000"
┌─────────────────────────────┐
│ WebSocket: /ws/power │
│ { type: 'message', │
│ content: '...', │
│ canvas_state: {...} } │
└─────────────┬───────────────┘
┌─────────────────────────────┐
│ AtomizerClaudeAgent │
│ ┌─────────────────────────┐ │
│ │ 1. Build context prompt │ │
│ │ 2. Send to Claude API │ │
│ │ 3. Claude decides tools │ │
│ └───────────┬─────────────┘ │
└─────────────┼───────────────┘
┌─────────────────────────────┐
│ Claude Tool Calls: │
│ ┌─────────────────────────┐ │
│ │ add_extractor( │ │
│ │ name="Volume", │ │
│ │ type="custom", │ │
│ │ code="..." ) │ │
│ │ │ │
│ │ add_constraint( │ │
│ │ name="Max Volume", │ │
│ │ operator="<=", │ │
│ │ threshold=1000 ) │ │
│ └───────────┬─────────────┘ │
└─────────────┼───────────────┘
┌─────────────────────────────┐
│ Each tool modifies spec: │
│ ┌─────────────────────────┐ │
│ │ Load → Modify → Save │ │
│ │ Send spec_modified │ │
│ └───────────┬─────────────┘ │
└─────────────┼───────────────┘
┌─────────────────────────────┐
│ Frontend receives events: │
│ ┌─────────────────────────┐ │
│ │ spec_modified → reload │ │
│ │ Canvas shows new nodes │ │
│ └─────────────────────────┘ │
└─────────────────────────────┘
```
---
## 7. Component Relationships
### 7.1 Frontend Component Hierarchy
```
<App>
├── <CanvasView>
│ ├── <AtomizerCanvas>
│ │ ├── <ReactFlow>
│ │ │ ├── <DesignVarNode> × N
│ │ │ ├── <ExtractorNode> × N
│ │ │ ├── <ObjectiveNode> × N
│ │ │ ├── <ConstraintNode> × N
│ │ │ ├── <ModelNode>
│ │ │ ├── <SolverNode>
│ │ │ └── <AlgorithmNode>
│ │ ├── <NodePalette>
│ │ ├── <NodeConfigPanel>
│ │ ├── <ValidationPanel>
│ │ └── <ExecuteDialog>
│ └── <ChatPanel>
│ ├── <ChatMessage> × N
│ └── <ToolCallCard> × M
├── <Home>
│ └── <StudyList>
└── <Setup>
└── <StudyWizard>
```
### 7.2 Backend Service Dependencies
```
FastAPI App
├── spec.py ─────────► SpecManager
│ ├── Pydantic Models
│ └── File I/O + Hash
├── claude.py ───────► AtomizerClaudeAgent
│ ├── ContextBuilder
│ ├── Anthropic Client
│ └── Write Tools
├── optimization.py ─► Runner Process
│ ├── TrialManager
│ ├── NX Solver
│ └── Extractors
└── WebSocket Hub ◄─── All routes broadcast
```
---
## 8. Critical Patterns
### 8.1 Modification Pattern
**Always use SpecManager for modifications:**
```python
# ❌ WRONG: Direct file write
with open("atomizer_spec.json", "w") as f:
json.dump(spec, f)
# ✅ CORRECT: Use SpecManager
manager = SpecManager(study_path)
spec = manager.load_spec()
spec.objectives[0].weight = 2.0
manager.save_spec(spec)
```
### 8.2 Optimistic Update Pattern
```typescript
// 1. Update UI immediately
setSpec(modifiedSpec);
// 2. Async sync to backend
patchSpec(path, value)
.then(({ hash }) => setHash(hash))
.catch(() => setSpec(originalSpec)); // Rollback on failure
```
### 8.3 WebSocket Sync Pattern
```
Client A ─────► Server ─────► Client B
│ │ │
│ PATCH │ │
├─────────────►│ │
│ │ broadcast │
│ ├─────────────►│
│ │ │
│◄─────────────┤◄─────────────┤
│ ack + hash │ │
```
---
## 9. Potential Improvements
### 9.1 Current Limitations
1. **No Undo/Redo**: Canvas modifications are immediate
2. **Single File Lock**: No distributed locking for multi-user
3. **Memory-only Sessions**: Session state lost on restart
4. **Limited Offline**: Requires backend connection
### 9.2 Recommended Enhancements
| Priority | Enhancement | Benefit |
|----------|-------------|---------|
| High | Operation history with undo | Better UX |
| High | Persistent sessions (Redis) | Scalability |
| Medium | Spec versioning/branching | Experimentation |
| Medium | Batch operations API | Performance |
| Low | Offline canvas editing | Flexibility |
---
## 10. Conclusion
Atomizer's architecture is **well-designed for its purpose**: enabling engineers to configure and run FEA optimizations through a visual interface with AI assistance.
**Strongest Points**:
- Single source of truth eliminates sync issues
- Pydantic ensures data integrity
- WebSocket enables real-time collaboration
- Optimistic updates provide responsive UX
**Areas for Attention**:
- Add undo/redo for canvas operations
- Consider persistent session storage for production
- Expand test coverage for spec migrations
The architecture is **production-ready** for single-user/small-team scenarios and can be enhanced for enterprise deployment.