TOMK / src /python /tom_world03 /expression.py
c0mbatduckzz's picture
Publish WQK 0.6 formal specification, use cases and verified release (part 2)
767a051 verified
Raw History Blame Contribute Delete
5.13 kB
"""Typed continuous expression evaluation over affine TOM trajectories."""
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Mapping, Protocol
from .interval import ClosedInterval
from .rational import Q
class TrajectoryLike(Protocol):
def state_at(self, time: Q) -> Mapping[str, Q]: ...
def field_interval(self, name: str, time: ClosedInterval) -> ClosedInterval: ...
def field_rate(self, name: str) -> Q: ...
def field_affine(self, name: str) -> tuple[Q, Q]: ...
@dataclass(frozen=True, slots=True)
class DualInterval:
value: ClosedInterval
derivative: ClosedInterval
def _op(expr: Mapping[str, Any]) -> str:
value = expr.get("op")
if not isinstance(value, str):
raise ValueError("expression requires string op")
return value
def validate_expression(expr: Any, *, depth: int = 0, max_depth: int = 64) -> None:
if depth > max_depth:
raise ValueError("expression depth exceeds profile limit")
if not isinstance(expr, Mapping):
raise TypeError("relation expression nodes must be objects")
op = _op(expr)
if op == "const":
Q.from_value(expr.get("value"))
return
if op == "field":
if not isinstance(expr.get("name"), str) or not expr["name"]:
raise ValueError("field expression requires nonempty name")
return
if op == "time":
return
if op == "neg":
validate_expression(expr.get("value"), depth=depth + 1, max_depth=max_depth)
return
if op in {"add", "sub", "mul"}:
args = expr.get("args")
if not isinstance(args, list) or len(args) != 2:
raise ValueError(f"{op} expression requires exactly two args")
validate_expression(args[0], depth=depth + 1, max_depth=max_depth)
validate_expression(args[1], depth=depth + 1, max_depth=max_depth)
return
raise ValueError(f"unsupported continuous expression op {op}")
def evaluate_point(expr: Mapping[str, Any], trajectory: TrajectoryLike, time: Q) -> Q:
op = _op(expr)
if op == "const":
return Q.from_value(expr["value"])
if op == "time":
return time
if op == "field":
state = trajectory.state_at(time)
name = str(expr["name"])
if name not in state:
raise ValueError(f"trajectory has no field {name}")
return Q.from_value(state[name])
if op == "neg":
return -evaluate_point(expr["value"], trajectory, time)
a = evaluate_point(expr["args"][0], trajectory, time)
b = evaluate_point(expr["args"][1], trajectory, time)
if op == "add":
return a + b
if op == "sub":
return a - b
if op == "mul":
return a * b
raise ValueError(f"unsupported op {op}")
def evaluate_dual_interval(
expr: Mapping[str, Any],
trajectory: TrajectoryLike,
time: ClosedInterval,
) -> DualInterval:
op = _op(expr)
if op == "const":
return DualInterval(ClosedInterval.point(expr["value"]), ClosedInterval.point(0))
if op == "time":
return DualInterval(time, ClosedInterval.point(1))
if op == "field":
name = str(expr["name"])
return DualInterval(
trajectory.field_interval(name, time),
ClosedInterval.point(trajectory.field_rate(name)),
)
if op == "neg":
a = evaluate_dual_interval(expr["value"], trajectory, time)
return DualInterval(-a.value, -a.derivative)
a = evaluate_dual_interval(expr["args"][0], trajectory, time)
b = evaluate_dual_interval(expr["args"][1], trajectory, time)
if op == "add":
return DualInterval(a.value + b.value, a.derivative + b.derivative)
if op == "sub":
return DualInterval(a.value - b.value, a.derivative - b.derivative)
if op == "mul":
return DualInterval(
a.value * b.value,
a.derivative * b.value + a.value * b.derivative,
)
raise ValueError(f"unsupported op {op}")
def affine_coefficients(
expr: Mapping[str, Any], trajectory: TrajectoryLike
) -> tuple[Q, Q] | None:
"""Return (slope, intercept) when expr is affine in global time."""
op = _op(expr)
if op == "const":
return Q(0), Q.from_value(expr["value"])
if op == "time":
return Q(1), Q(0)
if op == "field":
return trajectory.field_affine(str(expr["name"]))
if op == "neg":
inner = affine_coefficients(expr["value"], trajectory)
return None if inner is None else (-inner[0], -inner[1])
left = affine_coefficients(expr["args"][0], trajectory)
right = affine_coefficients(expr["args"][1], trajectory)
if left is None or right is None:
return None
if op == "add":
return left[0] + right[0], left[1] + right[1]
if op == "sub":
return left[0] - right[0], left[1] - right[1]
if op == "mul":
if left[0] == Q(0):
return right[0] * left[1], right[1] * left[1]
if right[0] == Q(0):
return left[0] * right[1], left[1] * right[1]
return None
raise ValueError(f"unsupported op {op}")