Skip to content

Integrations

turboswarm lives in the scientific-Python stack and integrates with the libraries you already use. Each integration is optional and lazily imported, so importing turboswarm never requires these packages — install the extra you need:

pip install turboswarm[scipy]    # SciPy drop-in wrapper
pip install turboswarm[sklearn]  # PSOSearchCV hyperparameter search
pip install turboswarm[optuna]   # PSO as an Optuna sampler
pip install turboswarm[parallel] # joblib-parallel objective evaluation
pip install turboswarm[pandas]   # history / convergence DataFrames
pip install turboswarm[agents]   # LLM agent tool (LangChain / LangGraph)
pip install turboswarm[all]      # everything

The integrations live under turboswarm.integrations and compose as a stack — each one targets a specific job, so you reach for the one that matches your task:

You want to… Use Module
pass array bounds / vectorized objectives NumPy (built in)
replace a scipy.optimize.minimize call with PSO SciPy wrapper integrations.scipy
tune model hyperparameters like GridSearchCV PSOSearchCV integrations.sklearn
drive an Optuna study with a PSO swarm TurboswarmSampler integrations.optuna
speed up an expensive Python objective Joblib / Dask integrations.parallel
analyze/plot the run as tables pandas export integrations.pandas
let an LLM agent call PSO as a tool LangChain tool integrations.agents

They build on each other: NumPy is the substrate; the SciPy and scikit-learn entry points call the same core minimize; parallel accelerates whichever objective you pass; and pandas turns any result into a DataFrame.

NumPy

NumPy is supported out of the box — no extra needed (it is a core dependency). bounds accepts a NumPy array of shape (dim, 2) (or (2,) with dim), and objectives may return NumPy scalars:

import numpy as np
import turboswarm as pso

bounds = np.array([[-5.0, 5.0], [-5.0, 5.0]])          # shape (dim, 2)
r = pso.minimize(lambda x: np.sum(np.asarray(x) ** 2), bounds=bounds, seed=0)

For expensive objectives, the vectorized path evaluates the whole swarm in one NumPy call — the objective receives an (n_particles, dim) array and returns an (n_particles,) array:

r = pso.minimize(lambda X: np.sum(X ** 2, axis=1),     # X is (n_particles, dim)
                 bounds=[(-5, 5)] * 10, vectorized=True, seed=0)

SciPy

A wrapper mirroring scipy.optimize.minimize lets you swap your optimizer for PSO by changing one line. It returns a SciPy OptimizeResult with .x, .fun, .nit, .nfev, .success and .message:

from turboswarm.integrations import scipy as ts_scipy

def rosen(x):
    return float(np.sum(100 * (x[1:] - x[:-1] ** 2) ** 2 + (1 - x[:-1]) ** 2))

res = ts_scipy.minimize(rosen, bounds=[(-5, 5)] * 3, seed=0)
print(res.x, res.fun)        # ≈ [1, 1, 1], close to the Rosenbrock optimum (f = 0)
print(res.nit, res.nfev, res.success, res.message)

Because PSO is population-based rather than local, the contract differs from SciPy in two honest ways:

  • bounds is required — the swarm is initialized inside it.
  • x0 is optional — it is only used to infer the dimension when bounds is a single (min, max) pair; PSO does not start from a single point.

SciPy's options={"maxiter": ...} (and maxfev/maxfun) are honored, and any extra keyword (n_particles, velocity, topology, seed, constraints, …) is forwarded to turboswarm.minimize. It also accepts a scipy.optimize.Bounds object.

scikit-learn

PSOSearchCV is a PSO-driven alternative to GridSearchCV / RandomizedSearchCV: it explores the hyperparameter space with the swarm and exposes the familiar search API (fit, best_params_, best_score_, best_estimator_, cv_results_, predict).

from sklearn.svm import SVC
from turboswarm.integrations.sklearn import PSOSearchCV

