Skip to main content

trade-study

CI PyPI DOI License: MIT Python 3.10+ Code style: ruff jcm-sci

Multi-objective trade-study orchestration: define factors, build parameter grids, run hierarchical study phases, and extract Pareto fronts — for any domain where you compare alternatives against competing objectives.

Statement of need

Comparing design alternatives against multiple objectives is a common task across scientific simulation, engineering trade-offs, and ML hyperparameter tuning. Researchers typically glue together separate tools for grid construction, execution, scoring, and Pareto analysis, writing ad-hoc scripts that are hard to reproduce or extend to multi-phase studies (screening → refinement → benchmark).

trade-study provides a single orchestration layer that composes these steps into a reproducible, protocol-driven workflow. Users supply a Simulator (generates data) and a Scorer (evaluates it); the framework handles grid construction, parallel execution, Pareto extraction, and phase chaining. All components are modular and optional — use only what you need.

This package targets researchers and practitioners who need:

  • structured multi-phase experimental design (screen → refine → benchmark),
  • multi-objective Pareto analysis across heterogeneous factors, and
  • a reproducible Python API that separates domain logic from study orchestration.

Why trade-study?

Need Without trade-study With trade-study
Parameter grid Manual itertools.product or one-off scripts build_grid(factors, method="sobol") — full factorial, LHS, Sobol, Halton
Multi-objective ranking Call pymoo directly, handle direction normalization extract_front(scores, directions) — direction-aware
Phased studies Custom loop with manual filtering between stages Study(phases=[Phase(..., filter_fn=top_k_pareto_filter(k=20)), ...])
Adaptive search Set up optuna study from scratch run_adaptive(world, scorer, factors, observables, n_trials=600)
Reproducibility Scattered scripts, no standard protocol Simulator / Scorer protocols + save_results() / load_results()

Existing tools solve pieces of this problem — optuna for adaptive optimization, pymoo for multi-objective solvers, SALib for sensitivity analysis — but none provide the hierarchical phase orchestration that connects them into a single study.

Quick start

This complete example needs NumPy and the pareto extra: pip install 'trade-study[pareto]'. Each replicate uses the same sample bank for both designs, so errors can be compared in pairs.

import numpy as np

from trade_study import (
    Direction, Factor, FactorType, Observable, build_grid,
    extract_front, paired_difference, run_grid,
)


class SampleMean:
    def generate(self, config, *, rep=0):
        rng = np.random.default_rng(np.random.SeedSequence([2026, rep]))
        return 0.0, rng.normal(size=32)[:config["samples"]]


class ErrorAndCost:
    def score(self, truth, observed, config):
        return {"error": float((observed.mean() - truth)**2),
                "cost": float(config["samples"])}


grid = build_grid(
    [Factor("samples", FactorType.DISCRETE, levels=[8, 32])], method="full"
)
observables = [Observable("error", Direction.MINIMIZE),
               Observable("cost", Direction.MINIMIZE)]
results = run_grid(SampleMean(), ErrorAndCost(), grid, observables, n_reps=64)
means = results.aggregate_replicates()
front = extract_front(means.scores, [o.direction for o in observables])
contrast = paired_difference(results, {"samples": 32}, {"samples": 8},
                             "error", seed=2026)
print(means.scores, front, contrast.mean, contrast.n_pairs)

results retains all 128 trial rows; means has one row per design. Bootstrap resamples in the paired interval do not increase the number of independent replicates. Inspect n_pairs: this helper excludes nonfinite pairs. A study must define its failure policy before interpreting a success-only comparison. Positive objective scaling preserves Pareto membership; use decision preferences when choosing among nondominated designs.

The typed standalone version is replicated_study.py. The capabilities and example checks map additional APIs, optional dependencies, and executable examples.

For a complete 15-minute demonstration with live code and saved figures, see the serosurvey design notebook and presentation guide. From a repository checkout, launch it with uv run --extra notebook jupyter lab examples/serosurvey_study.ipynb.

