Set tolerances and budgets
State how close a measurement must come, compare with strict bounds, and set the work and the memory a claim may spend before it is refused.
Every claim carries two limits: a tolerance inside its expectation, which says how close the measurement must come, and a work budget, which says how much work the engine may spend deciding it. Both are recorded in the result, so two readers of one result see the same rule. The engine adds host limits on what it admits and on the memory one analysis may use, set once where the engine is constructed.
Tolerance
A value matches when abs(measured - expected) <= tolerance. The default is 0.02 mm; tolerance: 0 demands the nominal value, which a STEP solid meets and a tessellated mesh rarely does. One tolerance covers every value in the expectation: each axis of a bounding box, the diameter of a hole, a volume. The result keeps the expectation with its tolerance filled in as evidence..
A negative tolerance, or a bounding-box expectation with a tolerance and no bound, refuses before anything is measured, with GEOSPEC_; the plan's other claims still run.
Work budgets
The engine charges work units as it measures, before it reads any cache, so a claim costs the same on every host and every run and never depends on wall time. A claim that spends its budget is refused with MATCHER_TIMEOUT: the diagnostic's details name the matcher, the budget and the units used, and the claim was not decided. The default budget is 8,000,000 units.
workUnitLimit sets the budget per chain, expectGeo(part, { workUnitLimit: 100 }), per loaded model, loadModel({ source, format, workUnitLimit }), or per assertion client. A limit of 0, or above this host's maximum of 512,000,000, throws a TypeError before anything runs. Beside the engine's budget and unitsUsed, the details carry limit, used and maximumLimit.
The budget belongs to the engine: Geo applies it to every claim the engine evaluates, and work_unit_budget=0 raises ValueError. The details carry budget, matcher, unit and unitsUsed.
The gear is a mesh of 624 triangles, and a bounding box over a mesh charges per triangle: 3,744 units for this one. The spec below allows 100, so the claim is refused before it measures anything.
const part = await loadModel({ source: 'involute-gear.glb', format: 'glb', sourceUnit: 'mm' });Tab indents. Press Escape, then Tab, to leave the editor.
The spec as a file, with the model loaded over the native addon
import { readFile } from 'node:fs/promises';import { expectGeo } from 'geospec';import { createModelLoader } from 'geospec/model';import { Engine } from 'geospec-engine/node';const engine = new Engine();const loadModel = createModelLoader({ engine, readSource: async (source) => new Uint8Array(await readFile(source as string)) });const part = await loadModel({ source: 'involute-gear.glb', format: 'glb', sourceUnit: 'mm' });expectGeo(part, { workUnitLimit: 100 }) .toHaveBoundingBox({ size: { z: 5 } });await loadModel.dispose();engine.close();involute-gear.glb · 52 KB · copied from an engine test fixture
Run the spec to see what the engine answers for each claim. Then change an expected value and run it again.
Raise the limit to 10000, workUnitLimit: 10000 here or work_ on the engine, and the claim passes. Then add tolerance: 0 beside size and it fails with GEOSPEC_: the triangles span 4.999976634979248 mm, not 5.
Host limits
The engine is constructed with limits on what it admits and on what one analysis may use. The entry profile, geospec-, admits a subject of 64 MiB with 2,000,000 vertices, 4,000,000 triangles and 65,536 occurrences, holds 32 subjects, and gives one analysis 256 MiB of working memory and 256 mesh entries. A claim whose analysis exceeds a limit is refused with GEOSPEC_, one whose derived evidence exceeds an admission limit with GEOSPEC_, and a model over an admission limit is a ProtocolError at load; the API reference lists every limit with its entry value.
The limits are the third argument of Engine from geospec-engine/node and the second of Engine from geospec-engine: maxSubjectBytes, maxResourceBytes, maxTotalBinaryBytes, maxVertices, maxTriangles, maxOccurrences, maxMeshBytes, maxMeshEntries, maxSolidEntries and maxRetainedSubjects, each a positive integer; an omitted limit keeps its entry value. geospec@0.1.0-beta.6 reads the default budget from the entry profile only, so a chain, model or client on an engine with its own limits sets workUnitLimit itself; without it, expectGeo throws TypeError: GeoSpec engine returned an unsupported configuration profile.
import { readFile } from 'node:fs/promises';
import { expectGeo } from 'geospec';
import { GeoSpecAssertionError } from 'geospec/assertion-client';
import { createModelLoader } from 'geospec/model';
import { Engine } from 'geospec-engine/node';
const engine = new Engine(undefined, undefined, { maxMeshBytes: 65_536 });
const loadModel = createModelLoader({
engine,
readSource: async (source) => new Uint8Array(await readFile(source as string)),
});
const gear = await loadModel({ source: 'involute-gear.glb', format: 'glb', sourceUnit: 'mm' });
try {
expectGeo(gear, { workUnitLimit: 8_000_000 }).toBeWatertight();
} catch (error) {
if (!(error instanceof GeoSpecAssertionError)) throw error;
const [{ code, details, message }] = error.diagnostics;
console.log(error.report.status, code, details.limit, details.required);
console.log(message);
}
await loadModel.dispose();
engine.close();Geo takes the same ten limits in snake_case, max_subject_bytes to max_, each a positive integer; an omitted limit keeps its entry value. For a pytest suite, override the plugin's geospec_ fixture in conftest.py to build Geo.
from geospec import GeoSpecAssertionError, GeoSpecEngine, expect_geo, load_model
with GeoSpecEngine(model_root=".", limits={"max_mesh_bytes": 65_536}):
gear = load_model(file="involute-gear.glb", format="glb", source_unit="mm")
try:
expect_geo(gear).to_be_watertight()
except GeoSpecAssertionError as error:
details = error.diagnostics[0]["details"]
print(error.result["status"], error.code, details["limit"], details["required"])
print(error)refused GEOSPEC_ANALYSIS_LIMIT 65536 67076
Watertight scratch needs 65.5 KiB (67076 bytes); the analysis limit is 64 KiB (65536 bytes).The engine above allows 65,536 bytes of watertight scratch and the gear's 624 triangles need more: the first requirement over the limit, 67,076 bytes, is refused before the claim decides anything, and the WASM engine counts its own allocations, 68,288 bytes for the same claim. The details carry limitClass, unit, limit and required, and the suggestion says to keep the claim and raise the limit: with maxMeshBytes: 131_072, "max_mesh_bytes": 131_072 in Python, the claim passes.
A limit under 1, a maxSubjectBytes or maxResourceBytes above maxTotalBinaryBytes, or a name the engine does not know is rejected before anything is admitted: TypeScript throws ProtocolError invalid-request; Python raises ValueError, or ProtocolError for the total rule. A capacity a proof profile fixes, such as the 64 faces of a contact target, carries the same refusal code and is raised by no limit; its suggestion says so.
Strict comparisons
Where a matcher takes a numeric expectation, write a number, or an object with any of value, greaterThan, greaterThanOrEqual, lessThan and lessThanOrEqual. The tolerance applies to value only; the four comparisons are strict as written. The plate's volume is 30429.20367320504 mm³, so the first claim below passes and the second fails with GEOSPEC_, although its bound is within 0.02 of the measurement: no tolerance widens a comparison. An object with none of the five fields refuses with GEOSPEC_.
const part = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });Tab indents. Press Escape, then Tab, to leave the editor.
The spec as a file, with the model loaded over the native addon
import { readFile } from 'node:fs/promises';import { expectGeo } from 'geospec';import { createModelLoader } from 'geospec/model';import { Engine } from 'geospec-engine/node';const engine = new Engine();const loadModel = createModelLoader({ engine, readSource: async (source) => new Uint8Array(await readFile(source as string)) });const part = await loadModel({ source: 'plate-with-through-bore.step', format: 'step' });expectGeo(part) .toHaveVolume({ value: { greaterThan: 30000, lessThan: 31000 } });expectGeo(part) .toHaveVolume({ value: { greaterThan: 30429.21 } });await loadModel.dispose();engine.close();plate-with-through-bore.step · 19 KB · copied from an engine test fixture
Run the spec to see what the engine answers for each claim. Then change an expected value and run it again.
Never widen a tolerance to pass
A failed claim reports what it measured. Read the measurement, then fix the model or correct the expectation to the value the design states. Widening tolerance until the claim passes records a looser requirement than the design has, and the next part that drifts inside the wider band passes too; the engine's own suggestions end with preserve the authored tolerance. A refusal is not a reason to widen anything either: Read a refusal says what to change instead.
Variations
A tolerance with its own name. to takes toleranceMm, the gap below which two pieces count as one; it defaults to 0.02 mm and the result records it under that name. Check meshes covers it.
What a claim costs. Bounds, closure, volume and mesh integrity on the gear each charge 3,744 units, a component count 49,005 and an inspectGeometry axis selector 52,138. A hole claim on the STEP plate charges 3, and the plate's bounds and volume pass with a budget of 1. Read what a refused claim spent from unitsUsed in its details.
Budgets on queries. A query charges the same way: client.query spends the client's workUnitLimit, query_geo the engine's budget, and analyzeMesh on the gear refuses at a budget of 1 with MATCHER_TIMEOUT. Query geometry covers the queries; the matcher reference lists the refusals every matcher shares.