Metadata-Version: 2.4
Name: vcti-measure
Version: 1.0.1
Summary: Domain-agnostic measurement model — stable identities, canonical units, and one self-describing JSON document per run
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-measure
Project-URL: Changelog, https://github.com/vcollab/vcti-python-measure/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pydantic>=2.7
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: numpy; extra == "test"
Requires-Dist: jsonschema; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# vcti-measure

Domain-agnostic measurement model — stable identities, canonical units, and one self-describing JSON document per run

Declare a quantity by name and the readings it yields. Declare what to
measure it on, and what to read the numbers against. This package obtains
the readings — or validates the ones you already have — and turns them into
one JSON document per run.

It measures whatever can be read: a person's blood pressure, a city's air, a
furnace, a piece of executing code. What is worth measuring, and what can
measure it, belong to the domain rather than to the model, so this package
**declares no measurements, provides no instruments and defines no units**.
It draws nothing either — reports are built from the document, elsewhere.

## Installation

```bash
pip install vcti-measure
```

### In `requirements.txt`

```
vcti-measure>=1.0.1
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-measure>=1.0.1",
]
```

---

## Quick Start

Declare the quantity you want, with the readings it yields and the units
they are recorded in:

```python
from vcti.measure.core import FactorDecl, Measurement, MetricDecl

BLOOD_PRESSURE = Measurement(
    name="blood-pressure",
    summary="Arterial pressure at the upper arm.",
    metrics={
        "systolic": MetricDecl(unit="mmHg"),
        "diastolic": MetricDecl(unit="mmHg"),
    },
    factors={"arm": FactorDecl(values=("left", "right"), required=True)},
)
```

Declare what to measure it on. A manifest is data, at module scope, holding
no presentation — under a namespace nobody else would choose, so your
results never collide with someone else's:

```python
from vcti.measure.core import Axis, Manifest, Study, Target, Unit

MANIFEST = Manifest(
    "example.cardiology",
    [Study(
        "medication-follow-up",
        summary="Follow arterial pressure after a medication change.",
        subjects={"patient-8841": patient_record(8841)},
        axes=[Axis(name="posture", values=("seated", "standing"))],
        measurements=[BLOOD_PRESSURE],
        targets={"blood-pressure.systolic": Target(at_most=120)},
    )],
    units=[Unit(name="mmHg")],
)
```

Run it, saying what the run was taken under:

```python
from vcti.measure.core import ProgressSummary, Registry, run

summary = ProgressSummary(units=MANIFEST.units)
artifact = run(
    MANIFEST,
    Registry([]),  # the instruments this run may use
    conditions={"site": {"clinic": "north", "protocol_revision": "3"}},
    progress=summary,
)
summary.finish()

document = artifact.model_dump_json(indent=2)
```

That document is the whole output: the declaration it ran, the units it
uses, the conditions, and every observation. Where it goes is yours — this
package touches no storage. Rendering it is a separate step, in a separate
package.

**Nothing above measures anything yet.** The registry is empty, so no
instrument provides `blood-pressure`, and every observation comes back
marked *unavailable* carrying the reason — which is the designed behaviour,
not a failure. The artifact is still complete and still valid. Register an
instrument that provides the measurement and the same manifest produces
numbers.

---

## Key API surface

Everything below is importable from `vcti.measure.core`.

| What you are doing | What you use |
|---|---|
| Declaring a quantity | `Measurement`, `MetricDecl`, `FactorDecl` |
| Declaring what to measure it on | `Study`, `Item`, `Axis`, `Derivation`, `Description` |
| Reading numbers against something | `Target` |
| Publishing a set of studies | `Manifest`, `Unit` |
| Providing a measurement | `Instrument`, `Reading`, `Registry`, `Selection` |
| Obtaining readings | `run()`, `ProgressSummary` |
| Validating readings you already hold | `assemble()`, `Observation`, `MetricValue`, `FactorValue` |
| Reading a document back | `load_artifact()`, `validate_artifact()` |
| Working with a document | `Artifact`, `Status`, `json_schema()`, `format_value()` |
| Naming a reading without measuring | `ObservationIdentity`, `MetricIdentity`, `SeriesIdentity` |
| Handling a refusal | `MeasureError` and its three stages |

An observation carries one of four statuses — `measured`, `unavailable`,
`failed` or `skipped` — and the last three carry a reason. A declaration is
refused as a `ManifestError`, a set of observations as an `AssemblyError`,
and an unreadable or invalid document as an `ArtifactError`; all three
descend from `MeasureError`.

The full reference, generated from the source, is published in the unified
VCollab documentation.

---

## What this package is not

It compares nothing. What it supplies is what a comparison needs to be
possible: a stable name for what was measured, a canonical unit, and a
record self-describing enough to be read years later. Which comparison is
worth making — against a target, against something measured beside it, along
an axis, across runs — belongs to whoever is asking.

A few things worth knowing, each explained in the design document:

- **Preparation receives the item handle and the coordinate**, never the
  instrument, and sits outside the measured interval.
- **Subjects** are followed over time; **references** exist only to give a
  subject's number meaning.
- Each measurement is taken separately, with its own preparation and
  release.
- **An attempt that produced nothing still appears**, carrying a status and
  a reason. Nothing silently disappears, because an observation missing from
  the document is indistinguishable from one nobody asked for.
- Metric names, units, targets and the points a study declines are all in
  the declaration, so a manifest can be checked with no instrument installed
  at all.
- **Which instrument answered is provenance, never identity**, so replacing
  a device does not orphan the readings it produced.

---

## Dependencies

`pydantic`. The artifact's contract is defined as pydantic models, so the
JSON Schema published with each format version is generated from the same
definitions the implementation validates against — the two cannot drift.

---

## Documentation

| If you want to… | Read |
|---|---|
| See practical, real-world usage | [docs/patterns.md](docs/patterns.md) |
| Understand the architecture and design decisions | [docs/design.md](docs/design.md) |
| Navigate and understand the source | [docs/source-guide.md](docs/source-guide.md) |
| Declare a measurement or write an instrument | [docs/extending.md](docs/extending.md) |
