Skip to main content

pin-derive

Python binding for pin-derive. One Network object: a builder, moves per value kind, a solve, and the reads. A cell holds partial information about a number (an interval), a choice (one member of a universe), or a subset (which members of a universe). A relation narrows every cell it touches from every other, in every direction, with no notion of input or output.

The engine is the Rust core, bundled as a wasm binary and run through wasmtime: a pure-Python wheel on every platform wasmtime supports, with no Rust toolchain and no compilation at install time. The wasm boundary is a handle, so construction crosses text once and solves and hot reads cross scalars and little-endian u64 mask words.

This is the same program as the JavaScript binding (the pin-derive npm package) and the Rust crate (pin-derive-core), modulo case convention.

Install

uv add pin-derive        # or: pip install pin-derive

Requires Python 3.11+. The only dependency is wasmtime.

Quickstart

This is the spec section 4 program. js/smoke.mjs is the same program in JavaScript; diff them.

from pin_derive import (
    Network, by, count, permits, point, product, quantize, ranges, sum_over,
)

catalog_ids = ["potion", "gem", "rope", "torch"]
catalog_prices = {"potion": 4, "gem": 9, "rope": 2, "torch": 1}

with Network() as net:
    price = net.cell("price", {"lo": 0})
    margin = net.cell("margin", {"lo": 0})
    profit = net.cell("profit", {"lo": 0})
    model = net.finite("model", ["krea2", "flux"])
    sampler = net.finite("sampler", ["euler", "dpmpp_2m", "res_multistep"])
    steps = net.cell("steps", {"lo": 1, "hi": 100})
    raw = net.cell("raw", {"lo": 0})
    width = net.cell("width", {"lo": 0})
    loot_count = net.cell("lootCount", {"lo": 0})
    spend = net.cell("spend", {"lo": 0})
    loot = net.set("loot", catalog_ids)

    net.relate(product(profit, price, margin))
    net.relate(permits(model, sampler, {
        "krea2": ["euler", "res_multistep"],
        "flux": ["euler", "dpmpp_2m"],
    }))
    net.relate(ranges(model, {"krea2": {"steps": [4, 12]}, "flux": {"steps": [20, 30]}}))
    net.relate(quantize(width, raw, by(model, {"krea2": 16, "flux": 64})))
    net.relate(count(loot_count, loot))
    net.relate(sum_over(spend, loot, catalog_prices))

    (net.pin(price, point(50))
        .pin(margin, point(0.2))
        .pin(steps, {"lo": 20, "hi": 30})
        .pin(loot_count, {"lo": 3, "hi": 5})
        .pin(loot, has_none=["torch"], name="no-torch"))

    net.solve()                 # "converged"

    net.value(profit)           # {"lo": 10.0, "hi": 10.0}
    net.possible(model)         # ["flux"] — the steps pin eliminated krea2
    net.must(loot)              # ["potion", "gem", "rope"] — counting closed the set
    net.value(spend)            # {"lo": 15.0, "hi": 15.0}
    net.decisions()             # the cells still open, with their remaining space
    net.eliminated(model)       # krea2, the relation that removed it, and the moves
    net.why(model)[0]["sentence"]

What a cell can be, and the four ways it can stand

A cell holds one of three kinds of partial information, and net.status(cell) reports which of four states it is in — the same four words for all three kinds:

status means
free nothing has narrowed it yet; the whole declared space is still open
bounded narrowed, but more than one value survives — a real decision remains
point exactly one value survives; the network has determined it
conflicting nothing survives; the pins and relations cannot all hold

net.decisions() is the list of cells still bounded — what a chooser or a person still has to settle. net.conflicts() is the conflicting ones with the moves that emptied them. A conflict is a normal, readable state, not an exception: you look at it, unpin(name) the move that caused it, and solve again.

Values and moves

cell kind declare pin / commit
number net.cell(id, {"lo":…, "hi":…}) point(x), {"lo":…, "hi":…}, or lo=/hi=
choice net.finite(id, universe) a member list (restrict) or restrict= / exclude=
subset net.set(id, universe) a member list (exactly) or has_all= / has_none= / exactly=

pin and commit are chainable and take the same arguments; a commit is the same narrowing with chosen provenance, which is how a chooser's proposal lands. The assigned move name lands on net.last_move and is what unpin(name) addresses. A member that is not in the cell's universe raises PinDeriveError naming the cell.

Reads

value, status, possible, must, may cross as scalars and masks. decisions, surface, conflicts, why, eliminated, describe, export, stats, rows, validate cross as JSON because they are cold. rank(cell) returns a choice cell's (lo, hi) position over an ORDERED universe, and None when the universe declared no order — never [0, n-1], because there is no fact to report. Every read shape and the document format are TypedDicts exported from pin_derive, and the package ships py.typed, so mypy and an editor show the whole contract.

Sessions

fork() returns a read-only network over a settled copy of the revision. preview(moves) returns the snapshot a batch would produce without mutating anything; its outcome is the same word solve() reports.

commit_batch(moves, accept=…, expect=…) is the atomic commit. expect is the BASE FINGERPRINT — the 64-hex string solve_fingerprint() returned before the batch was built — not a revision number, which moves only when the structure widens and so accepted every stale proposal. Committing against a base someone else has moved past raises, and the message says stale base; re-read solve_fingerprint() and propose again. A batch turned down by the ACCEPTANCE PREDICATE is the other thing entirely: it comes back as {"accepted": False, "conflicts": [...]} rather than a raise.

