millwright 0.1.0

A unified ML framework for Rust — proven Rust crates, assembled into one machine.
Documentation
[package]

name = "millwright"

version = "0.1.0"

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 }

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.

model-selection = ["dep:model-selection-rs", "dep:ndarray"]



# Voting / bagging / stacking meta-estimators, composed over the four traits.

# Leak-free stacking uses the model-selection CV engine when it is enabled.

ensemble = []



# 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",

    "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"]



# 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"]