Defaults and limits¶
Every number the library uses to decide "this is too big" or "this is not enough evidence" has a default, and you can change any of them for one module. This page lists each one with its meaning, its unit, what happens when it is exceeded, and when you would change it. It assumes you have made one decision already (Your first decision); words in italics on first use are in the glossary.
Defaults are starting points, not tuned values
The four data-check settings (reconstruction_margin,
operational_fk_prefactor, conformal_alpha, min_support) and the
calibration set carry no error rate as shipped: they make the examples run. In
production, learn them from your labelled history; the learned
values replace these defaults and come with a certified error rate
and a provenance record
(Learn parameters from your history).
On a synthetic history, the defaults acted with 7.94% of acts wrong
where settings learned for a 5% target had 0.93%
(Where every number comes from).
The size caps below are engineering budgets and are not learned.
Every value on this page was read from the installed package with:
import dataclasses
from grounded_decision import DEFAULT_LIMITS
print(dataclasses.asdict(DEFAULT_LIMITS))
How to change a value¶
All size caps and check thresholds live in one frozen object, Limits.
Build a new one with the fields you want to change and pass it when you
build the module. Everything you do not name keeps its default.
from grounded_decision import Limits, Module, parse
strict = Limits(conformal_alpha=0.05, require_conformal_calibration=True)
module = Module.from_expr(
"fraud", parse("test(suspicious) ; act(block)"), limits=strict
)
Pass the same object to dispatch(subgoal, limits=strict) if you use
the router, so its caps match. The values a
decision actually used are recorded on it: channels[].metrics
contains margin, alpha and min_support, so an auditor can see which settings
produced a verdict.
The Limits fields¶
Thresholds the checks use on every decision¶
| Field | Default | Unit | Meaning | When to change |
|---|---|---|---|---|
reconstruction_margin |
0.05 |
evidence strength | The neighbour-agreement check passes only when evidence strength (average links × agreement²) is above 1 + margin, so 1.05 by default. | Learn it from history (grounded-decision learn). By hand: raise it (0.2, 0.5) when acting wrongly is expensive and you would rather hold more; lowering it toward 0 brings the bar to the theoretical line at 1. The learner's grid also tries -1.0, which leaves only a requirement of some agreement. |
conformal_alpha |
0.1 |
fraction (0 to 1) | The unusual-case check passes when the conformal p-value is above α. Roughly: hold a case when fewer than 10% of recent good cases were at least as unusual. | Learn it from history (grounded-decision learn; 0 switches the check off). By hand: lower it (0.05) to hold fewer cases as unusual; raise it (0.2) to hold more. See the worked example below. |
require_conformal_calibration |
False |
yes/no | When True, an empty calibration set fails the unusual-case check. When False (default), an empty set gives p = 1 and the check passes, so it is silently switched off. |
Set True in production once you supply calibration scores, so a broken feed holds decisions instead of passing them. |
operational_fk_prefactor |
2.0 |
dimensionless | The constant in front of the practical knife-edge window width, \(2 \ln(1/(2\varepsilon)) / (\ln n)^2\). Larger means a wider window, so more decisions held near your cut-off. | Learn it from history (grounded-decision learn; 0 switches the window off). By hand: raise it if decisions near p_threshold flip too often in replay; lower it if the window holds too much at your typical size. |
friedgut_kalai_prefactor |
524288.0 (\(2^{19}\)) |
dimensionless | The constant from the published theorem. Used only to report the theorem's window width, which is vacuous (wider than 1) at every realistic size. Never changes a verdict. | Never. It is there so the record shows what the theorem alone would give. |
min_support |
0.5 |
fraction (0 to 1) | The support check passes when at least this share of the related cases carry the outcome the proposed action implies (EvidenceSnapshot.support). |
Learn it from history (grounded-decision learn; candidates 0 (off), 0.5 to 0.9). By hand: raise it when acting against a mixed neighbourhood is expensive. |
require_support |
False |
yes/no | When True, evidence without a support share fails the support check. When False (default), unmeasured support passes (support not measured). |
Set True once every decision's evidence measures support, so a broken mapping holds decisions instead of passing them. |
Size caps checked when a module is built¶
These protect you from a policy that would take too long or too much
memory to compile. Exceeding one either raises an error from
Module.from_expr (so you find out at deploy time, not at decide time)
or makes the progress check fail on every decision.
| Field | Default | Unit | What happens when exceeded | When to change |
|---|---|---|---|---|
max_star_height |
3 |
levels of nested * |
StarHeightError at build time. |
Almost never. A policy with repeats nested four deep is very hard for a person to review; rewrite it instead. |
max_nfa_states |
512 |
automaton states | CompileError("NFA has … states > 512") at build time. |
Raise it for a long, flat policy (hundreds of events) that is legitimately large. |
max_dfa_states |
256 |
automaton states | CompileError("DFA exceeded 256 states") at build time. |
As above. For scale: a policy of 30 test(e) ; act(a) alternatives compiles to 32 states. |
max_game_vertices |
200 |
states in the policy chart | The progress check cannot be solved; it fails, so every decision from this module abstains. No exception is raised. | Raise it with max_dfa_states if you raise that above 200. |
max_parity_depth |
64 |
recursion levels | Same as above: the progress check fails on every decision. | Only if you hand-build a chart with more than 64 distinct priorities. |
max_karp_vertices |
200 |
states in the policy chart | The mean-payoff solver raises GameTooLarge (… vertices exceed the mean-payoff limit 200); the progress check fails closed on every decision. With the defaults, max_game_vertices is reached first. |
Raise it together with max_game_vertices. Karp's table grows with the square of the number of states. |
Caps used by the router and the compare helpers¶
| Field | Default | Unit | What happens when exceeded | When to change |
|---|---|---|---|---|
max_wl_k |
2 |
Weisfeiler–Leman level k | dispatch returns a refusal labelled exptime_trap; refine raises WLRejected. |
Keep it. Level k costs roughly \(n^{k+1}\) work, so this is a cost budget, not a hardness result. |
max_table_cells_exact |
64 |
table cells | sample_two_row(..., limits=) raises ValueError: N cells exceed the exact-sampling limit 64; use sample_switch. |
Raise it if exact sampling of wider two-row tables is fast enough for you. |
max_switch_steps |
10000 |
chain steps | sample_switch(..., steps=N, limits=) raises ValueError: N switch steps exceed the limit 10000 (the default steps is 2,000). |
Raise it if you need longer chains and can afford the time. |
Performance budgets¶
These two fields are targets, not timers: nothing stops a slow
call. tests/test_performance_budget.py checks them on the library
itself in CI, by building the fraud_review module and requiring the
median of 200 decide calls to be under decide_ms_budget and the
median of 20 module builds to be under compile_ms_budget. The
measured costs are far lower; see
Limits and performance.
| Field | Default | Unit | Meaning |
|---|---|---|---|
decide_ms_budget |
50.0 |
milliseconds | Target for one decide call. See FAQ: What's the latency?. |
compile_ms_budget |
200.0 |
milliseconds | Target for building one module. |
Other defaults¶
These are not in Limits; they are arguments or fields with default
values.
| Where | Default | Meaning | When to change |
|---|---|---|---|
EvidenceSnapshot.n_states, snapshot_from_graph(n_states=) |
2 |
Number of label values a case can have (q). Used when turning raw agreement into neighbour agreement: \( \lambda = (q\,p_{\mathrm{same}} - 1)/(q-1) \). | Set it to the real number of labels (for example 3 for low / medium / high). For 5 or more labels the 1.05 bar is a policy choice rather than a theorem; see reconstruction theory. |
EvidenceSnapshot.scores |
() (empty) |
The calibration set. Empty means the unusual-case check passes with p = 1, unless require_conformal_calibration=True. |
Supply it in production, from learned parameters (learned.apply(evidence, proposed_action)). With α = 0.10 you need at least 9 scores before the check can ever fail. |
EvidenceSnapshot.label_score |
0.0 |
This case's unusualness score. 0.0 is the least unusual possible if your scores are non-negative, so the check passes. | Always supply it when you supply scores. |
EvidenceSnapshot.support, snapshot_from_graph(supporting_label=) |
None |
The support share; None means not measured, and the support check passes unless require_support=True. |
Supply it (pass supporting_label) once you have a mapping from actions to the outcomes they imply. |
snapshot_from_graph(agreement_baseline=) |
"uniform" |
How neighbour agreement is measured: against equal label proportions, as the Kesten–Stigum result assumes. | "label_mix" when one label dominates everywhere and links may be random; not for judging a homogeneous ring. See Do the related cases agree?. |
threshold_report(epsilon=) |
0.1 |
ε in the window formula: how sure the theory wants to be that the answer has settled. Module.decide always uses 0.1. |
Only when calling threshold_report yourself to explore. |
Module.from_expr(envelope=) |
Envelope.P_EXACT |
The hardness label recorded on every decision. Does not change decide. |
Leave it unless your records need a different label. |
Statechart().state(priority=, payoff=, environment=) |
0, 0.0, False |
Inputs to the progress check. A chart left at priority 0 everywhere passes the priority part whatever its shape. Text policies get priority 2 on states with an action or no way out, 1 elsewhere. | Mark loops the workflow must not circle in forever with odd priorities, and states someone else controls with environment=True. See Can the workflow keep going?. |
compare_outcome_laws(tau=) |
none (required) | Tolerance for "the distribution did not move". | Choose per comparison; see Compare outcomes and features. |
Defaults of the learner¶
These are the defaults of grounded_decision.learn
(Learn parameters from your history).
| Where | Default | Meaning |
|---|---|---|
RiskTarget(max_risk=) |
none (required) | The error rate you accept. A business input; --max-risk on the command line. |
RiskTarget(confidence=) |
0.95 |
How sure the certificate must be. |
RiskTarget(measure=) |
error_rate_among_acts |
Or false_act_rate (share of all decisions that act and are wrong). |
ParameterGrid.reconstruction_margins |
-1.0, 0.0, 0.05, 0.25, 0.5, 1.0, 2.0, 4.0 |
Candidate margins; -1.0 asks only for some agreement. |
ParameterGrid.window_prefactors |
0.0, 0.5, 1.0, 2.0, 4.0 |
Candidate window prefactors; 0.0 switches the knife-edge window off. |
ParameterGrid.decision_lines |
None (each case's own p_threshold) |
--learn-decision-line adds 5 quantiles of observed density (grid ×6). |
ParameterGrid.conformal_alphas |
0.0, 0.02, 0.05, 0.1, 0.2 |
Candidate alphas; 0.0 switches the unusual-case check off. |
ParameterGrid.minimum_supports |
0.0, 0.5, 0.6, 0.7, 0.8, 0.9 |
Candidate support minimums; 0.0 switches the support check off. |
ParameterGrid().size |
1200 |
Combinations tried (8 × 5 × 1 × 5 × 6). |
learn_parameters(procedure=) |
"fixed_sequence" |
Test the combinations in an order fixed from the calibration split, each at the full significance, stopping at the first failure. "bonferroni" divides the significance by the grid size instead. --procedure on the command line. |
learn_parameters(calibration_fraction=) |
0.3 |
Share of history used to build calibration scores. |
learn_parameters(holdout_fraction=) |
0.2 |
Share kept aside for the honest report; the remaining 0.5 is the risk split. |
learn_parameters(seed=) |
0 |
Seed for the random split; recorded in the provenance. |
| Settings when nothing is certified | margin 1e12, prefactor 2.0, alpha 0.1, support 0.5 |
Holds every decision (fails closed); exit code 2 on the command line. |
RiskMonitor(window=) |
500 |
Recent outcomes kept for the live check. |
make learn variables |
HISTORY=history.jsonl, MAX_RISK=0.02, PARAMETERS=parameters.json |
Input file, target and output file. |
The knife-edge window at different sizes¶
The knife-edge check holds a decision when the link density \( \hat p \)
is within a half-width of your cut-off p_threshold. The half-width
depends only on how many related cases there are (n). Values from
grounded_decision.evidence.thresholds.operational_width(n, 0.1) with
the default prefactor:
Related cases n |
Half-width (held if density is this close to the cut-off or closer) | What it means |
|---|---|---|
| 2 to 6 | 0.5 | Covers every density between 0 and 1: always held unless your cut-off is near 0 or 1 and density is at the other end. |
| 7 | 0.425 | |
| 10 | 0.304 | Density must be about 0.3 away from the cut-off. |
| 16 | 0.209 | |
| 20 | 0.179 | |
| 40 | 0.118 | |
| 80 | 0.084 | The size used by most examples. |
| 200 | 0.057 | |
| 1000 | 0.034 |
So graphs of under about ten related cases nearly always fail this check. That is intended: with so few cases, a single extra link moves the density by a lot. If your groups are routinely that small, read Are we on a knife-edge? and Choose thresholds and calibration.
Worked example: what α = 0.10 holds¶
With a calibration set of 40 scores spread evenly from 0.0 to 0.975
([i/40 for i in range(40)]), conformal_report gives:
| This case's score | Calibration scores at least as high | p-value | Verdict of the check |
|---|---|---|---|
| 0.875 | 5 | 0.146 | pass |
| 0.900 | 4 | 0.122 | pass |
| 0.925 | 3 | 0.098 | fail |
| 0.950 | 2 | 0.073 | fail |
The p-value is \( (1 + \text{count}) / (1 + 40) \). At α = 0.10 the check fails once three or fewer of the 40 recent cases were at least as unusual. At α = 0.05 it would fail only at one or none.
What these settings do not do¶
min_supporthas no effect unless the evidence carries a support share. Without it, the direction comes from the event you send.- The timing budgets are checked by a test on the library, not in your service, and nothing interrupts a slow call.
- Changing
Limitschanges the settings fingerprint (parameters_fingerprint) in the reference service, not the policy fingerprint, so saved states stay valid.
Learning the data-check settings from your own outcomes: Learn parameters from your history. What each setting does when set by hand: Choose thresholds and calibration. Capacity planning and measured costs: Limits and performance.