Contributing to TuiML

TuiML is open source and designed to be extensible. Whether you want to add an algorithm, fix a bug, or improve the documentation, this guide covers everything you need.

[01]
New algorithms

Implement a classifier, regressor, or clusterer using the base classes, add tests, and submit a PR.

[02]
Bug reports

Found an issue? Open a GitHub issue with a reproducible example and we will investigate.

[03]
Documentation

Improve docstrings, tutorials, or examples. Every contribution counts toward a better library.

Design philosophy


Native implementations first

TuiML ships its own implementations, with C++ hot paths for performance-critical code, rather than wrapping other libraries. That is what makes it a library rather than a facade, and it is the single most important thing to understand before you write code.

Bridges live in their own packages

Bridges to external libraries are welcome, but they live in dedicated packages, tuiml.sklearn, tuiml.capymoa and tuiml.weka, as optional extras, never inside the core namespaces.

[!]Wrapper files never go in algorithms/, preprocessing/ or features/. See External libraries for the full rules.
[!]algorithms/gradient_boosting/ is the one exception, and a historical one: the XGBoost, LightGBM and CatBoost wrappers predate this rule and keep their bare names for compatibility. Do not treat it as a precedent.
Optional dependencies are lazy at three levels

Anything that needs a library outside the core install — PyTorch, a boosting backend, a foundation model — has to stay invisible until it is actually used. The rule is not "wrap the import in a try": it is that the algorithm catalog must be byte-identical on every install, so an agent browsing the hub sees the same thing whatever the user happens to have.

[1]Import time. No module-scope import torch or import xgboost, anywhere. Classes are still defined, exported and registered without the library, so list_algorithms() is unchanged. An AST test fails the build if you add one.
[2]Construction time. YourModel() must succeed without the library — it only records hyperparameters. Checking availability in __init__ breaks parameter grids, pickling and the generic contract suite.
[3]Fit time. The first line of fit calls the shared helper, which raises an ImportError naming your class and the exact install command.

Use the existing helpers rather than writing your own check: tuiml.utils.torch_backend.require_torch for PyTorch, tuiml.algorithms.gradient_boosting._backend.require_backend for a boosting library.

from tuiml.utils.torch_backend import require_torch class YourNeuralModel(Classifier): def fit(self, X, y): torch, nn = require_torch(type(self).__name__) # first line ...

Also declare the extra in pyproject.toml, add it to the installer prompts in website/static/install.sh and install.ps1, and gate your tests with pytest.mark.skipif so they skip cleanly rather than erroring on a core-only install.

Development setup


Clone the repository and set up your local environment.

# Clone and install git clone https://github.com/tuiml/tuiml.git cd tuiml uv sync # Run the test suite uv run pytest # Run with coverage uv run pytest --cov=tuiml

A plain uv sync gives you the core library only. Tests that need an optional backend skip rather than fail, so a green run does not by itself mean you covered your change — install what your work touches:

# Everything, including PyTorch and the foundation model uv sync --all-extras # Or just what you need uv sync --extra torch # the neural models uv sync --extra foundation # TabICL uv sync --extra sklearn

If you changed something in algorithms/tabular_deep/ or algorithms/timeseries/deep/, check the skip count before you open the PR — uv run pytest -rs lists exactly what was skipped and why.

[!]Always use uv run to execute Python scripts and commands. Never use python3 directly.

Base classes


Every algorithm inherits from the Algorithm base class. Pick the subclass that matches what your algorithm predicts.

Base class Use it for Import
Classifier Predicting categorical labels from tuiml.base.algorithms import Classifier
Regressor Predicting continuous values from tuiml.base.algorithms import Regressor
Clusterer Unsupervised grouping from tuiml.base.algorithms import Clusterer
Associator Association rule mining from tuiml.base.algorithms import Associator
Required methods

Every algorithm must implement these abstract methods:

@abstractmethod def fit(self, X: np.ndarray, y: Optional[np.ndarray] = None) -> "Algorithm": """Train the algorithm on data.""" @abstractmethod def predict(self, X: np.ndarray) -> np.ndarray: """Make predictions on new data."""

Algorithm structure


A complete nearest-centroid classifier, start to finish.

