Parameter space search under uncertainty — explore, evaluate, and optimize simulation parameters with Monte Carlo awareness.
Built on top of gds-sim, gds-psuu provides a search engine for intelligently exploring simulation parameter spaces to optimize KPIs.
uv add gds-psuu
# or: pip install gds-psuu
# For Bayesian optimization (optional):
uv add "gds-psuu[bayesian]"from gds_sim import Model, StateUpdateBlock
from gds_psuu import (
KPI, Continuous, GridSearchOptimizer,
ParameterSpace, Sweep, final_value, mean_agg,
)
# Define a gds-sim model
model = Model(
initial_state={"population": 100.0},
state_update_blocks=[
StateUpdateBlock(
policies={"growth": lambda s, p, **kw: {"delta": s["population"] * p["growth_rate"]}},
variables={"population": lambda s, p, signal=None, **kw: ("population", s["population"] + signal["delta"])},
)
],
)
# Define what to search and what to measure
sweep = Sweep(
model=model,
space=ParameterSpace(params={"growth_rate": Continuous(min_val=0.01, max_val=0.2)}),
kpis=[KPI(name="avg_pop", metric=final_value("population"), aggregation=mean_agg)],
optimizer=GridSearchOptimizer(n_steps=5),
timesteps=10,
runs=3,
)
results = sweep.run()
best = results.best("avg_pop")
print(f"Best growth_rate: {best.params['growth_rate']:.3f}")Metric (per-run scalar) + Aggregation (cross-run reducer) = KPI:
from gds_psuu import (
KPI, final_value, trajectory_mean, max_value, min_value,
mean_agg, std_agg, percentile_agg, probability_above, probability_below,
)
# Mean of final population across Monte Carlo runs
KPI(name="avg_pop", metric=final_value("population"), aggregation=mean_agg)
# 95th percentile of trajectory means
KPI(name="p95", metric=trajectory_mean("x"), aggregation=percentile_agg(95))
# Probability that max value exceeds a threshold
KPI(name="risk", metric=max_value("x"), aggregation=probability_above(500.0))Per-run distributions are tracked in EvaluationResult.distributions for downstream analysis.
Three dimension types with validation:
from gds_psuu import Continuous, Integer, Discrete, ParameterSpace
space = ParameterSpace(params={
"learning_rate": Continuous(min_val=0.001, max_val=0.1),
"layers": Integer(min_val=1, max_val=5),
"activation": Discrete(values=("relu", "tanh")),
})Define feasible regions with linear or functional constraints:
from gds_psuu import LinearConstraint, FunctionalConstraint
# x + y <= 1.0
LinearConstraint(coefficients={"x": 1.0, "y": 1.0}, bound=1.0)
# Custom constraint function
FunctionalConstraint(fn=lambda p: p["x"] ** 2 + p["y"] ** 2 <= 1.0)Grid search filters infeasible points; random search uses rejection sampling.
| Optimizer | Strategy | When to use |
|---|---|---|
GridSearchOptimizer(n_steps) |
Exhaustive grid | 1-2 dimensions, full coverage |
RandomSearchOptimizer(n_samples, seed) |
Uniform random | Higher dimensions, exploration |
BayesianOptimizer(n_calls, target_kpi) |
Gaussian process (optuna) | Expensive sims, optimization |
Multi-KPI optimization:
from gds_psuu import SingleKPI, WeightedSum
# Single KPI (maximize or minimize)
obj = SingleKPI(name="avg_pop", maximize=True)
# Weighted combination of multiple KPIs
obj = WeightedSum(weights={"profit": 1.0, "risk": -0.5})
results.best_by_objective(obj)Screen parameter importance before running expensive sweeps:
from gds_psuu import OATAnalyzer, MorrisAnalyzer
# One-at-a-time: vary each parameter independently
oat = OATAnalyzer(n_steps=5)
result = oat.analyze(evaluator, space)
# Morris method: elementary effects (mu_star = influence, sigma = nonlinearity)
morris = MorrisAnalyzer(r=10, levels=4)
result = morris.analyze(evaluator, space)
result.ranking("my_kpi") # parameters sorted by importanceParameter Point -> Simulation -> Results -> Metric -> Aggregation -> KPI
|
Optimizer.suggest() --> Evaluator.evaluate(params) --> Optimizer.observe(scores)
^ | |
| gds-sim Simulation |
+------------------------ repeat --------------------------+
Full docs at blockscience.github.io/gds-core.
Apache-2.0 — BlockScience