Skip to content

Repository files navigation

trade-study

CIPyPIDOILicense: MITPython 3.10+Code style: ruffjcm-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?

NeedWithout trade-studyWith trade-study
Parameter gridManual itertools.product or one-off scriptsbuild_grid(factors, method="sobol") — full factorial, LHS, Sobol, Halton
Multi-objective rankingCall pymoo directly, handle direction normalizationextract_front(scores, directions) — direction-aware
Phased studiesCustom loop with manual filtering between stagesStudy(phases=[Phase(..., filter_fn=top_k_pareto_filter(k=20)), ...])
Adaptive searchSet up optuna study from scratchrun_adaptive(world, scorer, factors, observables, n_trials=600)
ReproducibilityScattered scripts, no standard protocolSimulator / 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

fromtrade_studyimport (
Direction,
Factor,
FactorType,
Observable,
Phase,
Study,
build_grid,
top_k_pareto_filter,
)
# 1. Define objectivesaccuracy=Observable("accuracy", Direction.MAXIMIZE)
latency=Observable("latency_ms", Direction.MINIMIZE)
cost=Observable("cost_usd", Direction.MINIMIZE)
# 2. Define factors and build a design gridfactors= [
Factor("learning_rate", FactorType.CONTINUOUS, bounds=(1e-4, 1e-1)),
Factor("backend", FactorType.CATEGORICAL, levels=["A", "B", "C"]),
]
grid=build_grid(factors, method="lhs", n_samples=500)
# 3. Run a hierarchical studystudy=Study(
world=MySimulator(), # implements Simulator protocolscorer=MyScorer(), # implements Scorer protocolobservables=[accuracy, latency, cost],
phases=[
Phase(
"screening",
grid=grid,
filter_fn=top_k_pareto_filter(k=20),
),
Phase("benchmark", grid="carry", filter_fn=None),
],
)
study.run(n_jobs=-1)
# 4. Inspect resultsprint(study.summary())
front=study.front("benchmark") # non-dominated config indiceshv=study.front_hypervolume("benchmark", ref_point) # hypervolume indicator

Protocols

Users implement two protocols to plug in their domain:

fromtrade_studyimportScorer, SimulatorclassMySimulator:
"""Implements the Simulator protocol."""defgenerate(self, config: dict) ->tuple:
"""Return (truth, observations) for a given config."""
...
classMyScorer:
"""Implements the Scorer protocol."""defscore(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]
ExtraPackagesPurpose
designpyDOE3, SALib, scipyGrid construction and sensitivity screening
paretopymooNon-dominated sorting and indicators
scoringscoringrulesProper scoring rules (CRPS, WIS, etc.)
stackingarviz, scipyBayesian and score-based ensemble weights
adaptiveoptunaMulti-objective Bayesian optimization
paralleljoblibParallel grid execution
allAll of the above

Core dependency: numpy only.

API overview

Design

fromtrade_studyimportFactor, FactorType, build_grid, screenfactors= [
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

fromtrade_studyimportrun_grid, run_adaptive# Grid mode: evaluate all configs (optional parallelism)results=run_grid(world, scorer, grid, observables, n_jobs=-1)
# Adaptive mode: multi-objective Bayesian optimization (NSGA-II)results=run_adaptive(world, scorer, factors, observables, n_trials=600)

Pareto analysis

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

Stacking

fromtrade_studyimportstack_scores, stack_bayesian, ensemble_predictweights=stack_scores(score_matrix) # simplex-constrained optimizationweights=stack_bayesian(idata_dict) # arviz stacking (Yao et al. 2018)combined=ensemble_predict(predictions, weights)

I/O

fromtrade_studyimportsave_results, load_resultssave_results(results, "study_results")
results=load_results("study_results")

Multi-fidelity search and surrogates

fromtrade_studyimportrun_successive_halving, run_hyperband, fit_surrogate, fit_regime_surrogate# Cheap-first budget-constrained screeningresults=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 descriptorsregime_surrogate=fit_regime_surrogate(results, regime_cols=["n", "p"])

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.

Related packages

PackageDescription
TradeStudy.jlJulia implementation of the same framework

Development

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.2.0},
doi = {10.5281/zenodo.19599838},
}

See CITATION.cff for machine-readable metadata.

License

MIT

About

Multi-objective trade-study orchestration with proper scoring rules, Pareto optimization, and Bayesian stacking for systematic model evaluation and design-space exploration.

Resources

Code of conduct

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages