Metadata-Version: 2.5
Name: pin-derive
Version: 0.11.3
Summary: pin-derive: numbers, choices, and subsets narrowed in every direction — Python binding over the wasm engine
Project-URL: Homepage, https://github.com/nightwork-dev/pin-derive
Project-URL: Repository, https://github.com/nightwork-dev/pin-derive
Project-URL: Changelog, https://github.com/nightwork-dev/pin-derive/blob/main/docs/RELEASE-NOTES.md
Project-URL: Issues, https://github.com/nightwork-dev/pin-derive/issues
Author: Nightwork
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: constraints,procedural-generation,solver,wasm,wasmtime
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: wasmtime>=27
Description-Content-Type: text/markdown

# 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`](https://pypi.org/project/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

```bash
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.

```python
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 `TypedDict`s 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:

```python
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:

```python
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.

```python
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](https://github.com/nightwork-dev/pin-derive).
