feat: Add Studio UI, intake system, and extractor improvements
Dashboard: - Add Studio page with drag-drop model upload and Claude chat - Add intake system for study creation workflow - Improve session manager and context builder - Add intake API routes and frontend components Optimization Engine: - Add CLI module for command-line operations - Add intake module for study preprocessing - Add validation module with gate checks - Improve Zernike extractor documentation - Update spec models with better validation - Enhance solve_simulation robustness Documentation: - Add ATOMIZER_STUDIO.md planning doc - Add ATOMIZER_UX_SYSTEM.md for UX patterns - Update extractor library docs - Add study-readme-generator skill Tools: - Add test scripts for extraction validation - Add Zernike recentering test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,7 @@ They provide validation and type safety for the unified configuration system.
|
||||
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Literal, Optional, Union
|
||||
from typing import Any, Dict, List, Literal, Optional, Tuple, Union
|
||||
from pydantic import BaseModel, Field, field_validator, model_validator
|
||||
import re
|
||||
|
||||
@@ -16,17 +16,34 @@ import re
|
||||
# Enums
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class SpecCreatedBy(str, Enum):
|
||||
"""Who/what created the spec."""
|
||||
|
||||
CANVAS = "canvas"
|
||||
CLAUDE = "claude"
|
||||
API = "api"
|
||||
MIGRATION = "migration"
|
||||
MANUAL = "manual"
|
||||
DASHBOARD_INTAKE = "dashboard_intake"
|
||||
|
||||
|
||||
class SpecStatus(str, Enum):
|
||||
"""Study lifecycle status."""
|
||||
|
||||
DRAFT = "draft"
|
||||
INTROSPECTED = "introspected"
|
||||
CONFIGURED = "configured"
|
||||
VALIDATED = "validated"
|
||||
READY = "ready"
|
||||
RUNNING = "running"
|
||||
COMPLETED = "completed"
|
||||
FAILED = "failed"
|
||||
|
||||
|
||||
class SolverType(str, Enum):
|
||||
"""Supported solver types."""
|
||||
|
||||
NASTRAN = "nastran"
|
||||
NX_NASTRAN = "NX_Nastran"
|
||||
ABAQUS = "abaqus"
|
||||
@@ -34,6 +51,7 @@ class SolverType(str, Enum):
|
||||
|
||||
class SubcaseType(str, Enum):
|
||||
"""Subcase analysis types."""
|
||||
|
||||
STATIC = "static"
|
||||
MODAL = "modal"
|
||||
THERMAL = "thermal"
|
||||
@@ -42,6 +60,7 @@ class SubcaseType(str, Enum):
|
||||
|
||||
class DesignVariableType(str, Enum):
|
||||
"""Design variable types."""
|
||||
|
||||
CONTINUOUS = "continuous"
|
||||
INTEGER = "integer"
|
||||
CATEGORICAL = "categorical"
|
||||
@@ -49,6 +68,7 @@ class DesignVariableType(str, Enum):
|
||||
|
||||
class ExtractorType(str, Enum):
|
||||
"""Physics extractor types."""
|
||||
|
||||
DISPLACEMENT = "displacement"
|
||||
FREQUENCY = "frequency"
|
||||
STRESS = "stress"
|
||||
@@ -62,18 +82,21 @@ class ExtractorType(str, Enum):
|
||||
|
||||
class OptimizationDirection(str, Enum):
|
||||
"""Optimization direction."""
|
||||
|
||||
MINIMIZE = "minimize"
|
||||
MAXIMIZE = "maximize"
|
||||
|
||||
|
||||
class ConstraintType(str, Enum):
|
||||
"""Constraint types."""
|
||||
|
||||
HARD = "hard"
|
||||
SOFT = "soft"
|
||||
|
||||
|
||||
class ConstraintOperator(str, Enum):
|
||||
"""Constraint comparison operators."""
|
||||
|
||||
LE = "<="
|
||||
GE = ">="
|
||||
LT = "<"
|
||||
@@ -83,6 +106,7 @@ class ConstraintOperator(str, Enum):
|
||||
|
||||
class PenaltyMethod(str, Enum):
|
||||
"""Penalty methods for constraints."""
|
||||
|
||||
LINEAR = "linear"
|
||||
QUADRATIC = "quadratic"
|
||||
EXPONENTIAL = "exponential"
|
||||
@@ -90,6 +114,7 @@ class PenaltyMethod(str, Enum):
|
||||
|
||||
class AlgorithmType(str, Enum):
|
||||
"""Optimization algorithm types."""
|
||||
|
||||
TPE = "TPE"
|
||||
CMA_ES = "CMA-ES"
|
||||
NSGA_II = "NSGA-II"
|
||||
@@ -100,6 +125,7 @@ class AlgorithmType(str, Enum):
|
||||
|
||||
class SurrogateType(str, Enum):
|
||||
"""Surrogate model types."""
|
||||
|
||||
MLP = "MLP"
|
||||
GNN = "GNN"
|
||||
ENSEMBLE = "ensemble"
|
||||
@@ -109,58 +135,104 @@ class SurrogateType(str, Enum):
|
||||
# Position Model
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CanvasPosition(BaseModel):
|
||||
"""Canvas position for nodes."""
|
||||
|
||||
x: float = 0
|
||||
y: float = 0
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Introspection Models (for intake workflow)
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ExpressionInfo(BaseModel):
|
||||
"""Information about an NX expression from introspection."""
|
||||
|
||||
name: str = Field(..., description="Expression name in NX")
|
||||
value: Optional[float] = Field(default=None, description="Current value")
|
||||
units: Optional[str] = Field(default=None, description="Physical units")
|
||||
formula: Optional[str] = Field(default=None, description="Expression formula if any")
|
||||
is_candidate: bool = Field(
|
||||
default=False, description="Whether this is a design variable candidate"
|
||||
)
|
||||
confidence: float = Field(
|
||||
default=0.0, ge=0.0, le=1.0, description="Confidence that this is a DV"
|
||||
)
|
||||
|
||||
|
||||
class BaselineData(BaseModel):
|
||||
"""Results from baseline FEA solve."""
|
||||
|
||||
timestamp: datetime = Field(..., description="When baseline was run")
|
||||
solve_time_seconds: float = Field(..., description="How long the solve took")
|
||||
mass_kg: Optional[float] = Field(default=None, description="Computed mass from BDF/FEM")
|
||||
max_displacement_mm: Optional[float] = Field(
|
||||
default=None, description="Max displacement result"
|
||||
)
|
||||
max_stress_mpa: Optional[float] = Field(default=None, description="Max von Mises stress")
|
||||
success: bool = Field(default=True, description="Whether baseline solve succeeded")
|
||||
error: Optional[str] = Field(default=None, description="Error message if failed")
|
||||
|
||||
|
||||
class IntrospectionData(BaseModel):
|
||||
"""Model introspection results stored in the spec."""
|
||||
|
||||
timestamp: datetime = Field(..., description="When introspection was run")
|
||||
solver_type: Optional[str] = Field(default=None, description="Detected solver type")
|
||||
mass_kg: Optional[float] = Field(
|
||||
default=None, description="Mass from expressions or properties"
|
||||
)
|
||||
volume_mm3: Optional[float] = Field(default=None, description="Volume from mass properties")
|
||||
expressions: List[ExpressionInfo] = Field(
|
||||
default_factory=list, description="Discovered expressions"
|
||||
)
|
||||
baseline: Optional[BaselineData] = Field(default=None, description="Baseline solve results")
|
||||
warnings: List[str] = Field(default_factory=list, description="Warnings from introspection")
|
||||
|
||||
def get_design_candidates(self) -> List[ExpressionInfo]:
|
||||
"""Return expressions marked as design variable candidates."""
|
||||
return [e for e in self.expressions if e.is_candidate]
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Meta Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class SpecMeta(BaseModel):
|
||||
"""Metadata about the spec."""
|
||||
version: str = Field(
|
||||
...,
|
||||
pattern=r"^2\.\d+$",
|
||||
description="Schema version (e.g., '2.0')"
|
||||
)
|
||||
created: Optional[datetime] = Field(
|
||||
default=None,
|
||||
description="When the spec was created"
|
||||
)
|
||||
|
||||
version: str = Field(..., pattern=r"^2\.\d+$", description="Schema version (e.g., '2.0')")
|
||||
created: Optional[datetime] = Field(default=None, description="When the spec was created")
|
||||
modified: Optional[datetime] = Field(
|
||||
default=None,
|
||||
description="When the spec was last modified"
|
||||
default=None, description="When the spec was last modified"
|
||||
)
|
||||
created_by: Optional[SpecCreatedBy] = Field(
|
||||
default=None,
|
||||
description="Who/what created the spec"
|
||||
)
|
||||
modified_by: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Who/what last modified the spec"
|
||||
default=None, description="Who/what created the spec"
|
||||
)
|
||||
modified_by: Optional[str] = Field(default=None, description="Who/what last modified the spec")
|
||||
study_name: str = Field(
|
||||
...,
|
||||
min_length=3,
|
||||
max_length=100,
|
||||
pattern=r"^[a-z0-9_]+$",
|
||||
description="Unique study identifier (snake_case)"
|
||||
description="Unique study identifier (snake_case)",
|
||||
)
|
||||
description: Optional[str] = Field(
|
||||
default=None,
|
||||
max_length=1000,
|
||||
description="Human-readable description"
|
||||
)
|
||||
tags: Optional[List[str]] = Field(
|
||||
default=None,
|
||||
description="Tags for categorization"
|
||||
default=None, max_length=1000, description="Human-readable description"
|
||||
)
|
||||
tags: Optional[List[str]] = Field(default=None, description="Tags for categorization")
|
||||
engineering_context: Optional[str] = Field(
|
||||
default=None, description="Real-world engineering context"
|
||||
)
|
||||
status: SpecStatus = Field(default=SpecStatus.DRAFT, description="Study lifecycle status")
|
||||
topic: Optional[str] = Field(
|
||||
default=None,
|
||||
description="Real-world engineering context"
|
||||
pattern=r"^[A-Za-z0-9_]+$",
|
||||
description="Topic folder for grouping related studies",
|
||||
)
|
||||
|
||||
|
||||
@@ -168,15 +240,20 @@ class SpecMeta(BaseModel):
|
||||
# Model Configuration Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class NxPartConfig(BaseModel):
|
||||
"""NX geometry part file configuration."""
|
||||
|
||||
path: Optional[str] = Field(default=None, description="Path to .prt file")
|
||||
hash: Optional[str] = Field(default=None, description="File hash for change detection")
|
||||
idealized_part: Optional[str] = Field(default=None, description="Idealized part filename (_i.prt)")
|
||||
idealized_part: Optional[str] = Field(
|
||||
default=None, description="Idealized part filename (_i.prt)"
|
||||
)
|
||||
|
||||
|
||||
class FemConfig(BaseModel):
|
||||
"""FEM mesh file configuration."""
|
||||
|
||||
path: Optional[str] = Field(default=None, description="Path to .fem file")
|
||||
element_count: Optional[int] = Field(default=None, description="Number of elements")
|
||||
node_count: Optional[int] = Field(default=None, description="Number of nodes")
|
||||
@@ -184,6 +261,7 @@ class FemConfig(BaseModel):
|
||||
|
||||
class Subcase(BaseModel):
|
||||
"""Simulation subcase definition."""
|
||||
|
||||
id: int
|
||||
name: Optional[str] = None
|
||||
type: Optional[SubcaseType] = None
|
||||
@@ -191,18 +269,18 @@ class Subcase(BaseModel):
|
||||
|
||||
class SimConfig(BaseModel):
|
||||
"""Simulation file configuration."""
|
||||
|
||||
path: str = Field(..., description="Path to .sim file")
|
||||
solver: SolverType = Field(..., description="Solver type")
|
||||
solution_type: Optional[str] = Field(
|
||||
default=None,
|
||||
pattern=r"^SOL\d+$",
|
||||
description="Solution type (e.g., SOL101)"
|
||||
default=None, pattern=r"^SOL\d+$", description="Solution type (e.g., SOL101)"
|
||||
)
|
||||
subcases: Optional[List[Subcase]] = Field(default=None, description="Defined subcases")
|
||||
|
||||
|
||||
class NxSettings(BaseModel):
|
||||
"""NX runtime settings."""
|
||||
|
||||
nx_install_path: Optional[str] = None
|
||||
simulation_timeout_s: Optional[int] = Field(default=None, ge=60, le=7200)
|
||||
auto_start_nx: Optional[bool] = None
|
||||
@@ -210,23 +288,31 @@ class NxSettings(BaseModel):
|
||||
|
||||
class ModelConfig(BaseModel):
|
||||
"""NX model files and configuration."""
|
||||
|
||||
nx_part: Optional[NxPartConfig] = None
|
||||
fem: Optional[FemConfig] = None
|
||||
sim: SimConfig
|
||||
sim: Optional[SimConfig] = Field(
|
||||
default=None, description="Simulation file config (required for optimization)"
|
||||
)
|
||||
nx_settings: Optional[NxSettings] = None
|
||||
introspection: Optional[IntrospectionData] = Field(
|
||||
default=None, description="Model introspection results from intake"
|
||||
)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Design Variable Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class DesignVariableBounds(BaseModel):
|
||||
"""Design variable bounds."""
|
||||
|
||||
min: float
|
||||
max: float
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_bounds(self) -> 'DesignVariableBounds':
|
||||
@model_validator(mode="after")
|
||||
def validate_bounds(self) -> "DesignVariableBounds":
|
||||
if self.min >= self.max:
|
||||
raise ValueError(f"min ({self.min}) must be less than max ({self.max})")
|
||||
return self
|
||||
@@ -234,16 +320,13 @@ class DesignVariableBounds(BaseModel):
|
||||
|
||||
class DesignVariable(BaseModel):
|
||||
"""A design variable to optimize."""
|
||||
id: str = Field(
|
||||
...,
|
||||
pattern=r"^dv_\d{3}$",
|
||||
description="Unique identifier (pattern: dv_XXX)"
|
||||
)
|
||||
|
||||
id: str = Field(..., pattern=r"^dv_\d{3}$", description="Unique identifier (pattern: dv_XXX)")
|
||||
name: str = Field(..., description="Human-readable name")
|
||||
expression_name: str = Field(
|
||||
...,
|
||||
pattern=r"^[a-zA-Z_][a-zA-Z0-9_]*$",
|
||||
description="NX expression name (must match model)"
|
||||
description="NX expression name (must match model)",
|
||||
)
|
||||
type: DesignVariableType = Field(..., description="Variable type")
|
||||
bounds: DesignVariableBounds = Field(..., description="Value bounds")
|
||||
@@ -259,8 +342,10 @@ class DesignVariable(BaseModel):
|
||||
# Extractor Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ExtractorConfig(BaseModel):
|
||||
"""Type-specific extractor configuration."""
|
||||
|
||||
inner_radius_mm: Optional[float] = None
|
||||
outer_radius_mm: Optional[float] = None
|
||||
n_modes: Optional[int] = None
|
||||
@@ -279,6 +364,7 @@ class ExtractorConfig(BaseModel):
|
||||
|
||||
class CustomFunction(BaseModel):
|
||||
"""Custom function definition for custom_function extractors."""
|
||||
|
||||
name: Optional[str] = Field(default=None, description="Function name")
|
||||
module: Optional[str] = Field(default=None, description="Python module path")
|
||||
signature: Optional[str] = Field(default=None, description="Function signature")
|
||||
@@ -287,32 +373,33 @@ class CustomFunction(BaseModel):
|
||||
|
||||
class ExtractorOutput(BaseModel):
|
||||
"""Output definition for an extractor."""
|
||||
|
||||
name: str = Field(..., description="Output name (used by objectives/constraints)")
|
||||
metric: Optional[str] = Field(default=None, description="Specific metric (max, total, rms, etc.)")
|
||||
metric: Optional[str] = Field(
|
||||
default=None, description="Specific metric (max, total, rms, etc.)"
|
||||
)
|
||||
subcase: Optional[int] = Field(default=None, description="Subcase ID for this output")
|
||||
units: Optional[str] = None
|
||||
|
||||
|
||||
class Extractor(BaseModel):
|
||||
"""Physics extractor that computes outputs from FEA."""
|
||||
id: str = Field(
|
||||
...,
|
||||
pattern=r"^ext_\d{3}$",
|
||||
description="Unique identifier (pattern: ext_XXX)"
|
||||
)
|
||||
|
||||
id: str = Field(..., pattern=r"^ext_\d{3}$", description="Unique identifier (pattern: ext_XXX)")
|
||||
name: str = Field(..., description="Human-readable name")
|
||||
type: ExtractorType = Field(..., description="Extractor type")
|
||||
builtin: bool = Field(default=True, description="Whether this is a built-in extractor")
|
||||
config: Optional[ExtractorConfig] = Field(default=None, description="Type-specific configuration")
|
||||
config: Optional[ExtractorConfig] = Field(
|
||||
default=None, description="Type-specific configuration"
|
||||
)
|
||||
function: Optional[CustomFunction] = Field(
|
||||
default=None,
|
||||
description="Custom function definition (for custom_function type)"
|
||||
default=None, description="Custom function definition (for custom_function type)"
|
||||
)
|
||||
outputs: List[ExtractorOutput] = Field(..., min_length=1, description="Output values")
|
||||
canvas_position: Optional[CanvasPosition] = None
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_custom_function(self) -> 'Extractor':
|
||||
@model_validator(mode="after")
|
||||
def validate_custom_function(self) -> "Extractor":
|
||||
if self.type == ExtractorType.CUSTOM_FUNCTION and self.function is None:
|
||||
raise ValueError("custom_function extractor requires function definition")
|
||||
return self
|
||||
@@ -322,19 +409,18 @@ class Extractor(BaseModel):
|
||||
# Objective Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ObjectiveSource(BaseModel):
|
||||
"""Source reference for objective value."""
|
||||
|
||||
extractor_id: str = Field(..., description="Reference to extractor")
|
||||
output_name: str = Field(..., description="Which output from the extractor")
|
||||
|
||||
|
||||
class Objective(BaseModel):
|
||||
"""Optimization objective."""
|
||||
id: str = Field(
|
||||
...,
|
||||
pattern=r"^obj_\d{3}$",
|
||||
description="Unique identifier (pattern: obj_XXX)"
|
||||
)
|
||||
|
||||
id: str = Field(..., pattern=r"^obj_\d{3}$", description="Unique identifier (pattern: obj_XXX)")
|
||||
name: str = Field(..., description="Human-readable name")
|
||||
direction: OptimizationDirection = Field(..., description="Optimization direction")
|
||||
weight: float = Field(default=1.0, ge=0, description="Weight for weighted sum")
|
||||
@@ -349,14 +435,17 @@ class Objective(BaseModel):
|
||||
# Constraint Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ConstraintSource(BaseModel):
|
||||
"""Source reference for constraint value."""
|
||||
|
||||
extractor_id: str
|
||||
output_name: str
|
||||
|
||||
|
||||
class PenaltyConfig(BaseModel):
|
||||
"""Penalty method configuration for constraints."""
|
||||
|
||||
method: Optional[PenaltyMethod] = None
|
||||
weight: Optional[float] = None
|
||||
margin: Optional[float] = Field(default=None, description="Soft margin before penalty kicks in")
|
||||
@@ -364,11 +453,8 @@ class PenaltyConfig(BaseModel):
|
||||
|
||||
class Constraint(BaseModel):
|
||||
"""Hard or soft constraint."""
|
||||
id: str = Field(
|
||||
...,
|
||||
pattern=r"^con_\d{3}$",
|
||||
description="Unique identifier (pattern: con_XXX)"
|
||||
)
|
||||
|
||||
id: str = Field(..., pattern=r"^con_\d{3}$", description="Unique identifier (pattern: con_XXX)")
|
||||
name: str
|
||||
type: ConstraintType = Field(..., description="Constraint type")
|
||||
operator: ConstraintOperator = Field(..., description="Comparison operator")
|
||||
@@ -383,8 +469,10 @@ class Constraint(BaseModel):
|
||||
# Optimization Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AlgorithmConfig(BaseModel):
|
||||
"""Algorithm-specific settings."""
|
||||
|
||||
population_size: Optional[int] = None
|
||||
n_generations: Optional[int] = None
|
||||
mutation_prob: Optional[float] = None
|
||||
@@ -399,22 +487,24 @@ class AlgorithmConfig(BaseModel):
|
||||
|
||||
class Algorithm(BaseModel):
|
||||
"""Optimization algorithm configuration."""
|
||||
|
||||
type: AlgorithmType
|
||||
config: Optional[AlgorithmConfig] = None
|
||||
|
||||
|
||||
class OptimizationBudget(BaseModel):
|
||||
"""Computational budget for optimization."""
|
||||
|
||||
max_trials: Optional[int] = Field(default=None, ge=1, le=10000)
|
||||
max_time_hours: Optional[float] = None
|
||||
convergence_patience: Optional[int] = Field(
|
||||
default=None,
|
||||
description="Stop if no improvement for N trials"
|
||||
default=None, description="Stop if no improvement for N trials"
|
||||
)
|
||||
|
||||
|
||||
class SurrogateConfig(BaseModel):
|
||||
"""Neural surrogate model configuration."""
|
||||
|
||||
n_models: Optional[int] = None
|
||||
architecture: Optional[List[int]] = None
|
||||
train_every_n_trials: Optional[int] = None
|
||||
@@ -425,6 +515,7 @@ class SurrogateConfig(BaseModel):
|
||||
|
||||
class Surrogate(BaseModel):
|
||||
"""Surrogate model settings."""
|
||||
|
||||
enabled: Optional[bool] = None
|
||||
type: Optional[SurrogateType] = None
|
||||
config: Optional[SurrogateConfig] = None
|
||||
@@ -432,6 +523,7 @@ class Surrogate(BaseModel):
|
||||
|
||||
class OptimizationConfig(BaseModel):
|
||||
"""Optimization algorithm configuration."""
|
||||
|
||||
algorithm: Algorithm
|
||||
budget: OptimizationBudget
|
||||
surrogate: Optional[Surrogate] = None
|
||||
@@ -442,8 +534,10 @@ class OptimizationConfig(BaseModel):
|
||||
# Workflow Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class WorkflowStage(BaseModel):
|
||||
"""A stage in a multi-stage optimization workflow."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
algorithm: Optional[str] = None
|
||||
@@ -453,6 +547,7 @@ class WorkflowStage(BaseModel):
|
||||
|
||||
class WorkflowTransition(BaseModel):
|
||||
"""Transition between workflow stages."""
|
||||
|
||||
from_: str = Field(..., alias="from")
|
||||
to: str
|
||||
condition: Optional[str] = None
|
||||
@@ -463,6 +558,7 @@ class WorkflowTransition(BaseModel):
|
||||
|
||||
class Workflow(BaseModel):
|
||||
"""Multi-stage optimization workflow."""
|
||||
|
||||
stages: Optional[List[WorkflowStage]] = None
|
||||
transitions: Optional[List[WorkflowTransition]] = None
|
||||
|
||||
@@ -471,8 +567,10 @@ class Workflow(BaseModel):
|
||||
# Reporting Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class InsightConfig(BaseModel):
|
||||
"""Insight-specific configuration."""
|
||||
|
||||
include_html: Optional[bool] = None
|
||||
show_pareto_evolution: Optional[bool] = None
|
||||
|
||||
@@ -482,6 +580,7 @@ class InsightConfig(BaseModel):
|
||||
|
||||
class Insight(BaseModel):
|
||||
"""Reporting insight definition."""
|
||||
|
||||
type: Optional[str] = None
|
||||
for_trials: Optional[str] = None
|
||||
config: Optional[InsightConfig] = None
|
||||
@@ -489,6 +588,7 @@ class Insight(BaseModel):
|
||||
|
||||
class ReportingConfig(BaseModel):
|
||||
"""Reporting configuration."""
|
||||
|
||||
auto_report: Optional[bool] = None
|
||||
report_triggers: Optional[List[str]] = None
|
||||
insights: Optional[List[Insight]] = None
|
||||
@@ -498,8 +598,10 @@ class ReportingConfig(BaseModel):
|
||||
# Canvas Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class CanvasViewport(BaseModel):
|
||||
"""Canvas viewport settings."""
|
||||
|
||||
x: float = 0
|
||||
y: float = 0
|
||||
zoom: float = 1.0
|
||||
@@ -507,6 +609,7 @@ class CanvasViewport(BaseModel):
|
||||
|
||||
class CanvasEdge(BaseModel):
|
||||
"""Connection between canvas nodes."""
|
||||
|
||||
source: str
|
||||
target: str
|
||||
sourceHandle: Optional[str] = None
|
||||
@@ -515,6 +618,7 @@ class CanvasEdge(BaseModel):
|
||||
|
||||
class CanvasGroup(BaseModel):
|
||||
"""Grouping of canvas nodes."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
node_ids: List[str]
|
||||
@@ -522,6 +626,7 @@ class CanvasGroup(BaseModel):
|
||||
|
||||
class CanvasConfig(BaseModel):
|
||||
"""Canvas UI state (persisted for reconstruction)."""
|
||||
|
||||
layout_version: Optional[str] = None
|
||||
viewport: Optional[CanvasViewport] = None
|
||||
edges: Optional[List[CanvasEdge]] = None
|
||||
@@ -532,6 +637,7 @@ class CanvasConfig(BaseModel):
|
||||
# Main AtomizerSpec Model
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class AtomizerSpec(BaseModel):
|
||||
"""
|
||||
AtomizerSpec v2.0 - The unified configuration schema for Atomizer optimization studies.
|
||||
@@ -542,36 +648,32 @@ class AtomizerSpec(BaseModel):
|
||||
- Claude Assistant (reading and modifying)
|
||||
- Optimization Engine (execution)
|
||||
"""
|
||||
|
||||
meta: SpecMeta = Field(..., description="Metadata about the spec")
|
||||
model: ModelConfig = Field(..., description="NX model files and configuration")
|
||||
design_variables: List[DesignVariable] = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
default_factory=list,
|
||||
max_length=50,
|
||||
description="Design variables to optimize"
|
||||
description="Design variables to optimize (required for running)",
|
||||
)
|
||||
extractors: List[Extractor] = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
description="Physics extractors"
|
||||
default_factory=list, description="Physics extractors (required for running)"
|
||||
)
|
||||
objectives: List[Objective] = Field(
|
||||
...,
|
||||
min_length=1,
|
||||
default_factory=list,
|
||||
max_length=5,
|
||||
description="Optimization objectives"
|
||||
description="Optimization objectives (required for running)",
|
||||
)
|
||||
constraints: Optional[List[Constraint]] = Field(
|
||||
default=None,
|
||||
description="Hard and soft constraints"
|
||||
default=None, description="Hard and soft constraints"
|
||||
)
|
||||
optimization: OptimizationConfig = Field(..., description="Algorithm configuration")
|
||||
workflow: Optional[Workflow] = Field(default=None, description="Multi-stage workflow")
|
||||
reporting: Optional[ReportingConfig] = Field(default=None, description="Reporting config")
|
||||
canvas: Optional[CanvasConfig] = Field(default=None, description="Canvas UI state")
|
||||
|
||||
@model_validator(mode='after')
|
||||
def validate_references(self) -> 'AtomizerSpec':
|
||||
@model_validator(mode="after")
|
||||
def validate_references(self) -> "AtomizerSpec":
|
||||
"""Validate that all references are valid."""
|
||||
# Collect valid extractor IDs and their outputs
|
||||
extractor_outputs: Dict[str, set] = {}
|
||||
@@ -638,13 +740,44 @@ class AtomizerSpec(BaseModel):
|
||||
"""Check if this is a multi-objective optimization."""
|
||||
return len(self.objectives) > 1
|
||||
|
||||
def is_ready_for_optimization(self) -> Tuple[bool, List[str]]:
|
||||
"""
|
||||
Check if spec is complete enough to run optimization.
|
||||
|
||||
Returns:
|
||||
Tuple of (is_ready, list of missing requirements)
|
||||
"""
|
||||
missing = []
|
||||
|
||||
# Check required fields for optimization
|
||||
if not self.model.sim:
|
||||
missing.append("No simulation file (.sim) configured")
|
||||
|
||||
if not self.design_variables:
|
||||
missing.append("No design variables defined")
|
||||
|
||||
if not self.extractors:
|
||||
missing.append("No extractors defined")
|
||||
|
||||
if not self.objectives:
|
||||
missing.append("No objectives defined")
|
||||
|
||||
# Check that enabled DVs have valid bounds
|
||||
for dv in self.get_enabled_design_variables():
|
||||
if dv.bounds.min >= dv.bounds.max:
|
||||
missing.append(f"Design variable '{dv.name}' has invalid bounds")
|
||||
|
||||
return len(missing) == 0, missing
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# Validation Response Models
|
||||
# ============================================================================
|
||||
|
||||
|
||||
class ValidationError(BaseModel):
|
||||
"""A validation error."""
|
||||
|
||||
type: str # 'schema', 'semantic', 'reference'
|
||||
path: List[str]
|
||||
message: str
|
||||
@@ -652,6 +785,7 @@ class ValidationError(BaseModel):
|
||||
|
||||
class ValidationWarning(BaseModel):
|
||||
"""A validation warning."""
|
||||
|
||||
type: str
|
||||
path: List[str]
|
||||
message: str
|
||||
@@ -659,6 +793,7 @@ class ValidationWarning(BaseModel):
|
||||
|
||||
class ValidationSummary(BaseModel):
|
||||
"""Summary of spec contents."""
|
||||
|
||||
design_variables: int
|
||||
extractors: int
|
||||
objectives: int
|
||||
@@ -668,6 +803,7 @@ class ValidationSummary(BaseModel):
|
||||
|
||||
class ValidationReport(BaseModel):
|
||||
"""Full validation report."""
|
||||
|
||||
valid: bool
|
||||
errors: List[ValidationError]
|
||||
warnings: List[ValidationWarning]
|
||||
|
||||
Reference in New Issue
Block a user