Skip to content

Commit fd42dfb

Browse files
committed
Merge orchestrator/lang (resolve condition aux_value + struct cleanup conflicts)
2 parents cee66aa + 8809f9d commit fd42dfb

8 files changed

Lines changed: 668 additions & 23 deletions

File tree

include/arbiter/arbiter_model.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ enum ARBITER_op {
5656
ARBITER_OP_CHANGED,
5757
ARBITER_OP_DELTA_GT,
5858
ARBITER_OP_DELTA_LT,
59+
ARBITER_OP_HYSTERESIS = 13,
5960
};
6061

6162
/** Action types. */
@@ -76,6 +77,14 @@ enum ARBITER_cond_group {
7677
ARBITER_COND_NOT,
7778
};
7879

80+
/**
81+
* Maximum number of conditions that support per-condition state (hysteresis).
82+
* Kept small to avoid dynamic allocation; sized for typical safety models.
83+
*/
84+
#ifndef CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS
85+
#define CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS 32
86+
#endif
87+
7988
/** Expression operators for compute engine. */
8089
enum ARBITER_expr_op {
8190
ARBITER_EXPR_ADD = 0, /**< target = left + right */
@@ -93,6 +102,7 @@ enum ARBITER_expr_op {
93102
ARBITER_EXPR_SCALE, /**< target = (left * right) / scale (fixed-point) */
94103
ARBITER_EXPR_ASSIGN, /**< target = left (copy fact or literal) */
95104
ARBITER_EXPR_ACCUMULATE, /**< target = target + (left * right) / scale */
105+
ARBITER_EXPR_LOOKUP = 15, /**< target = table_lookup(table[scale], left) */
96106
};
97107

98108
/** Fact definition (compiled model table entry). */
@@ -125,6 +135,7 @@ struct ARBITER_condition_def {
125135
arbiter_index_t fact_id;
126136
enum ARBITER_op op;
127137
int32_t value;
138+
int32_t aux_value; /**< Secondary threshold (e.g. falling edge for hysteresis). */
128139
enum ARBITER_cond_group group;
129140
};
130141

@@ -162,6 +173,13 @@ struct ARBITER_rule_def {
162173
#endif
163174
};
164175

176+
/** Lookup table definition for interpolation. */
177+
struct ARBITER_table_def {
178+
uint16_t count; /**< Number of entries in the table. */
179+
const int32_t *keys; /**< Sorted input key values. */
180+
const int32_t *values; /**< Output values (same count as keys). */
181+
};
182+
165183
/** Complete compiled model. */
166184
struct ARBITER_model {
167185
const char *name;
@@ -180,6 +198,8 @@ struct ARBITER_model {
180198
const struct ARBITER_action_def *actions;
181199
const struct ARBITER_expr_def *expressions;
182200
const char **mode_names;
201+
const struct ARBITER_table_def *tables; /**< Lookup tables. */
202+
uint16_t table_count;
183203
#if defined(CONFIG_ARBITER_FPGA_OFFLOAD) && CONFIG_ARBITER_FPGA_OFFLOAD
184204
const struct ARBITER_hw_offload_ops *offload_ops;
185205
#endif

lib/arbiter_eval.c

Lines changed: 114 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -83,10 +83,14 @@ struct arbiter_cond_cache_entry {
8383
* No pointer-to-pointer indirection -- values[] and timestamp are
8484
* passed directly so the compiler can keep them in registers.
8585
*/
86+
/* Per-condition hysteresis state bitmask (static — survives across evals). */
87+
static uint32_t hyst_state[CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS / 32 + 1];
88+
8689
ARBITER_ALWAYS_INLINE bool eval_condition(
8790
const struct ARBITER_condition_def *__restrict cond,
8891
const struct ARBITER_fact_value *__restrict values,
89-
arbiter_index_t vcount, uint32_t snap_ts)
92+
arbiter_index_t vcount, uint32_t snap_ts,
93+
arbiter_index_t cond_index)
9094
{
9195
if (unlikely(cond->fact_id >= vcount)) {
9296
return false;
@@ -147,6 +151,43 @@ ARBITER_ALWAYS_INLINE bool eval_condition(
147151
case ARBITER_OP_NOT_IN:
148152
return val != cond->value;
149153

154+
/* Hysteresis: rising = value, falling = aux_value.
155+
* State persists in a static bitmask across evaluations.
156+
*/
157+
case ARBITER_OP_HYSTERESIS: {
158+
const int32_t rising = cond->value;
159+
const int32_t falling = cond->aux_value;
160+
bool prev_state = false;
161+
162+
if (likely(cond_index <
163+
CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS)) {
164+
prev_state = (hyst_state[cond_index / 32] >>
165+
(cond_index & 31)) & 1u;
166+
}
167+
168+
bool result;
169+
170+
if (val >= rising) {
171+
result = true;
172+
} else if (val <= falling) {
173+
result = false;
174+
} else {
175+
result = prev_state;
176+
}
177+
178+
if (likely(cond_index <
179+
CONFIG_ARBITER_MAX_HYSTERESIS_CONDITIONS)) {
180+
if (result) {
181+
hyst_state[cond_index / 32] |=
182+
(1u << (cond_index & 31));
183+
} else {
184+
hyst_state[cond_index / 32] &=
185+
~(1u << (cond_index & 31));
186+
}
187+
}
188+
return result;
189+
}
190+
150191
default:
151192
return false;
152193
}
@@ -171,15 +212,16 @@ ARBITER_ALWAYS_INLINE bool eval_condition_group(
171212
/* Fast path: single condition -- skip loop entirely */
172213
if (likely(count == 1)) {
173214
bool r = eval_condition(&conds[start], values,
174-
vcount, snap_ts);
215+
vcount, snap_ts, start);
175216
return (group == ARBITER_COND_NOT) ? !r : r;
176217
}
177218

178219
/* ALL is the overwhelmingly common group type */
179220
if (likely(group == ARBITER_COND_ALL)) {
180221
for (arbiter_index_t i = 0; i < count; i++) {
181222
if (!eval_condition(&conds[start + i], values,
182-
vcount, snap_ts)) {
223+
vcount, snap_ts,
224+
start + i)) {
183225
return false;
184226
}
185227
}
@@ -189,15 +231,16 @@ ARBITER_ALWAYS_INLINE bool eval_condition_group(
189231
if (group == ARBITER_COND_ANY) {
190232
for (arbiter_index_t i = 0; i < count; i++) {
191233
if (eval_condition(&conds[start + i], values,
192-
vcount, snap_ts)) {
234+
vcount, snap_ts,
235+
start + i)) {
193236
return true;
194237
}
195238
}
196239
return false;
197240
}
198241

199242
/* ARBITER_COND_NOT: invert single child */
200-
return !eval_condition(&conds[start], values, vcount, snap_ts);
243+
return !eval_condition(&conds[start], values, vcount, snap_ts, start);
201244
}
202245

203246
/* ── Expression evaluator ─────────────────────────────────────── */
@@ -209,10 +252,56 @@ ARBITER_ALWAYS_INLINE bool eval_condition_group(
209252
* Switch cases ordered by frequency: ASSIGN and simple arithmetic
210253
* first (PID, Kalman models hit these 80%+ of the time).
211254
*/
255+
/**
256+
* Linear interpolation in a lookup table.
257+
* Clamps to table endpoints when input is outside range.
258+
*/
259+
ARBITER_ALWAYS_INLINE int32_t table_lookup(
260+
const struct ARBITER_table_def *__restrict tbl,
261+
int32_t input)
262+
{
263+
if (unlikely(tbl == NULL || tbl->count == 0)) {
264+
return 0;
265+
}
266+
const uint16_t n = tbl->count;
267+
const int32_t *__restrict keys = tbl->keys;
268+
const int32_t *__restrict vals = tbl->values;
269+
270+
/* Clamp below minimum */
271+
if (input <= keys[0]) {
272+
return vals[0];
273+
}
274+
/* Clamp above maximum */
275+
if (input >= keys[n - 1]) {
276+
return vals[n - 1];
277+
}
278+
/* Binary-ish scan for bracket (tables are small, linear is fine) */
279+
for (uint16_t i = 1; i < n; i++) {
280+
if (input <= keys[i]) {
281+
/* Linear interpolation between [i-1] and [i] */
282+
int32_t k0 = keys[i - 1];
283+
int32_t k1 = keys[i];
284+
int32_t v0 = vals[i - 1];
285+
int32_t v1 = vals[i];
286+
int32_t dk = k1 - k0;
287+
288+
if (dk == 0) {
289+
return v0;
290+
}
291+
/* lerp: v0 + (v1-v0)*(input-k0)/(k1-k0) */
292+
int64_t num = (int64_t)(v1 - v0) *
293+
(int64_t)(input - k0);
294+
return v0 + (int32_t)(num / dk);
295+
}
296+
}
297+
return vals[n - 1];
298+
}
299+
212300
ARBITER_ALWAYS_INLINE void eval_expression(
213301
const struct ARBITER_expr_def *__restrict expr,
214302
struct ARBITER_fact_value *__restrict values,
215-
arbiter_index_t vcount)
303+
arbiter_index_t vcount,
304+
const struct ARBITER_model *__restrict model)
216305
{
217306
const arbiter_index_t tid = expr->target_fact_id;
218307

@@ -295,6 +384,19 @@ ARBITER_ALWAYS_INLINE void eval_expression(
295384
case ARBITER_EXPR_SHIFT_L:
296385
result = left << (right & 31);
297386
break;
387+
case ARBITER_EXPR_LOOKUP: {
388+
/* scale field stores the table index */
389+
const uint16_t tbl_idx = (uint16_t)expr->scale;
390+
391+
if (likely(model->tables != NULL &&
392+
tbl_idx < model->table_count)) {
393+
result = table_lookup(
394+
&model->tables[tbl_idx], left);
395+
} else {
396+
result = 0;
397+
}
398+
break;
399+
}
298400
default:
299401
return;
300402
}
@@ -489,11 +591,12 @@ int ARBITER_eval(const struct ARBITER_model *model,
489591
for (arbiter_index_t i = 0; i < ec; i++) {
490592
const arbiter_index_t ei = es + i;
491593

492-
if (likely(ei < expr_count)) {
493-
eval_expression(
494-
&exprs[ei],
495-
values, vcount);
496-
}
594+
if (likely(ei < expr_count)) {
595+
eval_expression(
596+
&exprs[ei],
597+
values, vcount,
598+
model);
599+
}
497600
}
498601
ops += ec;
499602
}

python/arbiter/canonical.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ class CanonicalModel:
2121
actions: list[dict[str, Any]]
2222
modes: list[dict[str, Any]]
2323
expressions: list[dict[str, Any]] = field(default_factory=list)
24+
tables: list[dict[str, Any]] = field(default_factory=list)
25+
table_id_map: dict[str, int] = field(default_factory=dict)
2426
states: list[dict[str, Any]] = field(default_factory=list)
2527
transitions: list[dict[str, Any]] = field(default_factory=list)
2628
hazards: list[dict[str, Any]] = field(default_factory=list)
@@ -105,6 +107,22 @@ def canonicalize(data: dict[str, Any]) -> CanonicalModel:
105107
annotated["_expr_count"] = len(rule_exprs)
106108
rules.append(annotated)
107109

110+
# Flatten tables
111+
tables_raw = data.get("tables", [])
112+
tables: list[dict[str, Any]] = []
113+
table_id_map: dict[str, int] = {}
114+
if isinstance(tables_raw, list):
115+
tables_sorted = sorted(tables_raw, key=lambda t: t.get("id", "") if isinstance(t, dict) else "")
116+
for idx, tbl in enumerate(tables_sorted):
117+
if isinstance(tbl, dict) and "id" in tbl:
118+
table_id_map[tbl["id"]] = idx
119+
tables.append({
120+
"id": tbl["id"],
121+
"index": idx,
122+
"keys": [int(k) for k in tbl.get("keys", [])],
123+
"values": [int(v) for v in tbl.get("values", [])],
124+
})
125+
108126
# Flatten states and transitions (REQ-ARCH-039)
109127
states_flat, transitions_flat, state_id_map = _flatten_states(
110128
data.get("states", []), action_id_map, fact_id_map, conditions,
@@ -119,6 +137,8 @@ def canonicalize(data: dict[str, Any]) -> CanonicalModel:
119137
actions=actions,
120138
modes=modes,
121139
expressions=expressions,
140+
tables=tables,
141+
table_id_map=table_id_map,
122142
states=states_flat,
123143
transitions=transitions_flat,
124144
hazards=data.get("hazards", []),
@@ -147,6 +167,7 @@ def canonicalize(data: dict[str, Any]) -> CanonicalModel:
147167
"min": "min", "max": "max", "clamp": "clamp",
148168
"shift_r": "shift_r", "shift_l": "shift_l",
149169
"scale": "scale", "accumulate": "accumulate",
170+
"lookup": "lookup",
150171
}
151172

152173

@@ -194,15 +215,19 @@ def _flatten_expressions(
194215
op = _EXPR_OP_ALIASES.get(expr.get("op", "assign"), "assign")
195216
scale = int(expr.get("scale", 1))
196217

197-
out.append({
218+
entry: dict[str, Any] = {
198219
"target_fact_id": target_id,
199220
"op": op,
200221
"left_fact_id": left_fact_id,
201222
"left_literal": left_literal,
202223
"right_fact_id": right_fact_id,
203224
"right_literal": right_literal,
204225
"scale": scale,
205-
})
226+
}
227+
# Lookup: store table name for late binding (resolved by emitter)
228+
if op == "lookup" and "table" in expr:
229+
entry["table"] = expr["table"]
230+
out.append(entry)
206231
return out
207232

208233

@@ -221,13 +246,19 @@ def _flatten_conditions(
221246
for cond in group:
222247
if not isinstance(cond, dict):
223248
continue
224-
flat = {
249+
flat: dict[str, Any] = {
225250
"group": group_type,
226251
"fact": cond.get("fact", ""),
227252
"fact_id": fact_id_map.get(cond.get("fact", ""), 0),
228253
"op": cond.get("op", "=="),
229254
"value": cond.get("value", 0),
230255
}
256+
# Hysteresis: map rising → value, falling → aux_value
257+
if cond.get("op") == "hysteresis":
258+
flat["value"] = int(cond.get("rising", 0))
259+
flat["aux_value"] = int(cond.get("falling", 0))
260+
flat["rising"] = flat["value"]
261+
flat["falling"] = flat["aux_value"]
231262
conditions.append(flat)
232263

233264

0 commit comments

Comments
 (0)