"""My Custom Classifier implementation.""" import numpy as np from typing import Dict, List, Any, Optional from tuiml.base.algorithms import Classifier, classifier @classifier(tags=["custom", "example"], version="1.0.0") class MyCustomClassifier(Classifier): """A custom classifier for demonstration purposes. This is a simple nearest-centroid classifier that assigns each sample to the class of the nearest cluster centroid. Parameters ---------- metric : str, default="euclidean" Distance metric to use for computing distances to centroids. Options: "euclidean", "manhattan" Attributes ---------- classes_ : np.ndarray Unique class labels. centroids_ : np.ndarray Centroid for each class, shape (n_classes, n_features). """ def __init__(self, metric: str = "euclidean"): super().__init__() self.metric = metric self.classes_ = None self.centroids_ = None @classmethod def get_parameter_schema(cls) -> Dict[str, Dict[str, Any]]: """Return JSON Schema for constructor parameters.""" return { "metric": { "type": "string", "default": "euclidean", "enum": ["euclidean", "manhattan"], "description": "Distance metric for centroid computation" } } @classmethod def get_capabilities(cls) -> List[str]: """Return supported data capabilities.""" return ["numeric", "binary_class", "multiclass"] def fit(self, X: np.ndarray, y: np.ndarray) -> "MyCustomClassifier": """Fit the classifier to training data.""" X = np.asarray(X, dtype=float) y = np.asarray(y) if X.ndim == 1: X = X.reshape(-1, 1) self.classes_ = np.unique(y) self.centroids_ = np.array([ X[y == c].mean(axis=0) for c in self.classes_ ]) self._is_fitted = True return self def predict(self, X: np.ndarray) -> np.ndarray: """Predict class labels for samples.""" self._check_is_fitted() X = np.asarray(X, dtype=float) if X.ndim == 1: X = X.reshape(-1, 1) distances = np.sqrt( np.sum((X[:, np.newaxis, :] - self.centroids_) ** 2, axis=2) ) return self.classes_[np.argmin(distances, axis=1)]
Key components
[01]Decorator: @classifier registers your algorithm with the hub and carries its metadata, tags and version.
[02]NumPy docstring: required, not optional. It powers the generated docs and the LLM tool integration.
[03]__init__ and super(): always call super().__init__(), then set every attribute that will be read later.
[04]Metadata methods: implement get_parameter_schema(), get_capabilities() and get_complexity() for hub integration.

Step by step


Five steps from an empty file to a pull request.

Step 1: Choose the right location

Place your algorithm in the matching subdirectory under tuiml/algorithms/:

[+]anomaly/: outlier and novelty detection
[+]associations/: frequent itemsets and rules
[+]bayesian/: probabilistic models
[+]causal/: uplift and treatment effects
[+]clustering/: clustering algorithms
[+]ensemble/: meta-learners
[+]glassbox/: interpretable-by-design models
[+]gradient_boosting/: boosted ensembles
[+]linear/: linear models
[+]neighbors/: instance-based learning
[+]neural/: neural networks
[+]rules/: rule-based baselines
[+]survival/: time-to-event with censoring
[+]svm/: maximum-margin methods
[+]tabular_deep/: deep tabular architectures (FT-Transformer, SAINT, NODE)
[+]timeseries/: forecasting and series classification
[+]trees/: decision trees and forests
Step 2: Create your algorithm file

Lowercase with underscores, descriptive, no abbreviations unless they are standard: gradient_boosting_classifier.py, but svm.py.

Step 3: Implement the algorithm

Follow the structure above. Your checklist:

[x]Inherit from the correct base class
[x]Use the matching decorator, @classifier, @regressor and so on
[x]Implement fit() and predict()
[x]Set self._is_fitted = True at the end of fit()
[x]Implement the metadata methods for hub integration
Step 4: Export from __init__.py

Update both the category and the main __init__.py so your class is importable:

# tuiml/algorithms/your_category/__init__.py from .my_algorithm import MyCustomClassifier __all__ = [ # ... existing exports ... "MyCustomClassifier", ] # tuiml/algorithms/__init__.py from tuiml.algorithms.your_category import MyCustomClassifier
Step 5: Write tests and submit

Add a test file under tests/, confirm the whole suite passes with uv run pytest, then open a pull request.

Algorithm registration


The @classifier, @regressor, @clusterer and @associator decorators handle registration with the TuiML hub automatically.

Parameter Type Description
tags List[str] Categorization tags, for example "trees", "ensemble"
version str Semantic version string, for example "1.0.0"
author str Optional author name or organization
# Examples of the different decorators from tuiml.base.algorithms import ( classifier, regressor, clusterer, associator ) @classifier(tags=["trees", "ensemble"], version="2.0.0") class RandomForestClassifier(Classifier): """Random Forest classifier.""" @regressor(tags=["linear"], version="1.0.0") class RidgeRegressor(Regressor): """Ridge regression with L2 regularization.""" @clusterer(tags=["partition", "centroid"], version="1.5.0") class KMeansClusterer(Clusterer): """K-Means clustering algorithm."""