search = PSOSearchCV(
    SVC(),
    {
        "C": (1e-2, 1e2),          # (low, high) float  -> continuous range
        "gamma": (1e-4, 1e0),      # continuous range
        "kernel": ["rbf", "poly"], # list               -> categorical choice
        # an (int, int) tuple, e.g. "degree": (2, 5), would be an integer range
    },
    n_particles=20, max_iter=30, cv=5, scoring="accuracy", random_state=0,
)
search.fit(X, y)
print(search.best_params_, search.best_score_)
y_pred = search.predict(X_new)      # delegates to the refit best_estimator_

The search space per hyperparameter is: a (low, high) float tuple for a continuous range, a (low, high) int tuple for an integer range, or a list of categorical choices. It is a scikit-learn estimator (clonable, usable in a Pipeline), and n_jobs is forwarded to the cross-validation.

Optuna

If your workflow already lives in Optuna, use PSO as a drop-in sampler: each trial evaluates one particle, and the swarm updates after every generation. It supports Float/Int (including log=True) and Categorical distributions and both study directions.

import optuna
from turboswarm.integrations.optuna import TurboswarmSampler

def objective(trial):
    x = trial.suggest_float("x", -5, 5)
    lr = trial.suggest_float("lr", 1e-4, 1e0, log=True)
    kernel = trial.suggest_categorical("kernel", ["rbf", "poly"])
    ...
    return score

study = optuna.create_study(
    direction="minimize",
    sampler=TurboswarmSampler(n_particles=20, seed=0),
)
study.optimize(objective, n_trials=200)
print(study.best_params, study.best_value)

This keeps Optuna's study management, storage, dashboards and pruning hooks while searching with the swarm. It is designed for sequential studies (n_jobs=1).

Joblib / Dask

PSO parallelizes its swarm loop in Rust, but when the objective itself is an expensive Python call (a heavy model, a simulation) you can distribute the per-particle evaluations. These helpers wrap a per-particle objective into a vectorized one — use it with vectorized=True:

from turboswarm.integrations import parallel

def expensive(x):          # one particle; slow (e.g. trains a model)
    ...
    return cost

obj = parallel.joblib_objective(expensive, n_jobs=-1)      # all cores
r = pso.minimize(obj, bounds=[(-5, 5)] * 10, vectorized=True, seed=0)

joblib_objective accepts joblib's backend ("loky", "threading", "multiprocessing"). For a cluster, parallel.dask_objective(expensive, client=client) submits the calls to a Dask Client.

pandas

Export an optimization result as tidy DataFrames for analysis and reporting:

from turboswarm.integrations import pandas as ts_pandas

r = pso.minimize("sphere", bounds=[(-5, 5)] * 2, seed=1)

ts_pandas.convergence_dataframe(r)   # columns: iteration, best_value
ts_pandas.history_dataframe(r)       # columns: iteration, particle, x0, x1, ...

# write straight to disk (Parquet needs the [parquet] extra):
ts_pandas.to_csv(r, "history.csv")
ts_pandas.to_csv(r, "convergence.csv", kind="convergence")
ts_pandas.to_parquet(r, "history.parquet")

history_dataframe needs the run to have recorded history (the default, record_history=True); it is one row per (iteration, particle). From there the whole pandas/Matplotlib stack is available for custom analysis.

Agents (LangChain / LangGraph)

PSO is a useful tool for LLM agents: a gradient-free optimizer they can call to solve a numeric subproblem. optimization_tool() returns a LangChain StructuredTool (usable directly in a LangChain agent or as a LangGraph ToolNode) that minimizes a standard benchmark with PSO:

from turboswarm.integrations.agents import optimization_tool

tool = optimization_tool()
tool.invoke({"benchmark": "rastrigin", "dim": 2, "seed": 0})
# {'best_position': [0.0, 0.0], 'best_value': 0.0, 'evaluations': ..., 'stop_reason': 'max_iterations'}

# wire it into an agent (any LLM provider):
#   tools = [optimization_tool()]

It is deliberately safe: the agent picks a named benchmark (sphere, rastrigin, …) and a bounded budget — never arbitrary code. The tool itself calls no LLM, so it is provider-agnostic.

More integrations

A PyTorch example is on the roadmap.