SRHarness Engine¶
sr_harness_engine is the symbolic expression layer of SRHarness. It provides restricted parsing, canonical rendering, tree traversal, NumPy evaluation, parameter fitting, constant folding, and syntax for graphs, hypergraphs, and delays.
The Engine describes mathematics only. Train/validation splits, candidate ranking, and task-specific rollout metrics belong to Evaluators and the SRHarness runtime.
Quick start¶
import numpy as np
import sr_harness_engine as engine
x = np.linspace(-2.0, 2.0, 101)
target = 2.5 * np.sin(x) - 0.4
model = engine.parse("param('a') * sin(x) + param('b')")
fit = model.fit({"x": x}, target)
print(fit.expression) # expression with bound parameters
print(fit.parameters)
print(fit.loss)
print(fit.evaluate({"x": x})[:3])
parse() uses a restricted Python AST. It does not call eval or execute arbitrary Python from a formula.
Basic syntax¶
Values, variables, and operators¶
The language supports +, -, *, /, **, and unary minus. Use **, not Python's bitwise-XOR operator ^, when calling engine.parse() directly.
Built-in elementwise functions are:
sin cos tan sinh cosh tanh
arcsin arccos arctan
exp log log10 sqrt abs
sigmoid sign sec sech csc cot inv
Expressions can also be constructed through Python:
Parameters¶
Named parameters¶
Occurrences with the same name refer to one parameter. value is both a default and an optimization initial value. FitResult.expression contains bound parameter values after fitting.
fit = engine.fit(
engine.parse("param('a') * x + param('b')"),
{"x": x},
target,
initial={"a": 1.0, "b": 0.0},
)
Grouped parameters¶
Samples in one category share a parameter, while categories receive separate values. Initial mappings and a default are supported:
Parsing, rendering, and round trips¶
expression = engine.parse("x ** (4 / 3) + sqrt(2)")
text = engine.render(expression)
latex = engine.render(expression, latex=True)
assert str(engine.parse(str(expression))) == str(expression)
Attribute access, slicing, comprehensions, lambdas, arbitrary calls, and non-literal keyword arguments are rejected. Examples of invalid input are np.sin(x), __import__('os'), x[0:10], and custom_python_function(x).
Evaluation¶
Variables follow NumPy broadcasting. Parameter values can be supplied separately:
The equivalent module function is engine.evaluate(expression, values).
Expression trees¶
All nodes derive from Expression:
| Node | Meaning |
|---|---|
Number |
Fixed numeric literal |
Symbol / Variable |
Data variable |
Parameter |
Named parameter |
GroupedParameter |
Category-dependent parameter |
Unary / Binary |
Arithmetic operation |
Function |
Function call |
Indexed |
Variable with free indices |
Reduction |
Index reduction |
Gather / Aggregate / RelationLift |
Relation operation |
for node in expression.iter_preorder():
print(type(node).__name__, node)
copy = expression.copy()
simplified = expression.fold_constants()
parameter_count = expression.count_parameters()
operands exposes direct children, while replace(old, new) creates a tree with identity-based replacement.
Graph and hypergraph indices¶
Relation arrays use target-first ordering:
- graph
A.shape == (E, 2):(target, source)rows; - ternary hypergraph
T.shape == (H, 3):(target, source1, source2)rows.
Message aggregation¶
A[i, j] binds column 0 to target index i and column 1 to source index j. sum[j] eliminates the source index and aggregates by the remaining i. Supply num_nodes so isolated nodes remain represented:
Node variables normally have shape (..., N), edge fields (..., E), and hyperedge fields (..., H). Leading dimensions broadcast under NumPy rules; the final dimension is structural.
Hypergraph aggregation uses the same language:
Gather and relation fields¶
gather evaluates at nonzero relation coordinates and returns edge- or hyperedge-aligned output:
An edge field can participate in aggregation:
w may have shape (E,) or (..., E). When multiple relations are present, RelationField(w, relation="A") identifies the matching coordinate table.
Convenience syntax¶
Both desugar to sum[j](A[i, j], x[i] * x[j]). Canonical rendering therefore emits index syntax rather than preserving the original sugar.
Delays¶
The default evaluator interpolates along the first axis to obtain x(t - delta) and returns NaN outside available history:
prediction = engine.parse("delay(x, delta)").evaluate(
{"x": trajectory, "delta": lag},
time=sample_times,
)
ODE/DDE Evaluators can provide a custom delay_resolver backed by their own history representation.
Simplification and complexity¶
Constant folding preserves readable fractions, powers, and named functions. count_parameters() counts fitted parameters; len(expression) counts expression nodes. The default Evaluator uses node count as complexity.
Engine and Evaluators¶
DefaultEvaluator exposes five extension points:
split(context)
fit(f, y, context)
evaluate(f, y, context)
fit_candidate(f, context)
evaluate_candidate(f, context)
A general equality uses fit/evaluate; a target-eligible formula uses the candidate-specific entry points. An ODE Evaluator can therefore add integration and rollout metrics only for a formal dx_dt = f(x, t) candidate without treating every implicit equality as integrable dynamics.
Metrics, random/OOD/chronological splitting, and ODE integration infrastructure are available from sr_harness.evaluator.utils. See Formula Evaluation and Custom Evaluators for the Evaluator design and extension workflow, and the API Reference for signatures.
Executable specification¶
Behavior tests provide runnable examples for basic expressions, parameters, relations, gather, delay, simplification, and custom Evaluators:
See the API Reference for every public type and function.