Files
Atomizer/mcp-server/atomizer-tools/src/tools/physics.ts
Anto01 73a7b9d9f1 feat: Add dashboard chat integration and MCP server
Major changes:
- Dashboard: WebSocket-based chat with session management
- Dashboard: New chat components (ChatPane, ChatInput, ModeToggle)
- Dashboard: Enhanced UI with parallel coordinates chart
- MCP Server: New atomizer-tools server for Claude integration
- Extractors: Enhanced Zernike OPD extractor
- Reports: Improved report generator

New studies (configs and scripts only):
- M1 Mirror: Cost reduction campaign studies
- Simple Beam, Simple Bracket, UAV Arm studies

Note: Large iteration data (2_iterations/, best_design_archive/)
excluded via .gitignore - kept on local Gitea only.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 15:53:55 -05:00

408 lines
12 KiB
TypeScript

/**
* Physics Tools
*
* Tools for FEA explanations and method recommendations.
*/
import { readdir, readFile } from "fs/promises";
import { resolve } from "path";
import { AtomizerTool } from "../index.js";
import { ATOMIZER_ROOT, OPTIMIZATION_ENGINE_DIR } from "../utils/paths.js";
// Physics concepts knowledge base
const PHYSICS_CONCEPTS: Record<string, string> = {
displacement: `
**Displacement** is the change in position of a point in a structure under load.
In FEA, nodal displacements are the primary unknowns solved for.
Key metrics:
- Maximum displacement: Largest movement in any direction
- RMS displacement: Root-mean-square of all nodal displacements
- Directional displacement: Movement in a specific axis (X, Y, or Z)
Units: millimeters (mm) or inches (in)
Atomizer extractors: extract_displacement, extract_max_displacement
`,
stress: `
**Stress** is the internal force per unit area within a material.
In FEA, stress is computed from strain (displacement gradients).
Types:
- Von Mises stress: Combined stress for ductile materials (most common)
- Principal stresses: Maximum/minimum normal stresses
- Shear stress: Stress parallel to a surface
Key considerations:
- Compare to material yield strength for safety factor
- Stress concentrations occur at corners, holes, and thickness changes
Units: MPa (N/mm²) or psi
Atomizer extractors: extract_solid_stress, extract_von_mises_stress
`,
frequency: `
**Natural Frequency** is the rate at which a structure vibrates when disturbed.
Modal analysis finds these frequencies and their corresponding mode shapes.
Key concepts:
- Fundamental frequency: Lowest natural frequency (Mode 1)
- Mode shape: Pattern of vibration at each frequency
- Resonance: When excitation frequency matches natural frequency (dangerous!)
Design goals:
- Keep natural frequencies above or below operating frequencies
- Separate modes to avoid coupling
Units: Hz (cycles/second)
Atomizer extractors: extract_frequency, extract_modal_participation
`,
mass: `
**Mass** is the total material quantity in a structure.
In optimization, mass is often a key objective to minimize.
Calculation methods:
- From CAD: NX expression-based calculation (most accurate)
- From FEM: Sum of element masses (approximation)
- From BDF: Calculated from element properties
Related metrics:
- Center of gravity (CG): Balance point
- Moments of inertia: Rotational mass distribution
Units: kg or lbm
Atomizer extractors: extract_mass_from_expression, extract_mass_from_bdf
`,
zernike: `
**Zernike Polynomials** are mathematical functions for describing optical aberrations.
Used to characterize mirror surface deformation in precision optics.
Key coefficients:
- Z4 (Defocus): Curvature change
- Z5, Z6 (Astigmatism): Cylindrical deformation
- Z7, Z8 (Coma): Comet-like aberration
- Z11 (Spherical): Radial curvature variation
Fitting method:
1. Extract nodal displacements from FEA
2. Fit to Zernike polynomial basis
3. Report RMS WFE (wavefront error)
Units: nanometers (nm) or waves
Atomizer extractors: extract_zernike_figure, extract_zernike_annular
`,
convergence: `
**Optimization Convergence** indicates when the search has found a good solution.
Signs of convergence:
- Best value stops improving for many trials
- Low variance in recent trial results
- Improvement rate drops below threshold
When to stop:
- Converged: Best hasn't improved in 20+ trials
- Plateau: Improvement rate < 1% per 10 trials
- Budget exhausted: Reached max_trials
Tips:
- Don't stop too early - TPE needs ~50 trials to learn
- Consider switching to local optimizer after convergence
- Check multiple random seeds for robustness
`,
};
// Method recommendation logic
const METHOD_RECOMMENDATIONS: Record<string, { method: string; reason: string }> = {
"1_obj_few_dv": {
method: "TPE",
reason: "Tree-structured Parzen Estimator works well for single-objective with few design variables. It balances exploration and exploitation efficiently.",
},
"1_obj_many_dv": {
method: "CMA-ES",
reason: "Covariance Matrix Adaptation is effective for high-dimensional single-objective problems. It learns correlations between variables.",
},
"multi_obj": {
method: "NSGA-II",
reason: "Non-dominated Sorting Genetic Algorithm II finds Pareto-optimal solutions for multi-objective problems. Good for exploring trade-offs.",
},
"noisy": {
method: "RandomSearch + averaging",
reason: "For noisy objectives, random search with trial averaging is robust. Consider multiple FEA runs per trial.",
},
"expensive": {
method: "TPE + surrogate",
reason: "For expensive FEA (>5 min/eval), use TPE with neural network surrogate for pre-screening. See SYS_14 Neural Acceleration.",
},
};
export const physicsTools: AtomizerTool[] = [
{
definition: {
name: "explain_physics",
description:
"Explain FEA physics concepts relevant to optimization. Use this to help users understand displacement, stress, frequency, mass, or other physics quantities.",
inputSchema: {
type: "object" as const,
properties: {
concept: {
type: "string",
enum: [
"displacement",
"stress",
"frequency",
"mass",
"zernike",
"convergence",
],
description: "The physics concept to explain",
},
context: {
type: "string",
description:
"Optional context about the user's situation for tailored explanation",
},
},
required: ["concept"],
},
},
handler: async (args) => {
const concept = args.concept as string;
const context = args.context as string | undefined;
const explanation = PHYSICS_CONCEPTS[concept];
if (!explanation) {
return {
content: [
{
type: "text",
text: `Unknown concept: ${concept}. Available concepts: ${Object.keys(PHYSICS_CONCEPTS).join(", ")}`,
},
],
isError: true,
};
}
let response = explanation.trim();
if (context) {
response += `\n\n---\n\n**For your context** (${context}):\n`;
response +=
"Consider how this concept applies to your specific optimization goals.";
}
return {
content: [
{
type: "text",
text: response,
},
],
};
},
},
{
definition: {
name: "recommend_method",
description:
"Recommend an optimization method based on problem characteristics. Considers number of objectives, design variables, noise level, and evaluation cost.",
inputSchema: {
type: "object" as const,
properties: {
num_objectives: {
type: "number",
description: "Number of objectives (1 for single-objective)",
},
num_design_variables: {
type: "number",
description: "Number of design variables",
},
evaluation_time_minutes: {
type: "number",
description: "Approximate time for one FEA evaluation in minutes",
},
is_noisy: {
type: "boolean",
description: "Whether the objective has noise/variability",
},
budget_trials: {
type: "number",
description: "Maximum number of trials you can afford",
},
},
required: ["num_objectives", "num_design_variables"],
},
},
handler: async (args) => {
const numObjectives = args.num_objectives as number;
const numDV = args.num_design_variables as number;
const evalTime = (args.evaluation_time_minutes as number) || 1;
const isNoisy = args.is_noisy as boolean;
const budget = (args.budget_trials as number) || 100;
let key: string;
if (isNoisy) {
key = "noisy";
} else if (numObjectives > 1) {
key = "multi_obj";
} else if (evalTime > 5) {
key = "expensive";
} else if (numDV > 10) {
key = "1_obj_many_dv";
} else {
key = "1_obj_few_dv";
}
const rec = METHOD_RECOMMENDATIONS[key];
const response = {
recommended_method: rec.method,
reason: rec.reason,
problem_characteristics: {
objectives: numObjectives,
design_variables: numDV,
evaluation_time_minutes: evalTime,
is_noisy: isNoisy,
budget: budget,
},
estimated_trials_needed: numObjectives === 1 ? Math.min(budget, 50 + numDV * 5) : Math.min(budget, 100 + numDV * 10),
tips: [
`With ${numDV} design variables, expect ~${Math.min(budget, 30 + numDV * 3)} trials before meaningful convergence.`,
evalTime > 5
? "Consider using neural surrogate acceleration (see SYS_14)."
: "Standard optimization should work well.",
budget < 50 ? "Low budget - consider random search for baseline first." : "",
].filter(Boolean),
};
return {
content: [
{
type: "text",
text: JSON.stringify(response, null, 2),
},
],
};
},
},
{
definition: {
name: "query_extractors",
description:
"List available physics extractors in Atomizer for extracting results from FEA solutions.",
inputSchema: {
type: "object" as const,
properties: {
physics_type: {
type: "string",
enum: [
"displacement",
"stress",
"frequency",
"mass",
"zernike",
"all",
],
description: "Filter by physics type, or 'all' for complete list",
},
},
},
},
handler: async (args) => {
const physicsType = (args.physics_type as string) || "all";
const extractorsDir = resolve(OPTIMIZATION_ENGINE_DIR, "extractors");
try {
const files = await readdir(extractorsDir);
const extractorFiles = files.filter(
(f) => f.startsWith("extract_") && f.endsWith(".py")
);
const extractors: { name: string; physics: string; file: string }[] = [];
for (const file of extractorFiles) {
const name = file.replace("extract_", "").replace(".py", "");
// Determine physics type from name
let physics: string;
if (name.includes("displacement") || name.includes("deform")) {
physics = "displacement";
} else if (name.includes("stress") || name.includes("mises")) {
physics = "stress";
} else if (name.includes("freq") || name.includes("modal")) {
physics = "frequency";
} else if (name.includes("mass") || name.includes("weight")) {
physics = "mass";
} else if (name.includes("zernike") || name.includes("wfe")) {
physics = "zernike";
} else {
physics = "other";
}
if (physicsType === "all" || physics === physicsType) {
extractors.push({ name, physics, file });
}
}
// Read __init__.py for exported functions
const initPath = resolve(extractorsDir, "__init__.py");
let exports: string[] = [];
try {
const initContent = await readFile(initPath, "utf-8");
const match = initContent.match(/__all__\s*=\s*\[([\s\S]*?)\]/);
if (match) {
exports = match[1]
.split(",")
.map((s) => s.trim().replace(/['"]/g, ""))
.filter(Boolean);
}
} catch {
// Ignore if __init__.py not readable
}
return {
content: [
{
type: "text",
text: JSON.stringify(
{
filter: physicsType,
count: extractors.length,
extractors,
exported_functions: exports,
usage_example:
"from optimization_engine.extractors import extract_displacement",
},
null,
2
),
},
],
};
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
return {
content: [
{
type: "text",
text: `Error listing extractors: ${message}`,
},
],
isError: true,
};
}
},
},
];