π―π΅ ζ₯ζ¬θͺη README
Python reinforcement schedule engine for operant contingencies.
Bridges contingency-dsl AST nodes into executable Schedule runtime objects. Pure library with no session/context/renewal concerns β those live in experiment-core.
- Atomic reinforcement schedules: FR, VR, RR, FI, VI, RI, FT, VT, RT, CRF, EXT
- Compound schedules: Concurrent (+ COD), Multiple, Chained, Tandem, Alternative
- Differential: DRO (resetting, momentary), DRL, DRH
- Progressive Ratio (arithmetic, geometric, Hantula step functions)
- Limited Hold wrapper
- Fleshler-Hoffman VI/VR distribution generator (Fleshler & Hoffman, 1962; Hantula, 1991)
- DSL bridge:
contingency.bridge.from_dsl(ast)βSchedule - HAL (
contingency.hw): virtual / serial / HIL bridge backends
pip install contingency-pyFor development:
mise install # Python 3.11
mise exec -- python -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/pytest
Write the schedule in contingency-dsl, compile it with from_dsl, and drive it with responses:
from contingency_dsl import parse
from contingency_dsl.annotations import TemporalExtension
from contingency_dsl.extensions import ExtensionRegistry
from contingency import ResponseEvent, from_dsl
registry = ExtensionRegistry.build(TemporalExtension())
source = '@algorithm("fleshler-hoffman", n=12, seed=42)\nConc(VI30s, VI60s, COD=2s)'
schedule = from_dsl(parse(source, extensions=registry))
# Simulated subject: one response every 0.5 s, switching operanda every 10 s, for 10 min.
reinforcers = {"c0": 0, "c1": 0}
for i in range(1, 1201):
now = i * 0.5
operandum = "c0" if (i // 20) % 2 == 0 else "c1"
outcome = schedule.step(now, ResponseEvent(time=now, operandum=operandum))
if outcome.reinforced:
reinforcers[operandum] += 1
print(reinforcers) # {'c0': 9, 'c1': 3} β identical on every run (seeded)contingency-dsl-py is installed as a dependency. The same Schedule object runs inside a full session via session-runner.
A short hand-rolled loop usually gets the following wrong, and each one changes the data you would report:
- Variable-schedule intervals. VI/VR values are generated with the Fleshler-Hoffman (1962) progression (the Hantula, 1991, program variant is also available), not ad-hoc
random.uniformor exponential draws.nandseedare controllable, so a session can be replayed exactly. - Changeover delay in concurrent schedules.
Concroutes each response to its own component, holds an arranged reinforcer until the next response on that operandum, suppresses reinforcement forCODtime units after a changeover, and supports a fixed-ratio changeover requirement (FRCO) and direction-specific COD. - Conformance fixtures. 22 JSON fixtures under
conformance/pin the exact outcome sequence of each schedule for a known event sequence. The Rust port contingency-rs replays the same fixtures, so both runtimes are checked against one specification. - Round trip with Method sections. Because the schedule is written in contingency-dsl, contingency-dsl2procedure turns it into a Method-section paragraph and contingency-procedure2dsl extracts a schedule from published Method text.
- Input validation. Non-monotonic time, malformed parameters, and unknown operanda raise explicit errors instead of silently producing wrong reinforcement times.
pip install contingency-py also installs the conformance corpus and the contingency-conformance command. Any implementation, in any language, can be checked against the same 22 fixtures:
pip install contingency-py
contingency-conformance list # fixtures, kind, schedule type, steps
contingency-conformance run --only 'atomic/fr_*' -- python my_adapter.pycontingency-conformance path prints the fixture directory so a harness in another language can read the JSON files directly; CONTINGENCY_CONFORMANCE_DIR or --dir points the command at another copy.
Observed JSON. For each fixture, the implementation reports one entry per fixture step:
{"fixture": "atomic/fr_basic.json",
"steps": [{"reinforced": false},
{"reinforced": true, "reinforcer": {"time": 5.0, "magnitude": 1.0, "label": "SR+"}}]}reinforced must match exactly; when reinforced, reinforcer.time and magnitude must agree within 1e-9 and label must be equal; every meta key in the fixture must be present with the same value (extra keys are allowed); the number of steps must match. Write such files yourself and compare them with contingency-conformance check observed/*.json, or let run drive an adapter.
Adapter for run -- CMD. run starts CMD once per fixture, writes the fixture JSON to its stdin, and reads the observed JSON from its stdout (fixture may be omitted):
# my_adapter.py β a hand-written FR schedule checked against the fixtures
import json
import sys
class MyFR:
def __init__(self, n):
self.n, self.count = n, 0
def step(self, now, event): # event: None or {"time", "operandum"}
if event is None:
return None
self.count += 1
if self.count < self.n:
return None
self.count = 0
return {"time": event["time"], "magnitude": 1.0, "label": "SR+"}
def observe(fixture):
schedule = MyFR(fixture["schedule"]["params"]["n"])
steps = []
for step in fixture["steps"]:
sr = schedule.step(step["now"], step["event"])
steps.append({"reinforced": True, "reinforcer": sr} if sr else {"reinforced": False})
return {"implementation": "my-fr", "steps": steps}
if __name__ == "__main__":
json.dump(observe(json.load(sys.stdin)), sys.stdout)This adapter implements FR only and passes atomic/fr_basic.json; without --only, the other 21 fixtures are reported as ERROR because the adapter cannot build their schedules. To cover them, build each schedule type in observe. The 5 compound fixtures (Alternative, Chained, Concurrent, Multiple, Tandem) also require meta on some steps, such as current_component, alternative_winner, or cod_suppressed; the fixture files show which keys each step expects.
contingency-conformance replay-python is the same adapter for this package's own runtime; contingency-conformance run -- contingency-conformance replay-python passes all 22 fixtures.
Seeded fixtures. Six fixtures (seeded VR, VI, VT, RR, RI, RT) are pinned by Python's random.Random(seed). --stochastic chooses how they are judged:
count(default): the number of reinforcers must lie within 0.5-1.5 times the recorded number, and the first reinforcer must not precede the first step. Use this when your implementation has its own random number generator.strict: judged like the deterministic fixtures. Use this when you reproduce Python's generator.skip: not checked.
Output is one PASS / FAIL / ERROR / SKIP line per fixture with the first mismatch (step, field, expected, observed), then a summary; --json gives the same as JSON. The exit code is 0 when nothing failed, 1 when a fixture failed or the adapter crashed, and 2 for input errors (corpus not found, unknown fixture, unreadable file).
from contingency import Schedule, Observation, Reinforcer
from contingency.schedules import FR, VR, FI, VI, FT, VT, CRF, EXT, DRO
from contingency.schedules import Concurrent, Multiple, Chained, Tandem, Alternative
from contingency.builder import ScheduleBuilder
from contingency.bridge import from_dsl- Report bugs, request features, and ask usage questions in the issue tracker. For a bug, include the schedule expression (or the code that builds the schedule), the response/event sequence that reproduces it, the expected and observed outcomes, and the versions of Python, contingency-py, and contingency-dsl-py.
- The project is maintained by a single developer on a best-effort basis. There is no guaranteed response time, but every issue is read.
- See CONTRIBUTING.md for how to contribute changes and CODE_OF_CONDUCT.md for the code of conduct.
- Fleshler, M., & Hoffman, H. S. (1962). A progression for generating variable-interval schedules. Journal of the Experimental Analysis of Behavior, 5(4), 529-530. https://doi.org/10.1901/jeab.1962.5-529
- Hantula, D. A. (1991). A progression for generating variable-interval schedules. Journal of the Experimental Analysis of Behavior, 56(2), 391-392.
- Catania, A. C. (1966). Concurrent operants. In W. K. Honig (Ed.), Operant behavior: Areas of research and application (pp. 213-270). Appleton-Century-Crofts.
- Ferster, C. B., & Skinner, B. F. (1957). Schedules of reinforcement. Appleton-Century-Crofts.
MIT. See LICENSE.