External libraries


TuiML has bridges to scikit-learn and CapyMOA, and we welcome contributions that extend them. But where that code lives is not a matter of taste, it is the rule that keeps the core library coherent.

[!]
The one rule

Never put wrapper files inside the core namespaces. A file named sklearn_rf.py does not belong in tuiml/algorithms/trees/. The core algorithms/, preprocessing/ and features/ packages contain native TuiML implementations only. Wrappers go in tuiml/sklearn/, tuiml/capymoa/ or tuiml/weka/. Mixing them makes the library look like a thin shell over scikit-learn and leaves users unable to tell what is actually ours.

Where does my contribution go?
What you are adding Where it goes Dependency
An algorithm implemented from scratch tuiml/algorithms/<family>/ core, no new deps
A scikit-learn estimator exposed to TuiML tuiml/sklearn/ tuiml[sklearn] extra
A CapyMOA streaming learner tuiml/capymoa/ tuiml[capymoa] extra, needs Java
A Weka learner tuiml/weka/ tuiml[weka] extra, needs Java
A pretrained foundation model (weights downloaded, not shipped) tuiml/foundation/ tuiml[foundation] extra, hub key foundation.<Name>
A neural architecture TuiML trains itself algorithms/tabular_deep/ or timeseries/deep/ tuiml[torch] extra, bare name
A bridge to some other library (JAX, cuML) a new tuiml/<library>/ package its own extra

Name the package after the library it wraps (sklearn, capymoa, weka), not after the capability it provides (streaming). Capability names are reserved for native implementations, so tuiml/algorithms/streaming/ stays free for streaming algorithms we write ourselves.

Why wrappers never collide with native algorithms

Each bridge registers into the hub under a namespaced key, sklearn.<ClassName>, capymoa.<ClassName> or weka.<ClassName>. That is what lets a scikit-learn RandomForestClassifier and a native TuiML RandomForestClassifier coexist, and it lets users choose between them explicitly:

from tuiml.algorithms import RandomForestClassifier # native TuiML from tuiml.sklearn import RandomForestClassifier as SkRF # sklearn-backed # ...or by hub key, which is what agents and the CLI use: tuiml.train("RandomForestClassifier", data) # native tuiml.train("sklearn.RandomForestClassifier", data) # sklearn # Any scikit-learn estimator is auto-wrapped, no curated class needed: from sklearn.svm import SVC tuiml.train(SVC(), data)
Adding a curated wrapper

A wrapper declares its constructor parameters and builds the backing estimator lazily. _build_estimator() is the only place the external library is ever imported, that is what keeps the dependency optional:

# tuiml/sklearn/algorithms.py from tuiml.base.algorithms import Classifier from tuiml.sklearn._base import _SklearnBackedMixin, sk_classifier @sk_classifier(tags=["ensemble", "tree"]) class RandomForestClassifier(_SklearnBackedMixin, Classifier): """scikit-learn RandomForestClassifier (hub key ``sklearn.RandomForestClassifier``).""" def __init__(self, n_estimators: int = 100, random_state: int | None = None): super().__init__() self.n_estimators = n_estimators self.random_state = random_state def _build_estimator(self): # The ONLY import of sklearn, lazy, so `import tuiml` never needs it. from sklearn.ensemble import RandomForestClassifier as _Est return _Est(n_estimators=self.n_estimators, random_state=self.random_state) @classmethod def get_parameter_schema(cls) -> dict: return {"n_estimators": {"type": "integer", "default": 100}} @classmethod def get_capabilities(cls) -> list: return ["numeric", "multiclass", "probabilities"]

You never write the fit / predict bridge yourself, the mixin does it, and it raises a clear ImportError pointing at pip install 'tuiml[sklearn]' if the library is missing. CapyMOA works the same way via tuiml/capymoa/_base.py, adapting its instance-at-a-time API to TuiML's batch interface.

Do
[x]Keep every wrapper inside tuiml/sklearn/, tuiml/capymoa/ or tuiml/weka/
[x]Import the external library lazily, inside _build_estimator()
[x]Register the dependency as an extra in pyproject.toml
[x]Prefer a native implementation when the algorithm is core to the library
Don't
[!]Add sklearn_*.py files to algorithms/, preprocessing/ or features/
[!]Import sklearn, capymoa or weka at module top level
[!]Make a native algorithm depend on an external library
[!]Use sklearn for splitting or metrics, TuiML has evaluation.splitting and evaluation.metrics