snapshot_fingerprint() identifies the SOLVED state (values, decisions, conflicts) rather than the input state, and base.decision_delta(next) diffs two sessions of one network over move names: {pins: {added, removed, replaced}, commits: {…}}.

Every move takes meta= — any JSON-serialisable value, opaque provenance the caller owns, carried to the ledger and back out of export(). None means absent, not a JSON null.

Network.import_(doc) (also spelled Network.from_document) builds a network from a document, and export() writes one back with every relation in its authored form — permits exports as permits, never as its table expansion.

A Network owns a wasm handle, so it is a context manager:

with Network() as net:
    ...

Outside a with, call net.close(). Reading a closed network raises rather than touching a stale handle.

Relation constructors

Plain functions returning the catalog JSON, so a descriptor is inspectable, storable and diffable before anything is built: equal, sum_, linear, product, affine, quantize, lt, lte, within, min_, max_, clamp, lookup, curve, pow_, table, select, count, sum_over, member_of, subset_of, permits, ranges, compat, all_, any_. Parameters that depend on a choice or on magnitude are by(cell, map) and bands([(up_to, value), …]).

The names are the JavaScript binding's in snake_case; the six that would shadow a Python builtin take the PEP 8 trailing underscore (sum_, min_, max_, pow_, all_, any_, plus range_ for the value helper).

Masks

A mask is a run of little-endian u64 words; bit i of word w is universe index 64*w + i, and members always cross in universe order. Python's arbitrary-precision int is that bitset: mask_from_indices(indices, n) and indices_from_mask(mask, n) are the public conversions, and words_for(n) is the wire width. The binding does this for you on every pin, possible, must and may.

Bundled wasm

pin_derive/pin_derive_wasm.wasm ships inside the wheel and is the pin-derive-wasm cdylib built for wasm32-unknown-unknown. init() runs on import; pass a path or bytes to point the binding at another build:

from pin_derive import init
init("path/to/pin_derive_wasm.wasm")

init checks pd_abi_version against the binding's ABI_VERSION and refuses a mismatched pair rather than mis-reading a buffer. memory_pages() reports the engine's linear memory in 64 KiB pages — a handle ABI has exactly one way to be wrong that a functional test cannot see, and a leak check that cannot read the pages is not a leak check.

The wasmtime store and the engine's handle slab are single-threaded: use one interpreter thread, or one process per thread.

Coming from 0.10

0.11 is a new engine and a new API. The 0.10 entry point — solve_spec() over a JSON spec document — is gone; there is no deprecation shim. Where 0.10 handed the whole problem over as one document and got a result back, 0.11 gives you a live Network: you declare cells, relate them, pin, solve, read, unpin, and solve again, and every read tells you why.

Documents survive the change. A 0.10 spec document still loads through Network.import_(doc) — also spelled Network.from_document(doc) — and export() writes the format back. Every valid document in the project's compatibility corpus imports and solves unchanged. So if you have authored specs on disk, they are your migration path: import them, then use the network API from there.

import json
from pin_derive import Network

with Network.import_(json.load(open("my-0.10-spec.json"))) as net:
    net.solve()

Pre-1.0 semver applies. A minor release may break; a ^0.10 range does not pull 0.11 in. Pin what you depend on.

Status

0.11.1, the current release on PyPI. The catalog and the document format are complete, and the deterministic chooser roster (Rounding, NearestDefault, Lp, Fill) ships in the Rust core — it is not yet projected through this binding, which is the next wave here.

License

Apache-2.0. Source, design notes, and the full reference live in the pin-derive repository.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pin_derive-0.11.4.tar.gz (288.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pin_derive-0.11.4-py3-none-any.whl (271.6 kB view details)

Uploaded Python 3

File details

Details for the file pin_derive-0.11.4.tar.gz.

File metadata

  • Download URL: pin_derive-0.11.4.tar.gz
  • Upload date:
  • Size: 288.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pin_derive-0.11.4.tar.gz
Algorithm Hash digest
SHA256 ccbf80e4928cd8db1ad1beace9466a467d12d24a0fe4c605bbba6391120be327
MD5 6641049f6c6d4d41cd564cf3b5502268
BLAKE2b-256 23855a79860bcc06d45b71df22c90b5cf313f7d7b1f3afeb3d63430cbab9d89d

See more details on using hashes here.

File details

Details for the file pin_derive-0.11.4-py3-none-any.whl.

File metadata

  • Download URL: pin_derive-0.11.4-py3-none-any.whl
  • Upload date:
  • Size: 271.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.10.9 {"installer":{"name":"uv","version":"0.10.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for pin_derive-0.11.4-py3-none-any.whl
Algorithm Hash digest
SHA256 a11b80cf2dd77f06a8b85a2fdceb60709fc1aa9a018b23f00ad2120780734f00
MD5 b9da386d0635493a947a0c2649cc8be7
BLAKE2b-256 57cb82f4b7d043cc9675a272a7b4aa5818610a29710e3f09e805304410c2b326

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.11.4 This release

2 files

0.11.3

2 files

0.11.2

2 files

0.11.1

2 files

0.11.0

2 files

0.10.2

2 files

0.10.1

2 files

0.10.0

2 files

0.4.1

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page