Metadata-Version: 2.4
Name: modus-py
Version: 0.1.0a3
Summary: Schema-driven, unit-aware Polars transform engine.
License: BSD-3-Clause
License-File: LICENSE
Requires-Python: >=3.12
Requires-Dist: astropy<8.0.0,>=7.0.0
Requires-Dist: polars<2.0.0,>=1.0.0
Requires-Dist: pyarrow
Requires-Dist: pydantic<3.0.0,>=2.0.0
Requires-Dist: tzdata
Provides-Extra: dev
Requires-Dist: docstring-to-markdown==0.17; extra == 'dev'
Requires-Dist: importlib-metadata==9.0.0; extra == 'dev'
Requires-Dist: mypy-extensions==1.1.0; extra == 'dev'
Requires-Dist: pandas; extra == 'dev'
Requires-Dist: pathspec==1.1.1; extra == 'dev'
Requires-Dist: platformdirs==4.10.1; extra == 'dev'
Requires-Dist: playwright>=1.61; extra == 'dev'
Requires-Dist: pyright>=1.1; extra == 'dev'
Requires-Dist: pytest-asyncio; extra == 'dev'
Requires-Dist: pytest-benchmark; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest-mock; extra == 'dev'
Requires-Dist: pytest-ruff; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: python-lsp-jsonrpc==1.1.2; extra == 'dev'
Requires-Dist: python-lsp-server==1.14.0; extra == 'dev'
Requires-Dist: pytokens==0.4.1; extra == 'dev'
Requires-Dist: ruff>=0.15.0; extra == 'dev'
Requires-Dist: ujson==5.13.0; extra == 'dev'
Requires-Dist: zipp==4.1.0; extra == 'dev'
Provides-Extra: gui
Requires-Dist: altair>=5.0; extra == 'gui'
Requires-Dist: anywidget>=0.9; extra == 'gui'
Requires-Dist: nicegui>=2.0; extra == 'gui'
Requires-Dist: pyyaml>=6.0; extra == 'gui'
Requires-Dist: vegafusion[embed]; extra == 'gui'
Requires-Dist: vl-convert-python>=1.0; extra == 'gui'
Description-Content-Type: text/markdown

# modus

Schema-driven, unit-aware Polars transform engine.

modus pairs `ModusFrame` — a Polars DataFrame with per-column Astropy unit
metadata — with `FrameTransform`, a builder-pattern engine for expressing
timeseries transform pipelines (grouping, cleaning, deriving, filtering, and
aggregating) declaratively, while keeping unit metadata accurate throughout.

## Installation

```
pip install modus-py
```

## Quick start

```python
import astropy.units as u
import polars
import modus

frame = modus.ModusFrame(
    data=polars.DataFrame({"pressure_kpa": [101.3, 98.7]}),
    units={"pressure_kpa": u.kPa},
)

velocity = frame.col("distance") / frame.col("time")  # UnitExpr; unit propagates
frame = frame.with_columns(velocity=velocity)
```

```python
import modus
from modus.transform.ops import aggregations, cleaners, groupers, suppressors

result = (
    modus.FrameTransform()
    .add_grouper(groupers.DataGap("chunk", gap_seconds=60))
    .add_grouper(groupers.EventSequence("body_up_event", input_column="sts", value=1, within="chunk"))
    .add_cleaner(cleaners.Interpolate("pressure_kpa_kpa", within="chunk"))
    .add_suppressor(suppressors.SuppressLeadingEvent("body_up_event", within="chunk"))
    .add_suppressor(suppressors.SuppressTrailingEvent("body_up_event", within="chunk"))
    .group_by("chunk", "body_up_event")
    .add_aggregation(aggregations.Mean("pressure_kpa_kpa", output_name="mean_pressure_kpa_kpa"))
    .add_aggregation(aggregations.Duration(output_name="body_up_duration_s"))
    .apply(frame)
)

labelled = result.labelled      # ModusFrame -- timeseries with grouper columns added
aggregated = result.aggregated  # ModusFrame -- one row per (chunk, body_up_event) group
```

## Documentation

See [docs/](docs/) for the full reference: `ModusFrame`/`UnitExpr`, the unit
registry, `FrameTransform`'s execution model and pre-flight validation, each
operation family (groupers, cleaners, derivations, suppressors, group masks,
event views, aggregations), the declarative `TransformSpec` schema, and JSON
serialisation.

## Custom units

modus ships an extensible unit registry, in the same spirit as Astropy's own
enabled-units model:

```python
import astropy.units as u
import modus

modus.units.registry.register("kgcm2", u.def_unit("kgcm2", 98.0665 * u.kPa))
```

## Custom derivations and aggregations

Both extension points are plugin registries keyed by name, so a schema or
manifest layer built on top of modus can construct bespoke operations from
declarative configuration without importing analytic-specific Python:

```python
import modus

@modus.transform.DerivationRegistry.register("state_inference")
class StateInferenceDerivation(modus.transform.Derivation):
    ...

@modus.transform.AggregationRegistry.register("weighted_percentile")
class WeightedPercentile(modus.transform.Aggregation):
    ...
```

## Development

```
pip install -e .[dev]
pytest
```

## Licence

BSD-3-Clause — see [LICENSE](LICENSE).