Protocols

Users implement two protocols to plug in their domain:

from trade_study import Scorer, Simulator


class MySimulator:
    """Implements the Simulator protocol."""

    def generate(self, config: dict) -> tuple:
        """Return (truth, observations) for a given config."""
        ...


class MyScorer:
    """Implements the Scorer protocol."""

    def score(self, truth, observations, config: dict) -> dict[str, float]:
        """Return {observable_name: value} for a single trial."""
        ...

Installation

pip install trade-study[all]

Or install only the extras you need:

pip install trade-study[design,pareto]
Extra Packages Purpose
design pyDOE3, SALib, scipy Grid construction and sensitivity screening
pareto pymoo Non-dominated sorting and indicators
scoring scoringrules Proper scoring rules (CRPS, WIS, etc.)
stacking arviz, scipy Bayesian and score-based ensemble weights
adaptive optuna Adaptive multi-objective search (NSGA-II)
parallel joblib Parallel grid execution
viz matplotlib Plot scores and Pareto fronts
surrogate scikit-learn GP/RF score and regime surrogates
dataframe pandas ResultsTable export for analysis and CSV
all All of the above
notebook JupyterLab, nbconvert, matplotlib, pandas, pymoo Execute and present example notebooks

Core dependency: numpy only.

API overview

The snippets below show API composition; names such as world, scorer, run_fn and results stand for caller-supplied objects. Use the complete quickstart above or a worked guide to run a study.

Design

from trade_study import Factor, FactorType, build_grid, screen

factors = [
    Factor("x", FactorType.CONTINUOUS, bounds=(0.0, 1.0)),
    Factor("method", FactorType.CATEGORICAL, levels=["a", "b", "c"]),
]

grid = build_grid(factors, method="sobol", n_samples=1024)
si = screen(run_fn, factors, method="morris", n_trajectories=3000)

Execution

from trade_study import run_grid, run_adaptive

# Grid mode: evaluate all configs (optional parallelism)
results = run_grid(world, scorer, grid, observables, n_jobs=-1)

# Adaptive mode: evolutionary multi-objective optimization (Optuna NSGA-II)
results = run_adaptive(world, scorer, factors, observables, n_trials=600)

Pareto analysis

from trade_study import extract_front, hypervolume, pareto_rank

front_idx = extract_front(results.scores, directions)
ranks = pareto_rank(results.scores, directions)
hv = hypervolume(results.scores[front_idx], ref_point, directions)

Stacking

from trade_study import stack_scores, stack_bayesian, stack_proportional, ensemble_predict

weights = stack_scores(score_matrix)  # select best means; split exact ties
weights = stack_proportional(score_matrix)  # preserve weight across near ties
weights = stack_bayesian(idata_dict)  # arviz stacking (Yao et al. 2018)
combined = ensemble_predict(predictions, weights)

stack_scores solves the supplied linear-score objective directly using NumPy; positive changes of score units preserve its optimum. ensemble_predict normalizes a copy of the weights and accepts read-only arrays and views. See choosing ensemble weights for examples and the distinction between input scores and the score of a combined prediction.

I/O

from trade_study import save_results, load_results

save_results(results, "study_results")
results = load_results("study_results")
frame = results.to_dataframe()  # optional pandas extra; includes trial metadata
frame.to_csv("study_results.csv", index=False)

Multi-fidelity search and surrogates

from trade_study import run_successive_halving, run_hyperband, fit_surrogate, fit_regime_surrogate

# Cheap-first budget-constrained screening
results = run_successive_halving(world, scorer, factors, observables, budget_param="maxiters")
results = run_hyperband(world, scorer, factors, observables, budget_param="maxiters")

# Interpolate scores from a completed results table (GP or RF)
surrogate = fit_surrogate(results, method="gp")

# Recommend a config per regime, interpolating over regime descriptors
regime_surrogate = fit_regime_surrogate(results, regime_cols=["n", "p"])

