/** * Spec ↔ Canvas converters * * AtomizerSpec v2.0 is the single source of truth. * This module converts AtomizerSpec → ReactFlow nodes/edges for visualization. * * NOTE: Canvas edges are primarily for visual validation. * The computation truth lives in objective.source / constraint.source. */ import type { Node, Edge } from 'reactflow'; import type { AtomizerSpec, CanvasPosition, DesignVariable, Extractor, Objective, Constraint } from '../types/atomizer-spec'; import type { CanvasNodeData, ModelNodeData, SolverNodeData, AlgorithmNodeData, SurrogateNodeData, DesignVarNodeData, ExtractorNodeData, ObjectiveNodeData, ConstraintNodeData, } from './canvas/schema'; // --------------------------------------------------------------------------- // Layout defaults (deterministic) // --------------------------------------------------------------------------- const DEFAULT_LAYOUT = { startX: 80, startY: 80, colWidth: 260, rowHeight: 110, cols: { designVar: 0, model: 1, solver: 2, extractor: 3, objective: 4, constraint: 4, algorithm: 5, surrogate: 6, } as const, }; function toCanvasPosition(pos: CanvasPosition | undefined | null, fallback: CanvasPosition): CanvasPosition { if (!pos) return fallback; if (typeof pos.x !== 'number' || typeof pos.y !== 'number') return fallback; return { x: pos.x, y: pos.y }; } function makeFallbackPosition(col: number, row: number): CanvasPosition { return { x: DEFAULT_LAYOUT.startX + col * DEFAULT_LAYOUT.colWidth, y: DEFAULT_LAYOUT.startY + row * DEFAULT_LAYOUT.rowHeight, }; } // --------------------------------------------------------------------------- // Synthetic nodes (always present) // --------------------------------------------------------------------------- export function isSyntheticNodeId(id: string): boolean { return id === 'model' || id === 'solver' || id === 'algorithm' || id === 'surrogate'; } function makeModelNode(spec: AtomizerSpec): Node { const pos = toCanvasPosition( spec.model?.sim?.path ? (spec.model as any)?.canvas_position : undefined, makeFallbackPosition(DEFAULT_LAYOUT.cols.model, 0) ); return { id: 'model', type: 'model', position: pos, data: { type: 'model', label: spec.meta?.study_name || 'Model', configured: Boolean(spec.model?.sim?.path), filePath: spec.model?.sim?.path, fileType: 'sim', }, }; } function makeSolverNode(spec: AtomizerSpec): Node { const sim = spec.model?.sim; const pos = makeFallbackPosition(DEFAULT_LAYOUT.cols.solver, 0); return { id: 'solver', type: 'solver', position: pos, data: { type: 'solver', label: sim?.engine ? `Solver (${sim.engine})` : 'Solver', configured: Boolean(sim?.engine || sim?.solution_type), engine: sim?.engine as any, solverType: sim?.solution_type as any, scriptPath: sim?.script_path, }, }; } function makeAlgorithmNode(spec: AtomizerSpec): Node { const algo = spec.optimization?.algorithm; const budget = spec.optimization?.budget; const pos = toCanvasPosition( spec.optimization?.canvas_position, makeFallbackPosition(DEFAULT_LAYOUT.cols.algorithm, 0) ); return { id: 'algorithm', type: 'algorithm', position: pos, data: { type: 'algorithm', label: algo?.type || 'Algorithm', configured: Boolean(algo?.type), method: (algo?.type as any) || 'TPE', maxTrials: budget?.max_trials, sigma0: (algo?.config as any)?.sigma0, restartStrategy: (algo?.config as any)?.restart_strategy, }, }; } function makeSurrogateNode(spec: AtomizerSpec): Node { const surrogate = spec.optimization?.surrogate; const pos = makeFallbackPosition(DEFAULT_LAYOUT.cols.surrogate, 0); const enabled = Boolean(surrogate?.enabled); return { id: 'surrogate', type: 'surrogate', position: pos, data: { type: 'surrogate', label: enabled ? 'Surrogate (enabled)' : 'Surrogate', configured: true, enabled, modelType: (surrogate?.type as any) || 'MLP', minTrials: surrogate?.config?.min_training_samples, }, }; } // --------------------------------------------------------------------------- // Array-backed nodes // --------------------------------------------------------------------------- function makeDesignVarNode(dv: DesignVariable, index: number): Node { const pos = toCanvasPosition( dv.canvas_position, makeFallbackPosition(DEFAULT_LAYOUT.cols.designVar, index) ); return { id: dv.id, type: 'designVar', position: pos, data: { type: 'designVar', label: dv.name, configured: Boolean(dv.expression_name), expressionName: dv.expression_name, minValue: dv.bounds?.min, maxValue: dv.bounds?.max, baseline: dv.baseline, unit: dv.units, enabled: dv.enabled, }, }; } function makeExtractorNode(ext: Extractor, index: number): Node { const pos = toCanvasPosition( ext.canvas_position, makeFallbackPosition(DEFAULT_LAYOUT.cols.extractor, index) ); return { id: ext.id, type: 'extractor', position: pos, data: { type: 'extractor', label: ext.name, configured: true, extractorId: ext.id, extractorName: ext.name, extractorType: ext.type as any, config: ext.config as any, outputNames: (ext.outputs || []).map((o) => o.name), // Convenience fields innerRadius: (ext.config as any)?.inner_radius_mm, nModes: (ext.config as any)?.n_modes, subcases: (ext.config as any)?.subcases, extractMethod: (ext.config as any)?.extract_method, }, }; } function makeObjectiveNode(obj: Objective, index: number): Node { const pos = toCanvasPosition( obj.canvas_position, makeFallbackPosition(DEFAULT_LAYOUT.cols.objective, index) ); return { id: obj.id, type: 'objective', position: pos, data: { type: 'objective', label: obj.name, configured: Boolean(obj.source?.extractor_id && obj.source?.output_name), name: obj.name, direction: obj.direction, weight: obj.weight, extractorRef: obj.source?.extractor_id, outputName: obj.source?.output_name, }, }; } function makeConstraintNode(con: Constraint, index: number): Node { const pos = toCanvasPosition( con.canvas_position, makeFallbackPosition(DEFAULT_LAYOUT.cols.constraint, index) ); return { id: con.id, type: 'constraint', position: pos, data: { type: 'constraint', label: con.name, configured: Boolean(con.source?.extractor_id && con.source?.output_name), name: con.name, operator: con.operator, value: con.threshold, }, }; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- export function specToNodes(spec: AtomizerSpec | null | undefined): Node[] { if (!spec) return []; const nodes: Node[] = []; // Structural nodes nodes.push(makeModelNode(spec) as Node); nodes.push(makeSolverNode(spec) as Node); nodes.push(makeAlgorithmNode(spec) as Node); nodes.push(makeSurrogateNode(spec) as Node); // Array nodes spec.design_variables?.forEach((dv, i) => nodes.push(makeDesignVarNode(dv, i) as Node)); spec.extractors?.forEach((ext, i) => nodes.push(makeExtractorNode(ext, i) as Node)); spec.objectives?.forEach((obj, i) => nodes.push(makeObjectiveNode(obj, i) as Node)); spec.constraints?.forEach((con, i) => nodes.push(makeConstraintNode(con, i) as Node)); return nodes; } export function specToEdges(spec: AtomizerSpec | null | undefined): Edge[] { if (!spec) return []; const edges: Edge[] = []; const seen = new Set(); const add = (source: string, target: string, sourceHandle?: string, targetHandle?: string) => { const id = `${source}__${target}${sourceHandle ? `__${sourceHandle}` : ''}${targetHandle ? `__${targetHandle}` : ''}`; if (seen.has(id)) return; seen.add(id); edges.push({ id, source, target, sourceHandle, targetHandle, }); }; // Prefer explicit canvas edges if present if (spec.canvas?.edges && spec.canvas.edges.length > 0) { for (const e of spec.canvas.edges) { add(e.source, e.target, e.sourceHandle, e.targetHandle); } return edges; } // Fallback: build a minimal visual graph from spec fields (deterministic) // DV → model for (const dv of spec.design_variables || []) add(dv.id, 'model'); // model → solver add('model', 'solver'); // solver → each extractor for (const ext of spec.extractors || []) add('solver', ext.id); // extractor → objective/constraint based on source for (const obj of spec.objectives || []) { if (obj.source?.extractor_id) add(obj.source.extractor_id, obj.id); } for (const con of spec.constraints || []) { if (con.source?.extractor_id) add(con.source.extractor_id, con.id); } // objective/constraint → algorithm for (const obj of spec.objectives || []) add(obj.id, 'algorithm'); for (const con of spec.constraints || []) add(con.id, 'algorithm'); // algorithm → surrogate add('algorithm', 'surrogate'); return edges; }