Skip to content

About

Python reinforcement schedule engine for operant contingencies. Bridges contingency-dsl AST to executable Schedule objects. 31 schedule families: atomic + wrappers + compound + differential + progressive + trial-based + aversive.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

contingency-py

πŸ‡―πŸ‡΅ ζ—₯本θͺžη‰ˆ 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.

Scope

  • 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

Install

pip install contingency-py

For development:

mise install                                # Python 3.11
mise exec -- python -m venv .venv
.venv/bin/python -m pip install -e ".[dev]"
.venv/bin/pytest

30-second example

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.

Why not a 50-line hand-written schedule?

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.uniform or exponential draws. n and seed are controllable, so a session can be replayed exactly.
  • Changeover delay in concurrent schedules. Conc routes each response to its own component, holds an arranged reinforcer until the next response on that operandum, suppresses reinforcement for COD time 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.

Check your own implementation against the fixtures

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.py

contingency-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).

Public API

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

Support

  • 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.

References

  • 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.

License

MIT. See LICENSE.

About

Python reinforcement schedule engine for operant contingencies. Bridges contingency-dsl AST to executable Schedule objects. 31 schedule families: atomic + wrappers + compound + differential + progressive + trial-based + aversive.

Topics

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages