Skip to main content

undetermined

PyPI npm ci license MIT

Point it at a program; get back what can and cannot be determined about it.

Plenty of libraries fit a curve to measurements and hand you back a number. This one hands back a number with the error bar it was decided by, plus an explicit undetermined list with a reason on each entry, and it will put an observable on that list rather than fit a plateau to a drift.

heads: 1.9978 +/- 0.0032   4 rungs from truth=8 agree within 2.0 sigma
flat:  UNDETERMINED        no run of 3 rungs agrees; the constant is still moving
                           at the top of the ladder

The second line is the point. A tool that always produced the first line would be useless and would still pass every test that checks it produces one.

Ships as undetermined on PyPI and on npm, from one tree, at one version. No dependencies in either half.


Install

pip install undetermined
npm install undetermined

Use

You supply an adapter: an object that knows how to run the thing you are measuring at a controllable input size, and nothing else. This library never knows what program it is looking at.

import random
from undetermined import characterize

class Coin:
    def truths(self):
        # The controllable input values, known by construction. A ladder, not a point:
        # a constant that is only measured at one size cannot be shown to have settled.
        return [8, 32, 128, 512]

    @property
    def observables(self):
        def heads(truth, seed):
            r = random.Random(seed)
            return sum(1 for _ in range(truth) if r.random() < 0.5)

        def flat(truth, seed):
            r = random.Random(seed)
            return sum(1 for _ in range(12) if r.random() < 0.5) + 0.5

        return {"heads": heads, "flat": flat}

report = characterize(Coin(), trials=2500)

report["per_observable"]["heads"]["constant"]      # 1.9978...
report["per_observable"]["heads"]["constant_se"]   # 0.0032...
report["undetermined"]                             # ['flat']
report["notes"]                                    # why each one is on the list
import { characterize } from "undetermined";

const report = characterize(adapter, { trials: 2500 });
report.per_observable.heads.constant;   // 1.9978...
report.undetermined;                    // ['flat']

The adapter protocol

member required meaning
truths() yes the controllable input values, known by construction
observables yes {name: (truth, seed) -> number}, at least two
instances() no sibling instances of the same family
knobs no {name: [values]}: parameters of the program itself
perturbed(knob, v) with knobs the observables with that knob set to that value

At least two observables, always. With one there is no choice to make, so the library cannot be shown to make one; it raises rather than reporting a confident single answer.

Deriving the trial count instead of guessing it

characterize takes a trials count. If you would rather state the precision you need and have the budget derived:

from undetermined import to_tolerance
report = to_tolerance(MyAdapter(), tolerance=0.01)   # 1% on the constant

It doubles the trial count until every determined constant is inside the tolerance or the cap is reached, and reports which ones never got there. tolerance=0 raises: a tolerance is a decision about your problem, and this library will not choose it for you.


What it refuses, and why

Three refusals, and each is the answer to a way of being confidently wrong.

An observable that ignores its seed raises immediately. fit averages an observable over trials different seeds, so an observable that reads the clock or an unseeded RNG produces a mean over noise. A mean over noise still has a standard error, still forms a ladder, and can still plateau; every guard downstream compares against that error, so a broken adapter does not produce a wrong-looking answer, it produces a confident one. Each observable is called twice with the same (truth, seed) at the first and last rung; a disagreement is a wiring error, not a finding, so it throws.

A constant that never settles is reported as UNDETERMINED with the reason. A run of three consecutive rungs must agree within two combined standard errors before any plateau is reported; the value is then the inverse-variance-weighted mean over that run, and the report says which rung it started from.

A choice between observables that is not earned is reported as UNDETERMINED. With instances(), an observable is only called informative if its constant varies at least 3x its own measurement error across instances, and it is only chosen over the runner-up if it beats it by 3x. Two observables that both vary a lot are not a choice.

The rule underneath all of it

Compare against the noise, never against the size.

A spread only means something in units of the error on the thing that spread. Dividing by the magnitude instead is how a large number gets mistaken for a real one, and it is the single mistake this library is shaped around not making.

The thresholds

They are the contract, and python/tests/test_parity.py asserts both halves hold the same ones and produce the same output on the same numbers, including the same explanatory strings.

constant value what it gates
PLATEAU_K 2.0 sigma within which rungs must agree
PLATEAU_RUN 3 consecutive agreeing rungs required
MIN_RATIO 3.0 error-multiples an observable must vary by to be informative
MIN_MARGIN 3.0 factor by which the winner must beat the runner-up
SIGMAS 3.0 standard errors in a minimum detectable effect
FLOOR_TRIALS / CAP_TRIALS 400 / 40000 the budget search bounds

One formatter

The why strings are part of that contract: a consumer that quotes one is quoting both halves, so every number they carry is rendered by a rule this package writes out rather than by whatever each language's printf does. %g and toPrecision(6) agree only on integers below 10^6 and non-integers in [1e-4, 1e6); %.1f and toFixed(1) round halves in opposite directions. The rule lives in undetermined.fmt (Python) and undetermined/fmt (npm), and both halves are also pinned to the same expected strings independently, because two halves that had drifted together would still agree with each other:

the digits the shortest decimal that round-trips to the double: repr and String both produce exactly that, and agree
rounding half away from zero, applied to those digits
an exact integer written out in full, never rounded and never in exponent form: a rung at 16777216 is not clarified by calling it 1.67772e+07
anything else 6 significant digits, trailing zeros stripped, exponent notation outside [1e-4, 1e6) with the exponent padded to two places

This is a library, not a CLI

An adapter is a code object with closures in it. There is nothing to pass on a command line that would not amount to naming a Python or JavaScript symbol and importing it, so the package ships an import and no console script.

Where it sits

Layer 0: no dependencies inside or outside this network of packages, by design.

The nondet edge was considered and rejected. nondet addresses a function as FILE::NAME so it can re-run it in fresh processes; this library's observables are closures inside an adapter object and have no such address, so nondet cannot probe them. Wiring it in would have meant either a fake file path or a check that never ran: a dependency that looks like a guarantee and is not. The reproducibility precondition is implemented natively in both halves instead, and it is checked in the same call that would have needed the guarantee. nondet remains the right tool for the functions your adapter calls, which do have addresses; running it on those is a good idea and is not something this package can do on your behalf.

Development

python3 -m unittest discover -s python/tests -v   # PYTHONPATH=python
node --test js/test/*.test.js

The parity suite skips when node is not on PATH, so a Python-only contributor can still run everything else. CI asserts it was not skipped: a skipped test and a passing one look identical in a tally.

License

MIT

Download files

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

Source Distribution

undetermined-0.1.3.tar.gz (18.3 kB view details)

Uploaded Source

Built Distribution

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

undetermined-0.1.3-py3-none-any.whl (15.8 kB view details)

Uploaded Python 3

File details

Details for the file undetermined-0.1.3.tar.gz.

File metadata

  • Download URL: undetermined-0.1.3.tar.gz
  • Upload date:
  • Size: 18.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for undetermined-0.1.3.tar.gz
Algorithm Hash digest
SHA256 e7a0d0974ac00246ea315cdac69392aa683ad252555751d31aae2f695c533e4a
MD5 53a9062bb21f79c7881e32bff8c4542e
BLAKE2b-256 379d46ce710909b899eeb3641bac248ec943799247534fcd7db7112fcb60ff3e

See more details on using hashes here.

Provenance

The following attestation bundles were made for undetermined-0.1.3.tar.gz:

Publisher: release.yml on Megapixel99/undetermined

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file undetermined-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: undetermined-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 15.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for undetermined-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 0341bd27c0cb552089302864ef4eb2ae342ff90a87ddfdc0f910dc08716e34ef
MD5 2380297846b8bd8a32768a32eb853212
BLAKE2b-256 7f8a6c48012fd7ab6655fbfd94bda9c7a8a5fcd83d00988626031feac003b488

See more details on using hashes here.

Provenance

The following attestation bundles were made for undetermined-0.1.3-py3-none-any.whl:

Publisher: release.yml on Megapixel99/undetermined

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.0

2 files

This release

0.1.3 This release

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

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