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.3.tar.gz (288.3 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.3-py3-none-any.whl (271.6 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: pin_derive-0.11.3.tar.gz
  • Upload date:
  • Size: 288.3 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.3.tar.gz
Algorithm Hash digest
SHA256 a40f3499b77948aa97467d2972a0bf1e46ec25b72bd368c769ac76929edf2113
MD5 eda6fb9bf45cfbebe05ef9bbd386ce47
BLAKE2b-256 56e65bf57eba481c2539a03e1d79d7efaddcb14b2535e4a172f0cef9a7e5412f

See more details on using hashes here.

File details

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

File metadata

  • Download URL: pin_derive-0.11.3-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.3-py3-none-any.whl
Algorithm Hash digest
SHA256 f1c41722345838c3389e980699cc50508c5513bae67031e4e07a7f4638f7cd88
MD5 4f09ef7594f9febbfb6b4665fa2abd15
BLAKE2b-256 65efea73818f3f257003c139bdcd6a8cd302aa3d282f477cdacd51e5d63ffba8

See more details on using hashes here.

Release history Release notifications | RSS feed

0.11.4

2 files

This release

0.11.3 This release

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