Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions include/arbiter/arbiter_model.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ enum ARBITER_op {
ARBITER_OP_CHANGED,
ARBITER_OP_DELTA_GT,
ARBITER_OP_DELTA_LT,
ARBITER_OP_HYSTERESIS = 13,
};

/** Action types. */
Expand All @@ -76,6 +77,14 @@ enum ARBITER_cond_group {
ARBITER_COND_NOT,
};

/**
* Maximum number of conditions that support per-condition state (hysteresis).
* Kept small to avoid dynamic allocation; sized for typical safety models.
*/
#ifndef CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS
#define CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS 32
#endif

/** Expression operators for compute engine. */
enum ARBITER_expr_op {
ARBITER_EXPR_ADD = 0, /**< target = left + right */
Expand All @@ -93,6 +102,7 @@ enum ARBITER_expr_op {
ARBITER_EXPR_SCALE, /**< target = (left * right) / scale (fixed-point) */
ARBITER_EXPR_ASSIGN, /**< target = left (copy fact or literal) */
ARBITER_EXPR_ACCUMULATE, /**< target = target + (left * right) / scale */
ARBITER_EXPR_LOOKUP = 15, /**< target = table_lookup(table[scale], left) */
};

/** Fact definition (compiled model table entry). */
Expand Down Expand Up @@ -125,6 +135,7 @@ struct ARBITER_condition_def {
arbiter_index_t fact_id;
enum ARBITER_op op;
int32_t value;
int32_t aux_value; /**< Secondary threshold (e.g. falling edge for hysteresis). */
enum ARBITER_cond_group group;
arbiter_index_t group_index;
arbiter_index_t next;
Expand Down Expand Up @@ -163,6 +174,13 @@ struct ARBITER_rule_def {
#endif
};

/** Lookup table definition for interpolation. */
struct ARBITER_table_def {
uint16_t count; /**< Number of entries in the table. */
const int32_t *keys; /**< Sorted input key values. */
const int32_t *values; /**< Output values (same count as keys). */
};

/** Complete compiled model. */
struct ARBITER_model {
const char *name;
Expand All @@ -181,6 +199,8 @@ struct ARBITER_model {
const struct ARBITER_action_def *actions;
const struct ARBITER_expr_def *expressions;
const char **mode_names;
const struct ARBITER_table_def *tables; /**< Lookup tables. */
uint16_t table_count;
#if defined(CONFIG_ARBITER_FPGA_OFFLOAD) && CONFIG_ARBITER_FPGA_OFFLOAD
const struct ARBITER_hw_offload_ops *offload_ops;
#endif
Expand Down
125 changes: 114 additions & 11 deletions lib/arbiter_eval.c
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,14 @@ ARBITER_ALWAYS_INLINE int32_t resolve_operand(
* No pointer-to-pointer indirection -- values[] and timestamp are
* passed directly so the compiler can keep them in registers.
*/
/* Per-condition hysteresis state bitmask (static — survives across evals). */
static uint32_t hyst_state[CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS / 32 + 1];

ARBITER_ALWAYS_INLINE bool eval_condition(
const struct ARBITER_condition_def *__restrict cond,
const struct ARBITER_fact_value *__restrict values,
arbiter_index_t vcount, uint32_t snap_ts)
arbiter_index_t vcount, uint32_t snap_ts,
arbiter_index_t cond_index)
{
if (unlikely(cond->fact_id >= vcount)) {
return false;
Expand Down Expand Up @@ -133,6 +137,43 @@ ARBITER_ALWAYS_INLINE bool eval_condition(
case ARBITER_OP_NOT_IN:
return val != cond->value;

/* Hysteresis: rising = value, falling = aux_value.
* State persists in a static bitmask across evaluations.
*/
case ARBITER_OP_HYSTERESIS: {
const int32_t rising = cond->value;
const int32_t falling = cond->aux_value;
bool prev_state = false;

if (likely(cond_index <
CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS)) {
prev_state = (hyst_state[cond_index / 32] >>
(cond_index & 31)) & 1u;
}

bool result;

if (val >= rising) {
result = true;
} else if (val <= falling) {
result = false;
} else {
result = prev_state;
}

if (likely(cond_index <
CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS)) {
if (result) {
hyst_state[cond_index / 32] |=
(1u << (cond_index & 31));
} else {
hyst_state[cond_index / 32] &=
~(1u << (cond_index & 31));
}
}
return result;
}

default:
return false;
}
Expand All @@ -157,15 +198,16 @@ ARBITER_ALWAYS_INLINE bool eval_condition_group(
/* Fast path: single condition -- skip loop entirely */
if (likely(count == 1)) {
bool r = eval_condition(&conds[start], values,
vcount, snap_ts);
vcount, snap_ts, start);
return (group == ARBITER_COND_NOT) ? !r : r;
}

/* ALL is the overwhelmingly common group type */
if (likely(group == ARBITER_COND_ALL)) {
for (arbiter_index_t i = 0; i < count; i++) {
if (!eval_condition(&conds[start + i], values,
vcount, snap_ts)) {
vcount, snap_ts,
start + i)) {
return false;
}
}
Expand All @@ -175,15 +217,16 @@ ARBITER_ALWAYS_INLINE bool eval_condition_group(
if (group == ARBITER_COND_ANY) {
for (arbiter_index_t i = 0; i < count; i++) {
if (eval_condition(&conds[start + i], values,
vcount, snap_ts)) {
vcount, snap_ts,
start + i)) {
return true;
}
}
return false;
}

/* ARBITER_COND_NOT: invert single child */
return !eval_condition(&conds[start], values, vcount, snap_ts);
return !eval_condition(&conds[start], values, vcount, snap_ts, start);
}

/* ── Expression evaluator ─────────────────────────────────────── */
Expand All @@ -195,10 +238,56 @@ ARBITER_ALWAYS_INLINE bool eval_condition_group(
* Switch cases ordered by frequency: ASSIGN and simple arithmetic
* first (PID, Kalman models hit these 80%+ of the time).
*/
/**
* Linear interpolation in a lookup table.
* Clamps to table endpoints when input is outside range.
*/
ARBITER_ALWAYS_INLINE int32_t table_lookup(
const struct ARBITER_table_def *__restrict tbl,
int32_t input)
{
if (unlikely(tbl == NULL || tbl->count == 0)) {
return 0;
}
const uint16_t n = tbl->count;
const int32_t *__restrict keys = tbl->keys;
const int32_t *__restrict vals = tbl->values;

/* Clamp below minimum */
if (input <= keys[0]) {
return vals[0];
}
/* Clamp above maximum */
if (input >= keys[n - 1]) {
return vals[n - 1];
}
/* Binary-ish scan for bracket (tables are small, linear is fine) */
for (uint16_t i = 1; i < n; i++) {
if (input <= keys[i]) {
/* Linear interpolation between [i-1] and [i] */
int32_t k0 = keys[i - 1];
int32_t k1 = keys[i];
int32_t v0 = vals[i - 1];
int32_t v1 = vals[i];
int32_t dk = k1 - k0;

if (dk == 0) {
return v0;
}
/* lerp: v0 + (v1-v0)*(input-k0)/(k1-k0) */
int64_t num = (int64_t)(v1 - v0) *
(int64_t)(input - k0);
return v0 + (int32_t)(num / dk);
}
}
return vals[n - 1];
}

ARBITER_ALWAYS_INLINE void eval_expression(
const struct ARBITER_expr_def *__restrict expr,
struct ARBITER_fact_value *__restrict values,
arbiter_index_t vcount)
arbiter_index_t vcount,
const struct ARBITER_model *__restrict model)
{
const arbiter_index_t tid = expr->target_fact_id;

Expand Down Expand Up @@ -281,6 +370,19 @@ ARBITER_ALWAYS_INLINE void eval_expression(
case ARBITER_EXPR_SHIFT_L:
result = left << (right & 31);
break;
case ARBITER_EXPR_LOOKUP: {
/* scale field stores the table index */
const uint16_t tbl_idx = (uint16_t)expr->scale;

if (likely(model->tables != NULL &&
tbl_idx < model->table_count)) {
result = table_lookup(
&model->tables[tbl_idx], left);
} else {
result = 0;
}
break;
}
default:
return;
}
Expand Down Expand Up @@ -423,11 +525,12 @@ int ARBITER_eval(const struct ARBITER_model *model,
for (arbiter_index_t i = 0; i < ec; i++) {
const arbiter_index_t ei = es + i;

if (likely(ei < expr_count)) {
eval_expression(
&exprs[ei],
values, vcount);
}
if (likely(ei < expr_count)) {
eval_expression(
&exprs[ei],
values, vcount,
model);
}
}
ops += ec;
}
Expand Down
37 changes: 34 additions & 3 deletions python/arbiter/canonical.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ class CanonicalModel:
actions: list[dict[str, Any]]
modes: list[dict[str, Any]]
expressions: list[dict[str, Any]] = field(default_factory=list)
tables: list[dict[str, Any]] = field(default_factory=list)
table_id_map: dict[str, int] = field(default_factory=dict)
states: list[dict[str, Any]] = field(default_factory=list)
transitions: list[dict[str, Any]] = field(default_factory=list)
hazards: list[dict[str, Any]] = field(default_factory=list)
Expand Down Expand Up @@ -105,6 +107,22 @@ def canonicalize(data: dict[str, Any]) -> CanonicalModel:
annotated["_expr_count"] = len(rule_exprs)
rules.append(annotated)

# Flatten tables
tables_raw = data.get("tables", [])
tables: list[dict[str, Any]] = []
table_id_map: dict[str, int] = {}
if isinstance(tables_raw, list):
tables_sorted = sorted(tables_raw, key=lambda t: t.get("id", "") if isinstance(t, dict) else "")
for idx, tbl in enumerate(tables_sorted):
if isinstance(tbl, dict) and "id" in tbl:
table_id_map[tbl["id"]] = idx
tables.append({
"id": tbl["id"],
"index": idx,
"keys": [int(k) for k in tbl.get("keys", [])],
"values": [int(v) for v in tbl.get("values", [])],
})

# Flatten states and transitions (REQ-ARCH-039)
states_flat, transitions_flat, state_id_map = _flatten_states(
data.get("states", []), action_id_map, fact_id_map, conditions,
Expand All @@ -119,6 +137,8 @@ def canonicalize(data: dict[str, Any]) -> CanonicalModel:
actions=actions,
modes=modes,
expressions=expressions,
tables=tables,
table_id_map=table_id_map,
states=states_flat,
transitions=transitions_flat,
hazards=data.get("hazards", []),
Expand Down Expand Up @@ -147,6 +167,7 @@ def canonicalize(data: dict[str, Any]) -> CanonicalModel:
"min": "min", "max": "max", "clamp": "clamp",
"shift_r": "shift_r", "shift_l": "shift_l",
"scale": "scale", "accumulate": "accumulate",
"lookup": "lookup",
}


Expand Down Expand Up @@ -194,15 +215,19 @@ def _flatten_expressions(
op = _EXPR_OP_ALIASES.get(expr.get("op", "assign"), "assign")
scale = int(expr.get("scale", 1))

out.append({
entry: dict[str, Any] = {
"target_fact_id": target_id,
"op": op,
"left_fact_id": left_fact_id,
"left_literal": left_literal,
"right_fact_id": right_fact_id,
"right_literal": right_literal,
"scale": scale,
})
}
# Lookup: store table name for late binding (resolved by emitter)
if op == "lookup" and "table" in expr:
entry["table"] = expr["table"]
out.append(entry)
return out


Expand All @@ -221,13 +246,19 @@ def _flatten_conditions(
for cond in group:
if not isinstance(cond, dict):
continue
flat = {
flat: dict[str, Any] = {
"group": group_type,
"fact": cond.get("fact", ""),
"fact_id": fact_id_map.get(cond.get("fact", ""), 0),
"op": cond.get("op", "=="),
"value": cond.get("value", 0),
}
# Hysteresis: map rising → value, falling → aux_value
if cond.get("op") == "hysteresis":
flat["value"] = int(cond.get("rising", 0))
flat["aux_value"] = int(cond.get("falling", 0))
flat["rising"] = flat["value"]
flat["falling"] = flat["aux_value"]
conditions.append(flat)


Expand Down
Loading
Loading