Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions changelog/entries/2026-07-11-physics-spec-persistence.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "2026-07-11-physics-spec-persistence",
"version": "0.9.4",
"date": "2026-07-11",
"category": "feat",
"title": "Persisted physics assertions re-verify via receipts",
"summary": "predict_physics gains a label arg: the spec persists on the document, build_receipt re-solves it as physics.static claims, and verify_receipt classifies Holds/Stale/Violated.",
"features": ["physics", "receipt", "verification"],
"mcpTools": ["predict_physics", "build_receipt", "verify_receipt"]
}
103 changes: 103 additions & 0 deletions crates/vcad-ir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1807,6 +1807,10 @@ pub struct Document {
/// `check_clearance` and receipt verification whenever geometry changes.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub clearance_specs: Vec<ClearanceSpec>,
/// Named static-physics assertions (loads, supports, limits), re-solved by
/// `predict_physics` and receipt verification whenever geometry changes.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub physics_specs: Vec<PhysicsSpec>,
}

/// A named minimum-clearance assertion between two groups of parts.
Expand All @@ -1829,6 +1833,104 @@ pub struct ClearanceSpec {
pub min_mm: f64,
}

/// Axis-aligned box region (mm, world frame, Z-up) selecting FEA grid nodes.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export, export_to = "bindings/"))]
pub struct PhysicsRegion {
/// Minimum corner `[x, y, z]` in mm.
pub min: [f64; 3],
/// Maximum corner `[x, y, z]` in mm.
pub max: [f64; 3],
}

/// A load for static analysis: total force spread over a box region.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export, export_to = "bindings/"))]
pub struct PhysicsLoad {
/// Region the force is distributed over.
pub region: PhysicsRegion,
/// Total force vector `[fx, fy, fz]` in N.
pub force: [f64; 3],
}

/// A fixed (anchored) region for static analysis.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export, export_to = "bindings/"))]
pub struct PhysicsSupport {
/// Region whose grid nodes are anchored.
pub region: PhysicsRegion,
/// Which translations are fixed `[x, y, z]`; absent means all fixed.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts-rs", ts(optional))]
pub fix: Option<[bool; 3]>,
}

/// Solve tier for a persisted physics spec: which resolution the assertion is
/// re-verified at, and which claim basis the resulting claims carry
/// (`predict` → predicted/provisional, `verify` → verified).
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export, export_to = "bindings/"))]
pub enum PhysicsFidelity {
/// Coarse fast solve; claims stamped `basis: predicted`.
Predict,
/// Fine solve, same oracle; claims stamped `basis: verified`.
Verify,
}

/// A named static-structural assertion (voxel FEA) persisted on the document.
///
/// Stores everything needed to re-run the solve — loads, supports, material
/// properties, limits, and fidelity — so `build_receipt` re-emits it as
/// `physics.static.<label>.*` claims and `verify_receipt` re-solves and
/// classifies it Holds / Stale / Violated as geometry changes.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export, export_to = "bindings/"))]
pub struct PhysicsSpec {
/// Unique human-readable name, e.g. "bracket-load".
pub label: String,
/// Part id (stringified root node id) whose evaluated volume is analyzed.
/// Mutually exclusive with `domain_box`.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts-rs", ts(optional))]
pub part: Option<String>,
/// Analyze a solid box instead of a part. Mutually exclusive with `part`.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts-rs", ts(optional))]
pub domain_box: Option<PhysicsRegion>,
/// Applied loads.
pub loads: Vec<PhysicsLoad>,
/// Fixed supports.
pub supports: Vec<PhysicsSupport>,
/// Young's modulus in MPa; absent means the kernel default (6061 Al).
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts-rs", ts(optional))]
pub youngs_modulus_mpa: Option<f64>,
/// Poisson's ratio; absent means the kernel default.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts-rs", ts(optional))]
pub poisson: Option<f64>,
/// Voxels along the longest axis; absent means the fidelity-tier default.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts-rs", ts(optional))]
pub resolution: Option<u32>,
/// Solve tier the assertion is (re-)verified at.
pub fidelity: PhysicsFidelity,
/// Optional limit: max displacement ≤ this, in mm.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts-rs", ts(optional))]
pub max_displacement_mm: Option<f64>,
/// Optional limit: max von Mises stress ≤ this, in MPa.
#[serde(default, skip_serializing_if = "Option::is_none")]
#[cfg_attr(feature = "ts-rs", ts(optional))]
pub max_von_mises_mpa: Option<f64>,
}

