Modeling and realization
Current-source Python API. Install this source revision, then run and inspect. Canonical guide source.
Native declarations
Python declarations are immutable inputs to the same typed draft, validation, transaction, and canonical artifact path used by other Eqiora clients. Python does not implement a second model semantics.
import eqiora
x = eqiora.Field("x", role=eqiora.FieldRole.State)rate = eqiora.Parameter( "rate", value=1.0, value_type=eqiora.ValueType.real(eqiora.Dimension(time=-1)),)flow = eqiora.Relation( "flow", equations=((eqiora.derivative(x) + rate * x, 0),),)model = eqiora.Model.define("decay", x, rate, eqiora.Initial((x, 1)), flow)Field.value_type holds its mathematical scalar domain, physical dimension and
component roles. FieldRole.Variable declares an algebraic unknown;
FieldRole.State declares evolution or history independently of support.
Omitted types are dimensionless real scalars. Initial supplies simultaneous
ordered left/right equation pairs for fresh initialization; the Field stores no initial literal.
Fresh scalar ODE and admitted index-one DAE initialization checks the initial and
regular equations together. Missing state data, contradictory constraints, or an
unsupported initialization profile reject; this is not a general high-index DAE
solver. Restart uses an accepted State/history without reapplying these equations.
voltage = eqiora.ValueType.complex(eqiora.Dimension(mass=1, length=2, time=-3, current=-1))body = eqiora.Domain.box("body", (0.0, 1.0), (0.0, 1.0))channels = eqiora.Field( "channels", role=eqiora.FieldRole.Variable, domain=body, value_type=eqiora.ValueType.array(eqiora.ValueType.vector(voltage, 2), 3),)ValueType.real(dimension) and ValueType.complex(dimension) construct scalars.
vector(scalar, extent) and tensor(scalar, *extents) introduce spatial axes;
array(element, extent) adds a channel axis without changing the element’s frame.
Equal component counts do not make these types interchangeable. Spatial extents
must match the Field’s exact Domain. Complex execution is still under development.
Dimensions accept exact rational exponents, for example
eqiora.Dimension(length=Fraction(-1, 2)) with Fraction imported from fractions.
Initial equations follow expression typing: nonzero dimensioned constants need
an explicit compatible quantity. A scalar is not broadcast into a vector, tensor,
or array initial state. Distributed execution retains its admitted explicit
initial-data owner; a declaration alone does not establish executable initialization.
The same value_type= objects apply to Parameter,
eqiora.lang.Component.field, and eqiora.lang.Component.parameter.
ValueType.to_eqi() emits the canonical type through the Rust formatter.
Parameters accept real or complex scalars and nested channel sequences matching the declared shape. Inspection returns immutable nested tuples with every real/imaginary component:
coefficients = eqiora.Parameter( "coefficients", value_type=eqiora.ValueType.array(eqiora.ValueType.complex(), 2), value=[1 + 2j, 3 - 4j],)assert coefficients.value == (1 + 2j, 3 - 4j)selected = coefficients[1]Nonzero spatial coefficients carry an explicit frame context from an existing Domain. Their values remain uniform Parameters:
body = eqiora.Domain.box("body", (0, 1), (0, 1))kind = eqiora.ValueType.tensor(eqiora.ValueType.real(), 2, 2)coefficient = eqiora.Parameter( "coefficient", value_type=kind, value=((2, 3), (5, 7)), frame=body,)response = eqiora.Field( "response", role=eqiora.FieldRole.Variable, domain=body, value_type=kind,)law = eqiora.Relation("law", equations=((response, coefficient),), domain=body)model = eqiora.Model.define("Coefficients", body, coefficient, response, law)assert model.parameter("coefficient").value == ((2, 3), (5, 7))Include the frame Domain in Model.define; foreign or omitted declarations reject.
The Domain supplies the model-global Cartesian frame and ambient dimension, not
Parameter support. Values retain real/imaginary components and axis order through
inspection, edits, and replay. A channel array of tensors remains distinct from one
spatial tensor. Python Source uses the same constructor as
q.tensor_value(frame=body, components=((2, 3), (5, 7))), where body is an exact
Component.volume or Component.boundary handle from that Component. The resulting
expression can supply a Parameter default through Component.set_default. Constructor
components must be closed scalar expressions; referencing a named model value, including
a Parameter alias, inside the constructor rejects.
Indices are static exact nonnegative integers; mutable Parameters cannot supply indices. Typed value edits preserve the complete declared type and all components through replay. This authoring support does not establish a complex numerical solver.
Declare value_type=eqiora.ValueType.integer() to retain Python integers exactly,
including values above 2**53. Values must be signed 64-bit integers; booleans,
floating-point values and overflow reject. Without an explicit integer type,
ordinary numeric defaults keep their real interpretation. ParameterRef.value
returns the exact typed value, including after edits and Model replay.
species = eqiora.FiniteSpace("Species", labels=("A", "B"))population = eqiora.Parameter( "population", value_type=eqiora.ValueType.counts(species), value=(2, 9007199254740993),)observed = eqiora.Field("observed", role=eqiora.FieldRole.Variable)relation = eqiora.Relation("observation", equations=((observed, 0),))model = eqiora.Model.define("Population", species, population, observed, relation)assert model.parameter("population").value == (2, 9007199254740993)ValueType.coordinates(species) holds signed integer components in the same
ordered basis; counts(species) requires nonnegative components. Equal labels in
another FiniteSpace do not establish the same type. IndexSet("Rows", extent=3)
and ValueType.index(rows) similarly retain a distinct nominal identity and admit
only ordinals from zero through two. Include each native declaration in
Model.define. Nominal types need their declaration’s lexical scope for source
rendering, so their standalone to_eqi() rejects.
Python Source registers spaces with source.space(...) and constant-sized sets
with component.index_set(..., extent=3). Its counts, coordinates, and index
constructors require handles from the owning Source or Component. The closed
eqiora.lang.quotient, remainder, to_real, to_integer, and ordinal
expressions use the shared compiler’s explicit conversion and arithmetic rules.
Products, dual spaces, general maps, and dynamic indexing remain
outside this bounded discrete profile.
Finite scalar reductions bind one symbolic index through Component.sum or Component.product:
rows = component.index_set("Rows", extent=3)total = component.sum(lambda i: (q.ordinal(i) + 1) ** 2, over=rows)component.let_alias("total", total)Here q is eqiora.lang. The callback runs once to author the body; the compiler expands
its three terms and obtains 14. A nested reduction needs a distinct name, such as
name="j". Binders cannot escape their callback or capture another declaration, and the
set and captured declarations must belong to the same Component. An array expression accepts
values[q.ordinal(i)] within this scope. General runtime indexing remains unsupported.
The finite reduction rules define
ordering, scalar types, product units, expansion bounds and unsupported initializer contexts.
A numeric Parameter default uses the declared dimension’s coherent unit.
For example, parameter rate: 1 / s = 1; gives the same value as
parameter rate: 1 / s = 1[1 / s];. Explicit input units still express compatible
conversions. This context applies only to numeric Parameter defaults;
nonzero literals in general expressions do not silently acquire units.
A native Relation receives ordered equations=((left, right), ...) pairs.
Use (residual, 0) for a numerical residual equation. Named equal, not_equal,
less, less_equal, greater, and greater_equal functions produce predicates;
logical_not, logical_and, and logical_or compose them. Python == retains
handle identity, and symbolic Python truth testing rejects. Declarations and expressions
are frozen; validation and artifact creation happen atomically in Rust.
Author Eqiora Language source
eqiora.lang.Source is the equations-language route when a workflow should be
fully Python-authored without creating a second equation semantics:
import eqiorafrom eqiora import lang as qfrom eqiora import units as u
source = q.Source()component = source.component("Diffusion")body = component.volume("body", dimensions=2)value = component.field("value", on=body, role=eqiora.FieldRole.Variable, value_type=eqiora.ValueType.real(eqiora.Dimension(length=1)))length = component.parameter("length", value_type=eqiora.ValueType.real(eqiora.Dimension(length=1)))wave_number = component.let_alias("wave_number", q.math.pi / length)component.relation( "balance", on=body, left=q.div(q.grad(value)), right=-(wave_number**2) * value,)
text = source.to_eqi()model = eqiora.compile( source=source, entry="Diffusion", geometry=geometry, bindings={"body": geometry.selection("body"), "length": 1.0},)Source Relations require an ordered left= and right= pair, including an
explicit right=0 for a residual equation. They emit left = right;; Python == is
not overloaded. The Source draft owns exact supports and expressions, rejects
foreign handles and resource-limit violations, and freezes on its first emission or compile.
It emits ordinary readable UTF-8 .eqi; doc= values become attached ///
documentation. A blank paragraph is emitted as an empty /// line, keeping the
block attached to its declaration. Documentation is bounded to 16,384 UTF-8 bytes.
write_eqi(path) uses same-directory staging and atomic replacement, so an I/O
failure does not publish a partly written source file.
A Source can contain multiple Components within its existing declaration bound.
Use parent.instance(...) to bind a child’s requirements explicitly, and select the
entry with eqiora.compile(source=source, entry="Parent", ...) when the source
contains multiple public Components. A Source containing property contracts still
requires the exact Model Package compilation path described below.
component.let_alias(name, expression) declares a private immutable expression alias.
Its type and spatial support are inferred; value_type= asserts the inferred type,
and on= asserts the exact inferred Support owned by this Component.
Aliases can use the component’s Parameters, fields, and other aliases. For example,
heat_flux = component.let_alias("heat_flux", coefficient * q.grad(potential))
can appear in a volume relation as q.div(heat_flux). A Parameter-only alias can also
supply a nested-instance Parameter argument; a field-dependent alias cannot.
Aliases do not become required parameters, independent edit targets, unknowns, or equations.
Each occurrence retains the original dependencies, so Parameter edits and differentiation
pass through the expression. Expressions must have an intrinsically inferable spatial support:
keep context-dependent coordinate, trace, and normal expressions in their relations.
Writing on= cannot give a constant spatial support or select an unspecified boundary.
An equal-shaped, separately declared Support is still a different nominal support.
at=clock asserts that the expression’s runtime dependencies belong to that exact
Component-owned Clock. Equal periods do not make different Clocks interchangeable.
Static expressions and mixtures of continuous and clocked dependencies cannot assert a
single clock. State operators retain their exact-clock and initialization rules through
aliases; reading a current state through an alias adds no clock restriction.
Create nominal periodic clocks with exact seconds, using an integer or fractions.Fraction:
from fractions import Fraction
tick = component.clock("tick", period_s=Fraction(1, 10))memory = component.field( "memory", on=body, role=eqiora.FieldRole.State, value_type=eqiora.ValueType.real(), at=tick,)component.initial(left=q.pre(memory), right=1)observed = component.let_alias("observed", memory, on=body, at=tick)component.relation( "update", on=body, at=tick, left=q.next(memory), right=q.pre(memory),)The optional phase_s defaults to zero. Initial equations are simultaneous, and the
clock’s first tick follows initialization. Clock handles belong to their declaring
Component; foreign handles are rejected before changing a declaration. q.pre and
q.next use the same Rust state-role and use-context checks as emitted source.
This authoring path does not extend the execution backends’ admitted spatial time models.
Source values do not type-check or lower equations in Python. Direct compile
materializes source.to_eqi() and enters the same Rust parser, type checker,
lowerer, Geometry/support binder, and compiler used by a file path. Consequently,
direct and emitted-file compilation with identical bindings have the same Model
meaning and identity. Prose changes affect source bytes and package source-bundle
identity, not the physical Model. Compiler failures retain the existing structured
diagnostics.
q.math.pi is one immutable, ownerless Source expression that emits exactly
math.pi; q.math.sin(expression) emits the matching compiler-owned scalar
operation. Composing either with a Source-owned expression adopts that Source’s
existing ownership. The top-level Source vocabulary is reserved for equation
structure such as q.grad and q.div, while scalar functions and constants
live under q.math. They are not Python numerical operations, and the native
compiler remains the authority for their typing and value semantics.
The same Source owner can emit the bounded constant property declarations used by an exact Model Package:
source = q.Source()contract = source.property_contract("Diffusivity", value_type=eqiora.ValueType.real())release = source.property_release( "ReferenceDiffusivity", implements=contract, value=25, source_unit=u.one, source_scale=0.001, citation="org.example.measurement", license="spdx.CC0_1_0",)law = source.component("DiffusionLaw")law_body = law.volume("body", dimensions=2)diffusivity = law.property("diffusivity", contract=contract)value = law.field("value", on=law_body, role=eqiora.FieldRole.Variable, value_type=eqiora.ValueType.real())law.relation( "balance", on=law_body, left=-q.div(diffusivity * q.grad(value)), right=0,)material = source.material_composition( "ReferenceMaterial", properties={"diffusivity": release},)root = source.component("DiffusionProblem")root_body = root.volume("body", dimensions=2)root.instance( "equation", component=law, bindings={law_body: root_body, diffusivity: material["diffusivity"]},)source.write_eqi("src/property-diffusion.eqi")The composition mapping may contain several releases. Each instance binding
selects a member explicitly, such as diffusivity = ReferenceMaterial.diffusivity.
Compilation checks that every required property is supplied exactly once and
that each release implements the required nominal contract.
Contracts and releases authored in the same Source can compile locally through
eqiora.compile(source=source, entry=..., bindings=...). The shared Rust compiler
checks each exact release or composition member against its nominal contract;
a same-spelled handle from another Source is rejected before emission.
To retain exact package provenance, emit the .eqi into a Model Package, lock
it, and use compile_package. That route exposes the existing immutable
property_bindings inspection. Local compilation does not synthesize package
lineage, and Python does not normalize or evaluate the property itself.
The complete current vocabulary and steady-cylinder Component are shown in
examples/python/steady_cylinder_source.py.
The baseline slice has one public Component, public volume/parent-boundary
supports and parameters, typed scalar, spatial-vector/tensor and channel-array
continuum fields, continuous residual Relations, structural SI units, constants,
coordinates, arithmetic,
powers, gradient, divergence, trace, normal contraction, symmetric part, and
isotropic lift. The package-oriented extension admits multiple scalar contracts
and constant releases, one material composition, one consumer plus one root
Component, and complete direct or composed bindings.
Resolve and lock a local package project
An installed Eqiora distribution can add an exact standard fluid or solid dependency to an existing project through the same manifest/lock transaction:
import eqiora
resolution = eqiora.add_bundled_dependency( ".", "package-store", "Eqiora.Fluid.Incompressible", version="0.4.0")Create the store directory first. The request must match the exact release
shipped in the distribution. The solid package is
Eqiora.Solid.LinearElasticity, version 0.6.0.
The manifest records version = "0.4.0" and bundled = true for the fluid
dependency; local dependencies instead record an explicit path.
eqiora.toml is the author-maintained project and package manifest. It owns the
canonical name, exact version, source root, entry module, and direct
dependencies:
[package]name = "org.example.application"version = "0.1.0"source = "src"entry = "models.main"
[dependencies."org.example.materials"]version = "1.0.0"path = "packages/materials"
[dependencies."org.example.components"]version = "2.1.0"path = "packages/components"Each dependency directory contains its own eqiora.toml. Dependency paths are
relative to the declaring manifest; ../library explicitly selects a sibling
outside the project directory. Absolute paths and parent segments after named
directories are rejected. Each package’s source root remains confined to that
package directory. Package names, not local aliases, authorize imports.
entry selects a module relative to the source root: models.main selects
src/models/main.eqi with the default source root. The generated package
manifest retains this selection for offline compilation. entry_model selects
a Model in that module or through one of its explicit imports.
Python resolves that project into the store and atomically writes its current
project lock to eqiora.lock:
from pathlib import Path
import eqiora
store_root = Path("package-store")store_root.mkdir()resolution = eqiora.resolve_local_project(".", store_root)assert eqiora.open_project(".", store_root) == resolution
model = eqiora.compile_package( store_root, resolution, entry="materials.Calibration",)To move the project offline, create a destination directory and copy the accepted
closure with eqiora.vendor_project(".", store_root, "vendor"). After moving the
project, eqiora.open_project(".", "vendor") returns the validated resolution for
compile_package. Reopening checks current root sources and every locked package
without reading external dependency paths or selecting bundled releases.
fetch_project fills a store from the explicit source requests only if they still
match the accepted lock. update_project explicitly re-derives that lock from the
current sources. Compilation and reopening never update requests or fetch packages.
The CLI uses the same operations: package add --bundled, package fetch,
package update, package vendor --destination, and package check, each with
the project path and --store.
Explicit Git sources
On Linux, add a public HTTPS repository or an explicit absolute/.//../ local
repository path through the same project owner:
resolution = eqiora.add_git_dependency( ".", "package-store", "org.example.Materials", version="1.0.0", repository="https://example.org/materials.git", revision="refs/heads/main",)The CLI equivalent is eqiora package add . org.example.Materials --version 1.0.0 --git https://example.org/materials.git --rev refs/heads/main --store package-store.
Revisions are lowercase full 40-digit commit IDs or explicit refs/heads/... /
refs/tags/... names. Arbitrary revision expressions are rejected.
eqiora.lock is a project envelope containing the exact semantic resolution and
immutable Git commit selections. API return bytes remain the semantic resolution
accepted by compile_package; they are not the entire project lock. fetch_project
retains the accepted commit even if its branch moves. update_project explicitly
resolves the authored request again. open_project, compile and run never invoke Git.
Git acquisition requires /usr/bin/git, /usr/bin/prlimit and home-backed TMPDIR.
Each acquisition has a 90-second deadline; Git runs with 1 GiB address space,
64 MiB per-file storage, 60 CPU seconds, 64 open descriptors and bounded output.
Admission limits are 64 MiB stored inventory / 10,000 entries, 4,096 source files,
32 directory levels, 8 MiB per expanded blob and 32 MiB total source bytes.
One project acquires at most 16 repositories with nesting depth 8.
There is no checkout, hook/filter execution or submodule acquisition. Local repository configuration and object alternates are not accepted. Git tree symlinks, gitlinks, path escapes and conflicting paths fail before publication. Fetched package-local dependencies stay inside the fetched tree; fetched packages cannot select ambient local Git repositories. HTTPS is unauthenticated, with redirects and credential helpers disabled; userinfo, query strings and fragments are rejected. Unsupported containment environments fail instead of running an uncontained fetch.
The shared Rust owner opens manifest-relative paths without following symbolic
links, discovers bounded .eqi inventories, generates each closed package
manifest, prepares the exact graph leaf-first, and publishes the lock only
after the complete closure is installed.
An optional package-root README.md is retained as documentation in the exact
source bundle; it is never compiled as model source.
Use eqiora.add_local_dependency(project_root, store_root, name, version="1.0.0", path="packages/library") to add or replace a direct dependency, and
eqiora.remove_local_dependency(project_root, store_root, name) to remove it.
Both return the new semantic resolution bytes. The complete candidate is validated before the
manifest and lock are published; a failed update preserves the accepted pair.
Remove source imports before removing a dependency they require.
The CLI uses the same operations:
eqiora package lock . --store package-storeeqiora package add . org.example.Library --version 1.0.0 --path packages/library --store package-storeeqiora package remove . org.example.Library --store package-storeeqiora package check . --store package-store --entry-model MainIf a process stops during publication, locked compilation reads the previous accepted lock. The next explicit update recovers the saved pair before resolving. Concurrent project writes are rejected; retry after the other operation finishes.
Compile one exact locked package Model or Component
Python can bind an existing content-addressed package’s public Component to
caller-owned Geometry and produce the same ordinary immutable Model used by
local source compilation. Here support_bindings explicitly maps every support
name in the selected signature to a Geometry selection; each boundary maps to
(boundary_selection, parent_selection):
from pathlib import Path
import eqiora
store_root = Path("package-store")resolution = Path("resolution.canonical.json").read_bytes()model = eqiora.compile_package( store_root, resolution, geometry=geometry, entry="PoissonRectangle", bindings={**support_bindings, "wave_number": 3.14159, "source_scale": 19.7392},)
print(model.digest)print(model.package_compilation_digest)for binding in model.property_bindings: print(binding.contract, binding.release, binding.normalized_value) print(binding.validity, binding.citation, binding.license)The caller selects one explicit store directory and supplies the exact bytes
from ResolutionRecordV1.canonical_json(). The required entry= names the
selected public Model or Component. bindings= explicitly supplies its required
signature inputs, with geometry= authenticating any Geometry selections. Rust
verifies the complete locked closure and uses the same compiler-owned graph
for both declaration kinds.
Human-formatted, reordered, newline-terminated, duplicate-key, or
store-mismatched resolution bytes fail closed. Missing or ambiguous support
bindings fail instead of matching Geometry by bounds, coordinates, or digest.
package_compilation_digest is read-only lineage for the accepted compilation.
When the package binds an exact typed constant property release, property_bindings is
an immutable projection of the compiler-owned optional composition, contract, release, consuming
Component, requirement, complete value type and coherent-SI value, validity, citation, and license. It
is inspection metadata beside the compilation, not a second property evaluator.
The resulting Model enters ordinary eqiora.resolve(model, mesh=..., ...) and
eqiora.run(plan); its Plan and Run retain the same digest. Bare Model JSON
still carries Model/Geometry meaning but not the package sidecar, so replayed
Models use the same resolver with package_compilation_digest is None and an
empty property_bindings tuple. Package lineage persistence belongs to the
symmetric Model artifact I/O work. This
surface does not discover stores or lock files, access registries or networks,
or add a Studio package workflow.
Check one exact package structurally
An external package author can check the same locked closure without turning the check into a scientific or execution claim:
report = eqiora.check_package_conformance( store_root, resolution, entry_model="Main", profile="eqiora.package.structural-conformance-v1",)
print(report.packages)print(report.package_compilation_digest)print(report.model_digest)The read-only operation accepts one explicit store, exact canonical resolution
bytes, one bare root-local Model selector, and the exact profile token shown
above. It compiles and replays the closure twice through the existing package
and current Model boundaries, then returns immutable in-process facts only
after package-compilation and Model identity agree. Rejections raise the
existing structured EqioraError family and return no partial report.
This is structural compatibility only. The conformance fixture deliberately includes scientifically false documentation that still passes: a report does not establish physical truth, well-posedness, realizability, solver support, accuracy, convergence, performance, or verified physics. It executes no package code or tests and supplies no registry, discovery, installation, publishing, signature, trust, badge, attestation, durable report wire, scientific-evidence lookup, execution workflow, or Studio surface.
Authored CAD to exact geometry
The first accepted path projects one closed authored-CAD history into its exact transverse Geometry. Python names the two native-owned sketch inputs and does not implement their operations:
import eqiora
graph = eqiora.geometry.GeometryGraph()rectangle = graph.rectangle(x_bounds=(0.0, 2.2), y_bounds=(0.0, 0.41))circle = graph.circle(center=(0.2, 0.2), radius=0.05)fluid = graph.subtract(rectangle, circle)geometry = graph.build( fluid, named_topology={ "fluid": fluid.region, "inlet": rectangle.boundaries[0], "outlet": rectangle.boundaries[1], "walls": rectangle.boundaries[2:], "cylinder": circle.boundaries[0], },)
assert geometry.selection_dimension("fluid") == 2assert geometry.selection_dimension("cylinder") == 1print(geometry.digest)Rust owns validation, graph binding, operation order, canonical ordering,
bytes, and exact handle identity. Every coordinate and radius is a coherent-SI
metre.
The same GeometryGraph owns solid authoring through
graph.rectangle_extrusion(...) and
graph.circular_through_cut(solid, ...), producing the existing exact
canonical operations. The solid operation retains its explicit depth and CAD tolerances;
none enter the derived 2D Geometry, whose classification tolerance is supplied
separately. The circle remains centre-and-radius geometry, so chord count,
mesh size, and approximation tolerance cannot enter it. A general Sketch,
arbitrary planes or profiles, operation DAGs, general Booleans or sections,
multiple holes, Model binding, solve, Result, Studio, and visualization remain
separate slices. Installed Python exposes the common Geometry projection
only through the accepted authored graph; it does not publish a demo-shaped
constructor.
Bounded Gmsh mesh
The matching meshing operation is an explicit typed provider choice:
request = eqiora.meshing.GmshMesher( maximum_boundary_error=1e-4, maximum_target_size=0.05, minimum_mean_ratio=1e-5, maximum_boundary_facets=50,)plan = eqiora.meshing.resolve(geometry, request)mesh = eqiora.meshing.generate(plan)
assert mesh.source_digest == geometry.digestprint(mesh.digest)resolve is planning-only: it retains the exact source and derives the bounded
subdivision receipt directly from Geometry and policy without launching Gmsh or
constructing cells. generate then invokes exact Gmsh 4.15.2 once for that
call, admits its MSH 4.1 linear triangles, and derives
realized named selections through the geometry-to-mesh correspondence.
maximum_target_size=None leaves the global characteristic-size ceiling to the
provider; a finite positive value makes that ceiling caller-owned. The resolved
value and its automatic/explicit ownership are retained in production lineage.
It is a Gmsh characteristic target, not a guarantee on every realized edge.
canonical_bytes and digest identify only the accepted inner simplicial
mesh. The returned object retains source, correspondence, Mesh, and
provider-production identities. Missing, wrong-version, failed, or invalid
Gmsh output rejects without falling back to the retired spoke mesh.
This bounded operation supports the rectangle-with-circular-hole family and affine 2D triangles. It does not add caller-owned MSH import, paths, fields, multiple pieces, 3D, curved elements, repair, local or adaptive sizing, general Geometry matching, fixed output counts, or cross-platform byte identity.
Exact-cylinder steady Stokes result
The first fluid application keeps the component’s equations, fields,
dimensions, Parameters, and abstract support names in the installed .eqi
source. Python is the sole owner of concrete shape and size. compile checks
that exact Geometry selections close the selected public Component, derives
Parameter dimensions from its declarations, and returns the ordinary immutable
Model used by every resolver:
from importlib.resources import files
model = eqiora.compile( path=files(eqiora).joinpath("examples", "steady-flow-past-cylinder.eqi"), entry="SteadyFlowPastCylinder", geometry=geometry, bindings={ "fluid": geometry.selection("fluid"), **{ name: (geometry.selection(name), geometry.selection("fluid")) for name in ("inlet", "outlet", "walls", "cylinder") }, "dynamic_viscosity": 1.0e-3, "zero_pressure": 0.0, "inlet_speed": 0.3, "channel_height": geometry.bounds[1][1] - geometry.bounds[1][0], },)linear = eqiora.solve.Linear( relative_tolerance=1e-6, absolute_tolerance=1e-13, maximum_iterations=10_000,)plan = eqiora.resolve( model, mesh=mesh, spatial=eqiora.fem.MiniP1(), solve=linear, scaling=None,)result = eqiora.run(plan)
pressure = result.output(plan.capability.pressure)pressure_values = pressure.values("vertex")print(result.plan_key, pressure.coefficient_count("vertex"))print(result.solve)print(min(pressure_values), max(pressure_values))force = result.boundary_force(geometry.selection("cylinder"))inlet = result.boundary_flux(geometry.selection("inlet"))outlet = result.boundary_flux(geometry.selection("outlet"))print(force.on_domain, inlet.value + outlet.value)Freshly compiled and replayed Models use the same root resolver. The source or
host path is not Model meaning; only the accepted source, concrete Geometry,
and values enter identity. compile is keyword-only and accepts exactly one of
path= or source=; filename= labels diagnostics only for source=.
The Plan exposes the exact spaces, scales, solver tuple, backend, placement,
and existing Realization bytes before a worker starts.
The common Result exposes immutable velocity and pressure FieldOutput
objects selected by exact Model-bound FieldRef values; each output retains
the paired common Mesh.
Field values and Mesh coordinates/connectivity lazily publish
read-only NumPy views in matching mesh order. Exact GeometrySelection values
select the supported boundary force and inlet/outlet flux observations directly
from the Result. eqiora.fluid.steady_stokes_evidence(result) remains an
optional verification projection over the same accepted observations.
This operation admits only the checked exact-cylinder component, Geometry,
mesh, MINI/P1 policy, and SparseLU request. It is not a general Model catalog,
arbitrary Geometry/component closure, or general CFD authoring. Velocity
projection, drag/lift, transient flow, and FSI remain separate slices. The
runnable file is
examples/python/exact_cylinder_stokes.py.
Exact-cylinder pressure rendering
Install the Gmsh and Matplotlib adapters and ask the same runnable file to save the accepted pressure field:
uv venv --python 3.13 .venvuv pip install --python .venv/bin/python '.[gmsh,matplotlib]'uv run --no-project --python .venv/bin/python examples/python/exact_cylinder_stokes.py \ --pressure-png exact-cylinder-pressure.pngThese commands build the checked-out source with its matching example. The equivalent composition API is:
import eqiora.matplotlib as eqplot
pressure = result.snapshots[0]figure = eqplot.plot_scalar_field(result, field=pressure.field)figure.savefig("exact-cylinder-pressure.png")The adapter selects an exact Field from the common Result. It sends the
co-indexed P1 pressure, paired Mesh coordinates, and explicit accepted triangle
connectivity to Matplotlib and uses the Rust-owned pressure extrema in pascals.
Gouraud shading is presentation interpolation of accepted vertex coefficients,
not a new scientific field.
Matplotlib remains optional and is not imported by base eqiora. This bounded
helper currently renders scalar vertex or cell fields as still images.
Mixed-boundary structural result
The installed package also carries the accepted mixed-boundary elasticity
source. Python compiles it through the current Model path, resolves an
explicit linear-elasticity intent before execution, and submits the resulting
model-bound Plan through the ordinary Run path:
from importlib.resources import files
graph = eqiora.geometry.GeometryGraph()rectangle = graph.rectangle(x_bounds=(0.0, 1.0), y_bounds=(0.0, 1.0))geometry = graph.build( rectangle, named_topology={ "body": rectangle.region, "x_lower": rectangle.boundaries[0], "x_upper": rectangle.boundaries[1], "y_lower": rectangle.boundaries[2], "y_upper": rectangle.boundaries[3], },)mesh_plan = eqiora.meshing.resolve( geometry, eqiora.meshing.CartesianMesher(cells=(16, 16)),)mesh = eqiora.meshing.generate(mesh_plan)model = eqiora.compile( path=files(eqiora).joinpath("examples", "mixed-boundary-elasticity.eqi"), entry="MixedBoundaryElasticity2d", geometry=geometry, bindings={ "body": geometry.selection("body"), **{ name: (geometry.selection(name), geometry.selection("body")) for name in ("x_lower", "x_upper", "y_lower", "y_upper") }, "mu": 3.0, "lambda": 0.0, "length_scale": 1.0, },)plan = eqiora.resolve( model, mesh=mesh, spatial=eqiora.fem.Q1(), solve=eqiora.solve.Linear( relative_tolerance=1.0e-10, absolute_tolerance=1.0e-12, maximum_iterations=10_000, ),)result = eqiora.run(plan)
displacement = result.output(plan.capability.displacement)mesh = displacement.meshevidence = eqiora.solid.linear_elasticity_evidence(result)The root Plan exposes the exact caller-owned mesh, Q1 spatial policy, linear
solver policy, backend, and execution placement before a worker starts.
Resolution admits only supported typed policy combinations and rejects other
values instead of silently falling back.
The common Result owns one immutable vector FieldOutput selected by the
Plan’s exact Model-bound FieldRef; displacement.mesh is its paired exact
caller-generated Mesh. Output values, Mesh coordinates,
and Q1 connectivity lazily publish memoized, read-only NumPy views in one
co-indexed canonical order. The typed elasticity observation keeps the
Plan identity, reference-CG solve summary, assembly counts, constrained reaction,
integrated body force, and exact bounds outside the common result transport.
Model, Geometry, correspondence, Mesh, Plan, and Result identity remain
Rust-owned and relationally exact. Stress, strain, traction recovery,
analytic error, other meshes, and general structural solving are not implied.
The optional still displays original and explicitly scaled deformed edges:
import eqiora.matplotlib as eqplot
figure = eqplot.plot_deformed_field( result, field=plan.capability.displacement, scale=1.0,)figure.savefig("mixed-boundary-displacement.png")The complete runnable workflow is
examples/python/mixed_boundary_elasticity.py.
Fixed-mesh monolithic FSI result
The fixed-reference FSI path uses the same root lifecycle as every common
numerical Plan. Python authors the adjacent two-region Geometry, generates
its authenticated common Mesh, compiles the equations-only Component, and
then supplies exact Model-bound spatial scopes:
model = eqiora.compile( path=files(eqiora).joinpath("examples", "fixed-reference-fsi.eqi"), geometry=geometry, entry="FixedReferenceFsi2d", bindings={ **parameters, **{region: geometry.selection(region) for region in ("fluid", "solid")}, **{ f"{region}_{side}": ( geometry.selection(f"{region}_{side}"), geometry.selection(region) ) for region in ("fluid", "solid") for side in ("x_lower", "x_upper", "y_lower", "y_upper") }, },)plan = eqiora.resolve( model, mesh=mesh, spatial=( eqiora.fem.MiniP1().at(model.domain("fluid")), eqiora.fem.P1().at(model.domain("solid")), ), temporal=eqiora.time.BackwardEuler(step_s=0.05), solve=eqiora.solve.Linear( relative_tolerance=1.0e-11, absolute_tolerance=1.0e-13, maximum_iterations=20_000, ), scaling=None,)state = eqiora.State.initial( plan, time_s=0.0, fields=( eqiora.InitialField(model.field("fluid_velocity"), vertex_values=..., cell_values=...), eqiora.InitialField(model.field("fluid_pressure"), vertex_values=...), eqiora.InitialField(model.field("solid_velocity"), vertex_values=...), eqiora.InitialField(model.field("solid_displacement"), vertex_values=...), ),)result = eqiora.run(plan, state=state, steps=2, output_steps=(1, 2))evidence = eqiora.fsi.evidence(result)DomainRef, InitialField, Plan, State, Run, Result, and Trajectory
are common types. The Model decides that this is FSI; eqiora.resolve admits
only the complete MiniP1@fluid + P1@solid partition and binds the actual
Model, Geometry, Mesh, correspondence, production lineage, four exact Fields,
backward Euler policy, full coupled scaling receipt, MINRES provider, and host
placement. scaling=None requests automatic coupled scales; a complete
IncompressibleScaling value makes them manual.
Initial coefficients are immutable, exact-Field assignments in coherent SI. They must be complete and association-correct; pressure has no auxiliary zero-mean restriction in this fixed-reference formulation. A compatible State can restart a freshly resolved Plan even when solve or scaling policies differ, while a foreign Model, Geometry, field, or state space is rejected.
The complete runnable workflow is
examples/python/fixed_reference_fsi.py.
It is one fixed-reference 2D affine-triangle monolithic formulation. It does not
claim partitioned coupling, FVM/FEM transfer, ALE, remeshing, checkpointing,
general multiphysics policy maps, or per-domain time and solve policies.
Conserving connections
Scalar conserving connections use nominal physical-domain identity:
voltage = eqiora.Dimension(mass=1, length=2, time=-3, current=-1)current = eqiora.Dimension(current=1)electrical = eqiora.PhysicalDomain( "electrical", across_type=eqiora.ValueType.real(voltage), through_type=eqiora.ValueType.real(current),)left = eqiora.ConservingPort("left", domain=electrical)right = eqiora.ConservingPort("right", domain=electrical)component = eqiora.Relation( "component", equations=((eqiora.across(left), 0), (eqiora.through(right), 0)),)physical_model = eqiora.Model.define( "physical_pair", electrical, left, right, component, eqiora.connect(left, right),)Equal names and dimensions do not make separately constructed domains interchangeable.
Spatial declarations
Domain, boundary, Field support, and Relation support are exact frozen handles. Python does not infer support from names or reproduce the Semantic Kernel’s dimensional and spatial checks.
interval = eqiora.Domain.box("interval", (0.0, 1.0))lower = interval.boundary( "lower", axis=0, side=eqiora.BoundarySide.Lower,)upper = interval.boundary( "upper", axis=0, side=eqiora.BoundarySide.Upper,)potential = eqiora.Field( "potential", role=eqiora.FieldRole.Variable, domain=interval,)source = eqiora.Parameter( "source", value=1.0, value_type=eqiora.ValueType.real(eqiora.Dimension(length=-2)),)model = eqiora.Model.define( "poisson", interval, lower, upper, potential, source, eqiora.Relation( "balance", domain=interval, equations=((-eqiora.div(eqiora.grad(potential)) - source, 0),), ), eqiora.Relation( "lower_value", domain=lower, equations=((eqiora.trace(potential), 0),), ), eqiora.Relation( "upper_value", domain=upper, equations=((eqiora.trace(potential), 0),), ),)grad, div, and trace are a closed adapter vocabulary over the shared
draft. Shape, frame, dimension, support, and residual validity remain Kernel
decisions.
Typed spatial Plan
Spatial execution uses the same root lifecycle as the examples above: author
one concrete Geometry, resolve a typed meshing provider, compile an
equations-only component with that Geometry, and call
eqiora.resolve(model, mesh=..., spatial=..., solve=...). The returned common
Plan owns the exact Model, Mesh, and numerical policy identities; execution
accepts only eqiora.run(plan) or eqiora.submit(plan). Specialized scalar
requests and model-plus-realization execution are absent.
The same resolved Plan can be moved as one exact local artifact without its producer process:
plan.write("case.eqplan")portable = eqiora.Plan.read("case.eqplan")result = eqiora.run(portable).eqplan contains exactly plan.to_bytes(). Reading re-resolves the Plan
against the locally admitted provider identities and rejects unknown,
noncanonical, oversized, non-regular, symlinked, or wrongly suffixed inputs.
Use it to move one exact Plan between local processes.
Complete Results and spatial Trajectories use the same exact, type-owned file boundary. Reopening always requires the owning Plan:
result.write("run.eqresult")reopened = eqiora.Result.read(portable, "run.eqresult")
trajectory = reopened.trajectorytrajectory.write("run.eqtrajectory")same_trajectory = eqiora.trajectory.Trajectory.read( portable, "run.eqtrajectory")The files contain exactly result.to_bytes() and trajectory.to_bytes().
For a dynamic Result, the Result remains the single complete root and owns its
Trajectory; the separate Trajectory file is an optional spatial projection,
not a second occurrence record. Process-local Runs, restart checkpoints,
archives, and cloud transport remain outside this boundary.
Exact revisions and compiled Model files
Model owns one immutable canonical artifact. Previewing an edit never
mutates it, and committing a valid edit returns a child:
base = modeledit = base.preview_value_edit("source", 2.0)child = base.commit(edit)
assert base.revision != child.revisionassert base.digest == edit.base_digestCommit checks the edit’s exact base digest and graph revision atomically. Stale or foreign plans produce no partial child. Ordinary authoring, edits, and byte/file decoding all use the single current artifact contract:
restored = eqiora.Model.from_bytes(child.to_bytes())assert restored == child
child.write("child.eqmodel")same = eqiora.Model.read("child.eqmodel")assert same.revision == child.revisionThe canonical bytes still expose the persisted
eqiora.model-envelope/v16 schema, but callers do not select that suffix.
.eqi remains source text; .eqmodel is the canonical compiled Model artifact.
Only the current schema is accepted; decoding never sniffs, retries, or silently
migrates an older artifact.
Independent definitions allocate fresh canonical occurrence identities, so exact equality and digest equality are intentionally stronger than structural comparison:
source_model = eqiora.compile( source=""" model decay(parameter rate: 1 / s = 1) { state x: 1; initial { x = 1; } relation flow { derivative(x) + rate * x = 0; } } """)x = eqiora.Field("x", role=eqiora.FieldRole.State)rate = eqiora.Parameter( "rate", value=1.0, value_type=eqiora.ValueType.real(eqiora.Dimension(time=-1)),)native_model = eqiora.Model.define( "decay", x, rate, eqiora.Initial((x, 1)), eqiora.Relation( "flow", equations=((eqiora.derivative(x) + rate * x, 0),), ),)
assert source_model != native_modelassert source_model.digest != native_model.digestassert source_model.structurally_equivalent(native_model)assert ( source_model.structural_fingerprint == native_model.structural_fingerprint)The structural fingerprint omits names, formatting, source spans, occurrence IDs, package provenance, and artifact codec. It is comparison evidence, not a replacement for exact identity in execution, replay, provenance, or edits.
Quantity inputs use the compiler-owned eqiora.units catalog. For example,
q.quantity(Decimal("998.2"), u.kg / u.m**3) preserves the exact decimal input
until compiler normalization. Import Decimal from Python’s decimal module.
Integer inputs retain their decimal digits. Float inputs use Python’s shortest
round-trip decimal spelling; this is a source-authoring policy, not a claim of
exact binary-ratio rescaling. Native numerical inputs remain binary64 values in
coherent SI. Quantity literal spellings are limited to 256 bytes.