millwright 0.2.1

A unified ML framework for Rust — proven Rust crates, assembled into one machine.
Documentation
[package]
name = "millwright"
version = "0.2.1"
edition = "2021"
rust-version = "1.95"
description = "A unified ML framework for Rust — proven Rust crates, assembled into one machine."
license = "MIT"
homepage = "https://millwright-rs.dev/"
repository = "https://github.com/mi7plus/millwright"
readme = "README.md"
keywords = ["machine-learning", "ml", "pipeline", "sklearn", "smartcore"]
categories = ["science", "algorithms"]

[package.metadata.docs.rs]
# Document the whole Rust-facing surface (`python` is a cdylib built by maturin,
# and needs a Python interpreter, so it is excluded from the docs.rs build).
features = ["full"]

[lib]
# `cdylib` for the Python extension module (maturin); `rlib` for Rust consumers.
crate-type = ["cdylib", "rlib"]

# ---------------------------------------------------------------------------
# Pinning policy (Phase 8 · HARDEN → 1.0)
#
# The *engines* — the young, single-author ecosystem crates plus the smartcore
# and linfa families this framework assembles — are pinned to an EXACT version
# (`=x.y.z`). They are the framework's real maturity risk; an exact pin means a
# stray `cargo update` can never silently move a fragile engine underneath the
# stable trait contract. Bumps are deliberate, one line, one commit.
#
# General-purpose infrastructure (serde, tokio, axum, pyo3, ndarray, plotters,
# tract) stays on caret ranges: `=` pins on widely-shared crates force hard
# version conflicts in downstream graphs. The committed Cargo.lock reproduces
# their exact resolved versions for our own builds and CI.
# ---------------------------------------------------------------------------
[dependencies]
# --- engines: exact pins ---
smartcore = { version = "=0.6.10", optional = true }
imbalance-rs = { version = "=0.5.0", optional = true }
model-selection-rs = { version = "=0.1.0", optional = true, features = ["smartcore-metrics"] }
linfa = { version = "=0.8.1", optional = true }
linfa-clustering = { version = "=0.8.1", optional = true }
linfa-reduction = { version = "=0.8.1", optional = true }
linfa-nn = { version = "=0.8.1", optional = true }
hyperopt-rs = { version = "=0.1.1", optional = true }
regression-diagnostics = { version = "=0.2.0", optional = true }
shap-rs = { version = "=0.1.0", optional = true }
plotters-statistical = { version = "=0.2.0", optional = true }
onnx-export-rs = { version = "=0.1.1", optional = true, features = ["smartcore-compat"] }
driftwatch = { version = "=0.1.0", optional = true }
chronos-ts = { version = "=0.1.0", optional = true }
incremental-rs = { version = "=0.1.2", optional = true }
polars = { version = "=0.55.2", default-features = false, features = ["csv", "parquet", "dtype-categorical", "dtype-datetime", "temporal", "is_in"], optional = true }

# --- general infrastructure: caret ranges, reproduced via Cargo.lock ---
pyo3 = { version = "0.24", features = ["extension-module", "abi3-py39"], optional = true }
ndarray = { version = "0.16", optional = true }
rayon = { version = "1", optional = true }
# Decode a saved ONNX file back into the proto, to natively evaluate ONNX-ML ops
# (tree ensembles) that tract does not implement. Same version onnx-export-rs
# uses, so the generated proto types decode cleanly.
prost = { version = "0.13", optional = true }
plotters = { version = "0.3.7", default-features = false, features = ["svg_backend", "all_series"], optional = true }
tract-onnx = { version = "0.23", optional = true }
axum = { version = "0.8", optional = true }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"], optional = true }
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
# chronos-ts and incremental-rs pin ndarray 0.15, distinct from the 0.16 the
# rest of the stack uses — the "two ndarray worlds" the design settles. Renamed
# so both versions coexist; converted only at these adapters' edges.
ndarray015 = { package = "ndarray", version = "0.15", optional = true }

[dev-dependencies]
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
tower = { version = "0.5", features = ["util"] }
http-body-util = "0.1"
criterion = "0.5"

[[bench]]
name = "throughput"
harness = false
required-features = ["smartcore-backend"]

[features]
# Phase 0 · THE SPINE — the smartcore backend is the first (and default) engine.
# Phase 1 · PREP & SELECT — preprocessing, model-selection, and ensembles.
default = ["smartcore-backend", "preprocessing", "model-selection", "ensemble"]

smartcore-backend = ["dep:smartcore"]

# SMOTE and friends come from imbalance-rs as train-time `Balancer`s. Core
# transformers (impute / scale / encode) have no extra deps and are always on.
preprocessing = ["dep:imbalance-rs", "dep:ndarray"]

# Cross-validation splitters, scoring, and grid/random search over a pipeline.
# CV folds and search candidates are evaluated in parallel via rayon.
model-selection = ["dep:model-selection-rs", "dep:ndarray", "dep:rayon"]

# Voting / bagging / stacking meta-estimators, composed over the four traits.
# Leak-free stacking uses the model-selection CV engine when it is enabled;
# bagging fits its base estimators in parallel via rayon.
ensemble = ["dep:rayon"]