impl Default for Document {
fn default() -> Self {
Self {
Expand All @@ -1848,6 +1950,7 @@ impl Default for Document {
parameters: HashMap::new(),
bindings: Bindings::new(),
clearance_specs: Vec::new(),
physics_specs: Vec::new(),
}
}
}
Expand Down
108 changes: 107 additions & 1 deletion packages/ir/src/generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,12 @@ bindings?: Bindings,
* Named clearance/clash assertions between part groups, re-measured by
* `check_clearance` and receipt verification whenever geometry changes.
*/
clearance_specs?: Array<ClearanceSpec>, };
clearance_specs?: Array<ClearanceSpec>,
/**
* Named static-physics assertions (loads, supports, limits), re-solved by
* `predict_physics` and receipt verification whenever geometry changes.
*/
physics_specs?: Array<PhysicsSpec>, };

/**
* A canonicalized DRC summary: total, per-rule counts (sorted), and the
Expand Down Expand Up @@ -2213,6 +2218,107 @@ netTies?: Array<NetTie>, };
*/
export type PcbLayer = "FCu" | "BCu" | "In1Cu" | "In2Cu" | "In3Cu" | "In4Cu" | "In5Cu" | "In6Cu" | "FSilkS" | "BSilkS" | "FMask" | "BMask" | "FPaste" | "BPaste" | "FFab" | "BFab" | "FCrtYd" | "BCrtYd" | "EdgeCuts" | "UserDrawings" | "UserComments";

/**
* Solve tier for a persisted physics spec: which resolution the assertion is
* re-verified at, and which claim basis the resulting claims carry
* (`predict` → predicted/provisional, `verify` → verified).
*/
export type PhysicsFidelity = "predict" | "verify";

/**
* A load for static analysis: total force spread over a box region.
*/
export type PhysicsLoad = {
/**
* Region the force is distributed over.
*/
region: PhysicsRegion,
/**
* Total force vector `[fx, fy, fz]` in N.
*/
force: [number, number, number], };

/**
* Axis-aligned box region (mm, world frame, Z-up) selecting FEA grid nodes.
*/
export type PhysicsRegion = {
/**
* Minimum corner `[x, y, z]` in mm.
*/
min: [number, number, number],
/**
* Maximum corner `[x, y, z]` in mm.
*/
max: [number, number, number], };

/**
* A named static-structural assertion (voxel FEA) persisted on the document.
*
* Stores everything needed to re-run the solve — loads, supports, material
* properties, limits, and fidelity — so `build_receipt` re-emits it as
* `physics.static.<label>.*` claims and `verify_receipt` re-solves and
* classifies it Holds / Stale / Violated as geometry changes.
*/
export type PhysicsSpec = {
/**
* Unique human-readable name, e.g. "bracket-load".
*/
label: string,
/**
* Part id (stringified root node id) whose evaluated volume is analyzed.
* Mutually exclusive with `domain_box`.
*/
part?: string,
/**
* Analyze a solid box instead of a part. Mutually exclusive with `part`.
*/
domain_box?: PhysicsRegion,
/**
* Applied loads.
*/
loads: Array<PhysicsLoad>,
/**
* Fixed supports.
*/
supports: Array<PhysicsSupport>,
/**
* Young's modulus in MPa; absent means the kernel default (6061 Al).
*/
youngs_modulus_mpa?: number,
/**
* Poisson's ratio; absent means the kernel default.
*/
poisson?: number,
/**
* Voxels along the longest axis; absent means the fidelity-tier default.
*/
resolution?: number,
/**
* Solve tier the assertion is (re-)verified at.
*/
fidelity: PhysicsFidelity,
/**
* Optional limit: max displacement ≤ this, in mm.
*/
max_displacement_mm?: number,
/**
* Optional limit: max von Mises stress ≤ this, in MPa.
*/
max_von_mises_mpa?: number, };

/**
* A fixed (anchored) region for static analysis.
*/
export type PhysicsSupport = {
/**
* Region whose grid nodes are anchored.
*/
region: PhysicsRegion,
/**
* Which translations are fixed `[x, y, z]`; absent means all fixed.
*/
fix?: [boolean, boolean, boolean], };

/**
* A single pin's identity within a package.
*/
Expand Down
Loading