Constraints and checkers¶
Two modules share this page because they share the rule texts.
backend/constraints.py holds what the operator says: rules as plain
English, optional checker ids, and work zones. backend/checkers.py holds
closed-form ground truth for the six crisp preset rules, used for scoring and
for the simulated operator, and never for the shield's decision. Two further
presets are fuzzy by design and have no checker; hand labels score them (see
Evaluation internals).
Constraints¶
@dataclass
class Constraint:
id: str # "c1", "c2", ... stable for the session
text: str # what Jev reads, verbatim
enabled: bool = True
checker: str | None = None
The eight presets, in the order the console shows the chips. PRESETS maps
a preset id to its text; for the first six the id is also the checker id, and
preset_checker(id) returns None for the two in FUZZY_PRESETS.
| Preset id | Checker | Preset text |
|---|---|---|
speed_limit |
same | Speed limit 25 m/s |
lane_change_gap |
same | No lane change if a vehicle is within 10 m in the target lane |
left_lane_passing |
same | Keep the left lane for passing only |
lane_closed |
same | Lane 3 is closed ahead for a work zone |
headway |
same | Keep at least 2 seconds of headway to the vehicle ahead |
lane_change_rate |
same | At most 1 lane change in any 10 seconds |
cut_in |
none | Leave plenty of room when moving in ahead of a faster vehicle |
dense_traffic |
none | Drive conservatively in dense traffic |
The two fuzzy presets were added on Oct 17 (plan section 16.2) to give the judge rules that a code checker cannot reproduce. They are phrased as positive statements, like every preset, because the spike showed negation scores softer.
Inferring a checker from typed text¶
infer_checker(text) lowercases and collapses whitespace, then:
- An exact match to a preset returns its checker.
speed limit <digit>returnsspeed_limit, so "Speed limit 20 m/s" is scored.lane changetogether withwithin <number> mreturnslane_change_gap.passing onlyreturnsleft_lane_passing.lane <digit> is closedreturnslane_closed.at least <number> s of headwayreturnsheadway.at most <number> lane changereturnslane_change_rate.- Anything else returns
None: a free-text rule that Jev judges and that only hand labels can score. A fuzzy preset's exact text also returnsNone.
The checkers parse the number out of the text (_number with a regex and a
default), so a typed variant with a different limit or gap is scored against
its own number.
The store¶
ConstraintStore is one session's mutable set of rules and zones. Ids are
assigned from a counter and never reused within the session.
| Method | Behaviour |
|---|---|
add(text, checker=None) |
Collapses whitespace, rejects empty text with ValueError, infers the checker when none is given, appends and returns the constraint. |
add_presets(preset_ids=None) |
Adds the named presets with their checkers, all eight when None. The evaluator's standing rules are every preset but lane_closed. |
enabled() |
The enabled rules, the only ones the judge is asked about. |
get, toggle(id, enabled), remove(id) |
By id; KeyError for an unknown id, which the server turns into an error message. |
drop_zone(ego_x_m, lane=3, ahead_m=120, length_m=200, name="work zone") |
Places a zone in absolute road coordinates starting ahead_m in front of the ego, then adds the lane-closed rule with text Lane <lane> is closed ahead for a <name> or re-enables the existing one. |
zone_rule() |
The constraint whose checker is lane_closed, if any. |
retire_passed_zones(ego_x_m) |
Removes zones whose end is behind the ego; when none remain, disables the lane-closed rule. Returns the retired zones. |
clear_zones() |
Drops every zone and disables the rule. |
snapshot() |
{"constraints": [...], "zones": [...]} as dicts, the shape the hello and constraints messages carry. |
The zone lifecycle¶
@dataclass
class Zone:
name: str
lane: int # 1-based, numbered from the left
start_m: float # absolute road coordinate
end_m: float
A zone and the lane-closed rule share a lifecycle on purpose. The rule text
asserts a closure, and the judge weights that assertion even when the facts
carry no zone, so a stale rule would keep biasing every action in that lane
after the zone was passed. Dropping a zone arms the rule; passing the last zone
retires it. EpisodeRunner.step() calls retire_passed_zones before building
facts, and the console shows "Zone passed. The lane-closed rule was retired."
when it sees the rule flip off in the same message that empties the zone list.
stateDiagram-v2
[*] --> NoRule: session start
NoRule --> Armed: drop_zone adds the rule
Armed --> Armed: drop_zone again, another zone
Armed --> Retired: retire_passed_zones, no zones left
Retired --> Armed: drop_zone re-enables the rule
Armed --> Retired: clear_zones
Zones survive a reset
Session.reset recreates the episode but keeps the ConstraintStore,
rules and zones included. A zone dropped at 550 m stays at 670 to 870 m
in absolute coordinates, so a reset episode meets it again. Remove the
rule or wait for the ego to pass the zone to retire it; there is no
"clear zones" control in the console.
Checkers¶
Each checker is a pure function of one candidate's facts and the constraint,
returning True for a violation. They are closed-form on the pre-step state,
so every candidate gets a label every step, which is what makes five labeled
checks per rule per step available for calibration.
| Checker | Violation when |
|---|---|
check_speed_limit |
action.target_speed_after_mps is greater than the limit parsed from the text (default 25). It is the target after the action, not the current speed, so FASTER from 25 m/s is a violation and SLOWER from 30 m/s is not. |
check_lane_change_gap |
The action is LANE_LEFT or LANE_RIGHT, actually changes lane (not a road-edge no-op), and the smaller of the behind and ahead gaps in the target lane is below the threshold parsed from the text (default 10 m). Absent neighbours do not count. |
check_left_lane_passing |
The lane after the action is the leftmost, and there is no slower vehicle within 40 m ahead in the lane to its right (lane_right_of_lane_after.slower_vehicle_ahead_within_40_m). |
check_lane_closed |
Any zone in the facts has action_enters_zone true. |
check_headway |
lane_after_action.headway_s is below the seconds parsed from the text (default 2). No vehicle ahead means no violation. |
check_lane_change_rate |
The action is a real lane change and history.lane_changes_last_10_steps plus this one exceeds the count parsed from the text (default 1). The facts send Jev the same sum as action.lane_changes_last_10_steps_including_this_action, so the judge and the checker read one number. |
label(facts, constraint) returns None when the constraint has no checker;
label_all(candidates, constraints) builds {action: {constraint_id: label}}
for the step.
Scoring only, enforced by structure¶
The invariant that checkers never feed the shield is kept by the call graph rather than by a flag:
shield.decide()takes the ranking, the probability grid and the thresholds. There is no parameter through which a label could arrive.episode.pycallslabel_alland writes the result into the record'schecks[].ground_truthand intoEscalation.ground_truth.- The only consumer of
Escalation.ground_truthissimulated_operator, which stands in for a human during evaluation and headless runs. The server strips the field before sending an escalation to the console. metrics.pyandevaluate.pyread labels from the records to count violations and build the reliability diagram.
So in mock mode, in live mode and in shadow mode alike, the decision depends on Jev and the thresholds, and on nothing the checkers know.
Free-text rules¶
A rule without a checker is judged like any other. Its checks carry
ground_truth: null, the inspector shows no mark in its cells, the console
labels it "no checker, not scored", and it contributes no violations. For
calibration, collect_checks in metrics.py looks the pair up in the hand
labels under backend/logs/labels/, keyed by a hash of the candidate's facts
and the rule text; a labeled pair counts under the rule's preset id or a slug
of its text with its labeler as label_source, an unlabeled one is skipped.
interlock label collects those labels by hand and interlock autolabel by
the fixed standard. The escalation's
ground_truth ignores free-text rules, which means the simulated operator
denies an uncertain escalation fired only by them (an unknown label is
treated as a violation).
Tests¶
tests/test_constraints.py covers preset order and inferred checkers, the
fuzzy presets having none, typed variants including headway and lane-change
rate, empty text rejection, ids and toggling, zone drops in absolute
coordinates arming the rule, and retirement when the last zone is passed.
tests/test_checkers.py covers the speed rule on the target after the
action, the gap rule only on real lane changes, the passing rule needing
someone slower to the right, the zone entry fact, the headway rule on the
headway after the action, the lane-change rate counting recent changes plus
this one, free text labelling None, and label_all covering every pair.
tests/test_label.py covers the labeling command. tests/test_episode.py checks that a
zone dropped mid-episode appears in the facts and retires during the episode.