# Ingest & EDA — the front of the lifecycle. A polars-backed, dtype-aware
# `Table` (CSV/Parquet in, real string/categorical/datetime/null columns) that
# lowers to the numeric `Frame`, plus a `Profile` that reports the data and
# drafts a starting preprocessing pipeline.
eda = ["dep:polars"]

# Phase 2 · BACKENDS & HPO.
# The second backend: linfa clustering (k-means, GMM, DBSCAN) and PCA, proving
# the Frame <-> ndarray boundary conversion for a whole other engine.
linfa-backend = [
    "dep:linfa",
    "dep:linfa-clustering",
    "dep:linfa-reduction",
    "dep:linfa-nn",
    "dep:ndarray",
]

# Bayesian / TPE hyperparameter search via hyperopt-rs, behind the same search
# API as grid/random search (needs the model-selection CV engine).
hpo = ["dep:hyperopt-rs", "model-selection"]

# Phase 3 · INSIGHT — trust the model, not just run it.
# OLS regression diagnostics (VIF, residual tests, influence, summary()).
diagnostics = ["dep:regression-diagnostics", "dep:ndarray"]

# SHAP values and permutation importance via shap-rs.
explain = ["dep:shap-rs"]

# Probability calibration (Platt / isotonic) + reliability curves — turn raw
# classifier scores into calibrated probabilities. Core, no extra deps.
calibration = []

# Unsupervised outlier detection: Mahalanobis distance and a kNN distance score.
# Core, no extra deps.
anomaly = []

# Report figures (ROC, calibration, residual) via plotters-statistical.
viz = ["dep:plotters-statistical", "dep:plotters"]

# Phase 4 · PORTABILITY.
# Export trained models/pipelines to ONNX (onnx-export-rs) and load & run any
# ONNX file with a tract-backed InferenceModel.
onnx = [
    "dep:onnx-export-rs",
    "onnx-export-rs/smartcore-compat",
    "dep:tract-onnx",
    "dep:ndarray",
    "dep:prost",
    "smartcore-backend",
    "smartcore/serde",
]

# Phase 4 · PYTHON — pyo3 bindings (`pip install millwright`), built by maturin.
# Deliberately NOT part of `full`: pyo3's `extension-module` defers libpython
# symbols, so a plain `cargo test`/`clippy` cannot link a test binary with it.
# Build & test this feature through maturin instead (see pyproject.toml).
python = ["dep:pyo3", "smartcore-backend", "model-selection", "explain", "onnx", "eda"]

# Phase 5 · OPERATIONS — past where scikit-learn stops.
# A versioned model registry: content-addressed ONNX artifact + metadata +
# reference distribution, with movable tags and rollback.
registry = ["dep:serde", "dep:serde_json", "onnx"]

# PSI prediction-drift monitoring via driftwatch.
monitor = ["dep:driftwatch"]

# An axum HTTP inference server (/predict + validation, /metrics drift), backed
# by the tract InferenceModel and an optional drift monitor.
serve = ["dep:axum", "dep:tokio", "dep:serde", "dep:serde_json", "onnx"]

# Every Rust-facing feature (everything except the Python extension).
full = [
    "smartcore-backend",
    "preprocessing",
    "model-selection",
    "ensemble",
    "eda",
    "linfa-backend",
    "hpo",
    "diagnostics",
    "explain",
    "calibration",
    "anomaly",
    "viz",
    "onnx",
    "registry",
    "monitor",
    "serve",
    "timeseries",
    "incremental",
    "automl",
]
# Phase 6 · SPECIALIZED — same contract, different data shapes.
# chronos-ts ARIMA / auto-ARIMA forecasting behind a Forecaster contract.
timeseries = ["dep:chronos-ts", "dep:ndarray015"]

# incremental-rs out-of-core partial_fit estimators behind a PartialFit contract.
incremental = ["dep:incremental-rs", "dep:ndarray015"]

# Phase 7 · SYNTHESIS — AutoML, the framework pointed at itself.
# No new crate: it orchestrates model-selection, the ensemble core, and the
# smartcore backend into a budgeted preprocessing × model × HPO search.
automl = ["model-selection", "ensemble", "smartcore-backend"]

[[example]]
name = "spine"
required-features = ["smartcore-backend"]

[[example]]
name = "explore"
required-features = ["eda", "smartcore-backend"]

[[example]]
name = "trust"
required-features = ["calibration", "anomaly"]

[[example]]
name = "workflow"
required-features = ["smartcore-backend", "preprocessing", "model-selection", "ensemble"]

[[example]]
name = "backends"
required-features = ["smartcore-backend", "linfa-backend", "hpo"]

[[example]]
name = "insight"
required-features = ["smartcore-backend", "diagnostics", "explain", "viz"]

[[example]]
name = "portability"
required-features = ["smartcore-backend", "onnx"]

[[example]]
name = "operations"
required-features = ["smartcore-backend", "onnx", "registry", "monitor", "serve"]

[[example]]
name = "specialized"
required-features = ["timeseries", "incremental"]

[[example]]
name = "automl"
required-features = ["smartcore-backend", "automl", "onnx"]