Architecture¶
TSL is a Cargo workspace plus two Python packages:
| Component | Path | What it is |
|---|---|---|
tsl_rust (lib tsl) |
src/ |
the Rust core — the model and fitting |
tsl-py |
tsl-py/ |
PyO3/maturin wrapper + scikit-learn API |
tslviz |
tsl-split-evolution-dashboard/ |
FastAPI + D3 app to replay a fit |
The model is a three-level hierarchy, and the src/ module tree mirrors it exactly.
Read these three files first: src/grid_tensor.rs, src/stage_predictor.rs,
src/forest.rs.
TSL (forest) Vec<StagePredictor> src/forest.rs
└── StagePredictor bag of GridTensors + OLS src/stage_predictor.rs
└── GridTensor one separable component src/grid_tensor.rs
GridTensor(page) — one fitted separable component, stored in two-tensor form (backbone_values,tilt_values,lambda_plus,lambda_minus).StagePredictor(page) — one boosting stage: a bag ofn_treesGridTensors aggregated into oneprimary_grid_tensor, plus OLSscaling_plus/scaling_minus.TSL(page) — the boosted model, aVec<StagePredictor>;predictsums stage predictions.
src/lib.rs re-exports the four modules (stage_predictor, forest, grid_tensor,
logging) and defines FitResult { err, residuals, y_hat }, the common training-result
struct.
Data flow of a fit¶
fit_boosted (src/forest/fitter.rs) drives the whole thing:
- Initialize residuals \(R = y\).
- For each epoch: fit a
StagePredictor(fit_ensemble) on the current residuals. Internally this fitsn_treesGridTensors (in parallel underuse-rayon) via the grid-refinement loop, then aggregates them. - Backfit: refit
scaling_plus/scaling_minusfor all stages so far by incremental OLS over the per-stage \([\tilde{m}_+, -\tilde{m}_-]\) columns. - Update residuals;
n_iterdecays bydecayafter the first epoch.
See Fitting for the math.
Two critical invariants¶
These two rules are load-bearing throughout the codebase — get them wrong and predictions double-count scale or lose the sign structure.
-
Scaling is applied exactly once, at the
StagePredictorlevel, viascaling_plus/scaling_minusfrom the OLS backfit.GridTensor::predict_unscaledandextract_two_tensor_predictions_unscaleddeliberately return unscaled \(\tilde{m}_+\)/\(\tilde{m}_-\); the legacyGridTensor::scalingfield is ignored in two-tensor mode (set to1.0). Do not multiply by it. -
Positivity: \(\lambda_+,\lambda_-\ge 0\) and \(b\ge 0\), so \(\tilde{m}_+,\tilde{m}_-\ge 0\). This removes the sign ambiguity of unconstrained tensor decompositions; signed effects come only from the \(\tilde{m}_+ - \tilde{m}_-\) difference. Enforced by clamping in the solver (
solve_two_tensor); checked intests/forest.rsandtests/stage1_positive_only.rs.
The action/reducer pattern¶
Grid-tensor fitting (src/grid_tensor/) is structured as an action/reducer loop over a
mutable FittingState:
- a
SplitStrategy(splitting.rs:Random/Best/TopK) proposes aFittingAction(ApplySplit,ApplyResplit,ApplyMerge,ApplyScaling,Terminate,Composite); fitting_reducer(reducer.rs) applies it, calling theRefinementStrategy(refinement.rs:L2Refinement/HuberRefinement) which solves the per-node \(2\times2\) problem viatwo_tensor_solver.rs.
This separation keeps the fit loop testable and modular. Details on GridTensor.
The builder pattern¶
Hyperparameters use fluent builders throughout, nested to mirror the hierarchy:
TSLBoostedParamsBuilder (src/forest/params.rs) — epochs, decay, seed, visualdb
└── StagePredictorParamsBuilder (src/stage_predictor/params.rs) — n_trees, aggregation, similarity
└── GridTensorParamsBuilder (src/grid_tensor/params.rs) — n_iter, split + refinement params
├── SplitStrategyParamsBuilder
└── RefinementStrategyParamsBuilder
The Python TSL.fit(...) classmethod takes flat hyperparameters and maps them onto these
builders; see Python API and the
Hyperparameters reference.
Feature flags¶
Default features are use-rayon (parallel bag fitting) and evo-logging (SQLite split
logging — see Logging). The core is pure Rust and needs no system libraries
to build. See Getting started.