mixle.engines.arithmetic module

Backend-dispatched arithmetic helpers used by mixle classes.

Array operations dispatch on their arguments’ engine (engine_of); scalar constants have no arguments to dispatch on, so they resolve from the active engine instead – NUMPY_ENGINE by default (plain floats, the historical behaviour). Select a different one to make the constants come from it, e.g. the symbolic engine, so that pi stays a symbolic pi and half an exact 1/2 instead of collapsing to floats during exact arithmetic:

import mixle.engines.arithmetic as arith
with arith.using_engine("symbolic"):
    arith.pi      # SymbolicExpression "pi", not 3.141592...

This is the extension point for letting users control the arithmetic backend without switching mixle to a full computer-algebra system. maxint / maxrandint / eps are implementation limits and tolerances (not mathematical constants), so they stay engine-independent.

to_numpy(x)[source]

Convert an engine array/tensor payload to NumPy at an explicit boundary.

Deliberately NOT built via _dispatch() like the other allocation wrappers above: _dispatch resolves ONE engine for the whole payload (correctly, recursing through engine_of()) but then hands that single engine the ORIGINAL, unconverted container – which fails to stack a ragged list of arrays, leaves dict values unconverted (NumpyEngine.to_numpy -> np.asarray on a dict wraps it in a useless 0-d object array instead of touching its values), and can hand a device tensor to a host conversion without transferring it first. This is exactly the bug mixle.engines.to_numpy() was fixed for (MXR-080-0123): a dict/list/tuple that CONTAINS an engine-owned value is converted leaf by leaf here too, preserving container structure and resolving each leaf’s own owning engine independently.

This is intentionally NOT a direct alias of the now-fixed mixle.engines.to_numpy() – there is no import-cycle reason to avoid that (mixle.engines does not import this module at all, only the reverse). That free function’s “no engine-owned value anywhere” fallback is hardcoded to NUMPY_ENGINE, while this whole module’s documented purpose (see the module docstring) is resolving exactly that fallback against the ACTIVE engine (_DEFAULT_ENGINE, e.g. a caller inside using_engine("symbolic")) – aliasing the package-level function directly would silently drop that contract for plain, non-engine-owned data. The container- walking shape below is intentionally the same recursion as mixle.engines.to_numpy() (reusing its private _contains_engine_value leaf-ownership test rather than re-deriving it, so this stays in sync with any future fix to engine-ownership detection there); only the final leaf’s fallback engine differs.

Parameters:

x (Any)

Return type:

Any

constant(value)[source]

Wrap value in the active engine’s scalar representation (identity for numeric engines).

Parameters:

value (Any)

Return type:

Any

get_default_engine()[source]

Return the engine that supplies scalar constants and the operation-dispatch default.

Return type:

ComputeEngine

set_default_engine(engine)[source]

Set the active engine (a ComputeEngine or "numpy"/"symbolic"); return the previous one.

This is a direct, unscoped assignment: like using_engine, it lives in a ContextVar so it is isolated per thread/task, but there is no token to auto-restore – the caller (or a test’s isolation fixture) is responsible for putting the previous engine back. Prefer using_engine for a scoped override that always restores itself, including when the block raises.

Parameters:

engine (ComputeEngine | str)

Return type:

ComputeEngine

using_engine(engine)[source]

Context manager that makes engine the active engine for its block, then restores the previous one.

Parameters:

engine (ComputeEngine | str)