Surrogate fits report cross-validated cv_r2 and cv_rmse; check those diagnostics before trusting recommendations or post-hoc sensitivity results.

Constraint / feasibility_filter express infeasible regions of the design space; FactorConstraint couples factors during grid construction (e.g. keep x + y <= 1); screen() supports both Morris and Sobol sensitivity analysis. run_grid(..., n_reps=N) supports replicated trials, and sensitivity_from_table() estimates post-hoc sensitivity from an existing results table through a validated surrogate.

Release compatibility

Version 0.5.1 fixes score-unit sensitivity in stack_scores and prevents ensemble_predict from modifying supplied weights, including read-only arrays and views. It also updates executable documentation and illustrated examples.

Version 0.5.0 adds a prespecified fixed replication policy and versioned replication-contract metadata. Adaptive allocation remains the default. Pareto operations now identify the optional pareto extra when it is missing.

Version 0.4.0 adds decision reporting (preference_sweep() with regret, practical equivalence and optional paired uncertainty), fixed-sample proportion intervals and replicate sizing, versioned result bundles with provenance, sequential replication (run_sequential(), with explicit fixed/adaptive policy and recorded counts/uncertainty assumptions), evaluation caching, incremental grid checkpoints with bounded retries, grouped surrogate validation with support diagnostics, and the IVAC serosurvey notebook. The adaptive extra now requires Optuna 4.5 or later, and strict </> adaptive constraints treat exact equality as infeasible, matching result-table feasibility.

Version 0.3.0 adds replicated and paired studies, uncertainty diagnostics, persistent adaptive sessions, checkpointing, and DataFrame export. Existing phase-name-only checkpoints are refused; use a fresh checkpoint directory and set checkpoint_key to track model/data revisions. Confidence constraints require estimated standard errors rather than assuming unknown uncertainty is zero. stack_proportional() now uses exponential score utilities in both directions; set its temperature in score units to control concentration.

Package Description
TradeStudy.jl Inactive design scaffold for a possible Julia port; it does not contain a usable implementation

Development

See CONTRIBUTING.md for setup, code standards, and the PR workflow.

uv sync --extra dev
just ci          # lint → mypy --strict → pytest with coverage
just format      # auto-format
just check       # auto-fix lint

Citation

If you use this package in your research, please cite:

@software{trade_study2026,
  author = {Macdonald, Joshua C.},
  title = {{trade-study}: Multi-Objective Trade-Study Orchestration},
  year = {2026},
  url = {https://github.com/jcm-sci/trade-study},
  version = {0.5.1},
  doi = {10.5281/zenodo.19599838},
}

See CITATION.cff for machine-readable metadata.

License

MIT

Metadata

Release files for trade-study 0.5.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for trade-study 0.5.1
File Size Uploaded Provenance
trade_study-0.5.1.tar.gz 167.8 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for trade-study 0.5.1
File Interpreter ABI Platform
trade_study-0.5.1-py3-none-any.whl Python 3 none any Details

Total release size: 269.0 kB

Release files / trade_study-0.5.1.tar.gz

Download URL trade_study-0.5.1.tar.gz
Size 167.8 kB
Tags Source
SHA-256 checksum
How to use checksums
24e79eba8e45fea3fefd306a1fb75f9bf6f53614e440afb1c36028f8ecaec25c
BLAKE2b-256 checksum
How to use checksums
2c442dd9763ac108e4e40ac074e6b76fb5f0117aed12bb7bf012646aaf3d88ee
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Release files / trade_study-0.5.1-py3-none-any.whl

Download URL trade_study-0.5.1-py3-none-any.whl
Size 101.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
59eaf34f854e796c5b515290d6a96af250a6cbad5f08d9a6907492a936b74ae3
BLAKE2b-256 checksum
How to use checksums
d360f45926a369c3edfa1f5149ad4a2bcf3e1d47c7c38af051de50447a9b1ae1
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

Security for trade-study 0.5.1

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for trade_study-0.5.1.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 19, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page