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.0. The catalog and the document format are complete; the deterministic
chooser roster is the next wave.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pin_derive-0.11.0.tar.gz.
File metadata
- Download URL: pin_derive-0.11.0.tar.gz
- Upload date:
- Size: 258.0 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f11c49fd615c080a7a2245d5249dcde664cef217a70ebc07d4631744539c06f8
|
|
| MD5 |
e892005803002daa0c1a86b34522c095
|
|
| BLAKE2b-256 |
73c3ea24838710110ecdb238ebe2d1ffc71eb2014bbbad8312adc1d352e30804
|
File details
Details for the file pin_derive-0.11.0-py3-none-any.whl.
File metadata
- Download URL: pin_derive-0.11.0-py3-none-any.whl
- Upload date:
- Size: 244.1 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f06f4fd5309db5d42ca821c1affbe11bf6ff6ce13f47fdbefe380722749b3954
|
|
| MD5 |
7e11bf984a542f732db50516310af78a
|
|
| BLAKE2b-256 |
03011176272489a35d0f6e704100dfe6de5858a445e20bcd5c0bd813135249eb
|