Testing your algorithm


Tests are what let the next contributor change code without breaking yours. TuiML uses pytest.

# tests/algorithms/your_category/test_my_algorithm.py import numpy as np import pytest from tuiml.algorithms.your_category import MyCustomClassifier class TestMyCustomClassifier: """Test suite for MyCustomClassifier.""" def test_initialization(self): """Test that classifier initializes correctly.""" clf = MyCustomClassifier(metric="euclidean") assert clf.metric == "euclidean" assert not clf._is_fitted def test_fit(self): """Test fitting on simple data.""" X = np.array([[1, 2], [2, 3], [3, 4], [6, 7], [7, 8]]) y = np.array([0, 0, 0, 1, 1]) clf = MyCustomClassifier() clf.fit(X, y) assert clf._is_fitted assert hasattr(clf, 'classes_') assert len(clf.classes_) == 2 def test_predict(self): """Test predictions.""" X_train = np.array([[1, 2], [2, 3], [3, 4], [6, 7], [7, 8]]) y_train = np.array([0, 0, 0, 1, 1]) X_test = np.array([[1.5, 2.5], [6.5, 7.5]]) clf = MyCustomClassifier() clf.fit(X_train, y_train) predictions = clf.predict(X_test) assert len(predictions) == 2 assert predictions[0] == 0 assert predictions[1] == 1 def test_parameter_schema(self): """Test that parameter schema is returned.""" schema = MyCustomClassifier.get_parameter_schema() assert "metric" in schema
Running tests
# Run all tests uv run pytest # Run tests for a specific algorithm uv run pytest tests/algorithms/your_category/test_my_algorithm.py -v # Run with a coverage report uv run pytest --cov=tuiml

Documentation convention


TuiML uses NumPy/SciPy docstring style. The gold standard reference is algorithms/anomaly/isolation_forest.py.

[!]Never use Google style (Args: or Parameters:), the doc generator will not parse it. Section headers must use dash underlines.
Algorithm docstring sections

In this order, for every class under algorithms/:

1. Summary # one line, **bold** key concepts 2. Overview # numbered steps 3. Theory # RST math notation 4. Parameters # constructor args 5. Attributes # fitted attributes, trailing _ 6. Notes # complexity + when to use 7. References # [AuthorYear] with DOI 8. See Also # :class:`~tuiml...` cross-references 9. Examples # >>> doctest format, imports included

For datasets/ and features/, keep it short: Summary, Parameters and Returns only. Every Examples block must open with the import that defines the names it uses, a reader copies the block straight out of the rendered docs and runs it.

Math notation

Use RST math, it renders through KaTeX. Inline: :math:`x^2`. Display block: .. math::

LaTeX backslash rules

Python reads \t \a \b \f \n \r \v as escape sequences inside docstrings, so LaTeX commands starting with those letters get mangled. Double the backslash:

[!]Must double-escape: \\text \\alpha \\beta \\frac \\begin \\nabla \\right \\varepsilon
[x]Safe single-backslash: \sigma \sum \sqrt \lambda \log \pi \mu \infty
Quick reference
[+]Run uv run scripts/generate_docs.py after docstring changes to regenerate the HTML
[+]See Also entries look like :class:`~tuiml.algorithms.svm.SVR` : Description. — the target class must actually exist
[+]References use the [AuthorYear] format with a DOI link
[+]Timeseries models use the Regressor base class, not Classifier
[+]Fitted attributes carry a trailing _, the scikit-learn convention

Best practices


Do
[x]Handle missing values (NaN) gracefully
[x]Validate input shapes and types
[x]Write complete NumPy docstrings
[x]Include a runnable usage example in the class docstring
[x]Set self._is_fitted in fit()
[x]Use type hints on every method
[x]Follow PEP 8
Don't
[!]Modify input arrays in place
[!]Rely on global state
[!]Print to stdout, use logging
[!]Skip parameter validation
[!]Return unexpected array shapes
[!]Forget to call super().__init__()
[!]Ignore edge cases such as empty data

Submitting your contribution


Once your algorithm is complete and tested, send it in.

[01]Fork and open a pull request: fork the repository, add your algorithm on a new branch, and describe it clearly in the PR.
[02]Include documentation: complete docstrings, with at least one runnable usage example in the class docstring.
[03]Ensure tests pass: the whole suite, not just yours. Run uv run pytest before submitting.

Need help? Open an issue on GitHub, we are happy to help you get your algorithm integrated.

Fork on GitHub Read the docs → See the build board →