Add a propagator
A propagator is a constraint enforced by domain filtering. Adding one means: writing a module with three Numba-jitted
functions, registering it with an ALG_* id, and adding a parameterized test.
1. Pick a name and signature
<name>is snake_case, derived from the constraint (e.g.abs_eq,sum_leq_c). Suffix_cmeans a constant parameter is involved.- Decide what
domainsandparameterscarry:domainsis anNDArrayof shape(n, 2)— one(MIN, MAX)row per variable, in a fixed order chosen by you.parametersis a 1-DNDArrayof ints (may be empty). Use it for constants, coefficients, or table data.
- Document the variable order in the
compute_domains_<name>docstring — callers rely on it.
2. Create nucs/propagators/<name>_propagator.py
The file must contain three functions and the standard copyright header (copy from header.txt — every source file in
nucs/ and tests/ starts with the ASCII-art banner).
Reference: nucs/propagators/abs_eq_propagator.py is the minimal template.
def get_complexity_<
name > (n: int, parameters: NDArray) -> int:
# Not jitted. Return an int estimate of work per call.
# Used to order propagators in the queue — relative magnitude matters, not units.
...
@njit(cache=True, fastmath=True)
def get_triggers_<
name > (n: int, variable: int, parameters: NDArray) -> int:
# Return an EVENT_MASK_* constant from nucs.constants for the given variable index.
# Controls when this propagator wakes up after another propagator filters that variable.
...
@njit(cache=True, fastmath=True)
def compute_domains_<
name > (domains: NDArray, parameters: NDArray) -> int:
# Mutate domains in place. Return PROP_INCONSISTENCY, PROP_CONSISTENCY, or PROP_ENTAILMENT.
# Use domains[i][MIN] and domains[i][MAX]; never reassign domains[i] = ....
...
Rules for the jitted functions (see [[numba-rules]] in CLAUDE.md):
- No Python objects, no exceptions, no list/dict comprehensions over heterogeneous types.
- Mutate
domainsin place. After each tightening, checkif domains[i][MIN] > domains[i][MAX]: return PROP_INCONSISTENCY. - Return
PROP_ENTAILMENTonly when the constraint can never be violated again (rare; safe to returnPROP_CONSISTENCYif unsure).
3. Register in nucs/propagators/propagators.py
Add the import alongside the others, then append a registration line. The ALG_* lines are ordered alphabetically by
id — keep that.
from nucs.propagators. < name > _propagator
import
(
compute_domains_ < name >,
get_complexity_ < name >,
get_triggers_ < name >,
)
...
ALG_ < NAME > = register_propagator(get_triggers_ < name >, get_complexity_ < name >, compute_domains_ < name >)
The returned id is the propagator's index; never hardcode it.
4. Add tests/propagators/test_<name>.py
Follow the PropagatorTest pattern (see tests/propagators/test_abs_eq.py):
class Test< Name > (PropagatorTest):
@pytest.mark.parametrize(
"domains,parameters,consistency_result,expected_domains",
[
([(lo, hi), ...], [param, ...], PROP_CONSISTENCY, [[lo, hi], ...]),
# one row per case: boundary, inconsistency, entailment, no-change
],
)
def test_compute_domains(self, domains, parameters, consistency_result, expected_domains) -> None:
self.assert_compute_domains(
compute_domains_ < name >, domains, parameters, consistency_result, expected_domains
)
Cover at minimum: a pruning case, an inconsistency case, and a no-op case where the input is already tight.
5. Verify
./scripts/bash/style.sh
NUMBA_CACHE_DIR=.numba/cache PYTHONPATH=. pytest tests/propagators/test_<name>.py
For debugging the propagator logic interactively, run with NUMBA_DISABLE_JIT=1 so tracebacks land in your Python
source.
6. Document
Add the propagator to docs/source/reference/reference_propagators.rst (the .. autofunction:: list is ordered
alphabetically by module name — keep that).
7. Wire it into the FlatZinc adapter (only if it backs a FlatZinc builtin)
If the propagator exists to support a MiniZinc/FlatZinc constraint, also connect it in nucs/fzn/:
- Add one entry to the
BUILTINSdict innucs/fzn/builtins.py, keyed by the FlatZinc builtin name. Its handler resolves the args (model.var_index_of/var_list_of/int_list_of/const_of) and callsmodel.problem.add_propagator(ALG_<NAME>, variables, parameters). - If it is a global you want MiniZinc to keep native rather than decompose, add a body-less predicate file under
nucs/fzn/share/minizinc/nucs/(seefzn_all_different_int.mzn), and key the dispatch entry on thefzn_*(or custom) predicate name that file produces.
Verify end-to-end with tests/fzn/ and, when minizinc is installed, minizinc --solver nucs.
Source: yangeorget/nucs — distributed by TomeVault.