Skip to content

Trade Studies

Design of experiments, batch evaluation, and Pareto extraction for parametric trade studies.

Requirements Template

RequirementsTemplate dataclass

RequirementsTemplate(parameters=dict())

Defines parameter sweep ranges for DOE/trade studies.

Parameters are stored as (name, min, max) tuples describing the design space to explore.

Example

rt = RequirementsTemplate() rt.add("freq_hz", 10e9, 30e9) rt.add("tx_power_w", 10.0, 200.0) space = rt.to_parameter_space()

add

add(name, lo, hi)

Add a parameter with its range.

Parameters:

Name Type Description Default
name str

Parameter name (must be unique).

required
lo float

Lower bound of the sweep range.

required
hi float

Upper bound of the sweep range.

required
Source code in src/opensatcom/trades/requirements.py
def add(self, name: str, lo: float, hi: float) -> None:
    """Add a parameter with its range.

    Parameters
    ----------
    name : str
        Parameter name (must be unique).
    lo : float
        Lower bound of the sweep range.
    hi : float
        Upper bound of the sweep range.
    """
    if lo > hi:
        raise ValueError(f"lo ({lo}) must be <= hi ({hi}) for parameter '{name}'")
    self.parameters[name] = (lo, hi)

to_parameter_space

to_parameter_space()

Return the parameter space as a dict of (min, max) tuples.

Returns:

Type Description
dict of str to tuple of (float, float)

Parameter names mapped to (lo, hi) bounds.

Source code in src/opensatcom/trades/requirements.py
def to_parameter_space(self) -> dict[str, tuple[float, float]]:
    """Return the parameter space as a dict of (min, max) tuples.

    Returns
    -------
    dict of str to tuple of (float, float)
        Parameter names mapped to ``(lo, hi)`` bounds.
    """
    return dict(self.parameters)

Design of Experiments

DesignOfExperiments

DesignOfExperiments(parameter_space)

Generate parameter combinations for trade study evaluation.

Supports Latin Hypercube Sampling (LHS), full factorial, and random sampling.

Parameters:

Name Type Description Default
parameter_space dict[str, tuple[float, float]]

Parameter names mapped to (min, max) ranges.

required
Source code in src/opensatcom/trades/doe.py
def __init__(self, parameter_space: dict[str, tuple[float, float]]) -> None:
    self.parameter_space = parameter_space
    self._names = list(parameter_space.keys())
    self._bounds = [parameter_space[n] for n in self._names]

generate

generate(n_samples, method='lhs', seed=42)

Generate parameter combinations.

Parameters:

Name Type Description Default
n_samples int

Number of design points.

required
method str

Sampling method: "lhs", "random", or "full_factorial".

'lhs'
seed int

Random seed for reproducibility.

42
Source code in src/opensatcom/trades/doe.py
def generate(self, n_samples: int, method: str = "lhs", seed: int = 42) -> pd.DataFrame:
    """Generate parameter combinations.

    Parameters
    ----------
    n_samples : int
        Number of design points.
    method : str
        Sampling method: "lhs", "random", or "full_factorial".
    seed : int
        Random seed for reproducibility.
    """
    if method == "lhs":
        return self.lhs(n_samples, seed=seed)
    elif method == "random":
        return self.random(n_samples, seed=seed)
    elif method == "full_factorial":
        return self.full_factorial(n_samples)
    else:
        raise ValueError(f"Unknown sampling method: {method}")

lhs

lhs(n_samples, seed=42)

Latin Hypercube Sampling using scipy.

Source code in src/opensatcom/trades/doe.py
def lhs(self, n_samples: int, seed: int = 42) -> pd.DataFrame:
    """Latin Hypercube Sampling using scipy."""
    from scipy.stats.qmc import LatinHypercube

    d = len(self._names)
    sampler = LatinHypercube(d=d, seed=seed)
    unit_samples = sampler.random(n=n_samples)

    # Scale to parameter bounds
    data = np.zeros_like(unit_samples)
    for j, (lo, hi) in enumerate(self._bounds):
        data[:, j] = lo + unit_samples[:, j] * (hi - lo)

    return pd.DataFrame(data, columns=self._names)

random

random(n_samples, seed=42)

Uniform random sampling.

Source code in src/opensatcom/trades/doe.py
def random(self, n_samples: int, seed: int = 42) -> pd.DataFrame:
    """Uniform random sampling."""
    rng = np.random.default_rng(seed)
    d = len(self._names)
    unit_samples = rng.random((n_samples, d))

    data = np.zeros_like(unit_samples)
    for j, (lo, hi) in enumerate(self._bounds):
        data[:, j] = lo + unit_samples[:, j] * (hi - lo)

    return pd.DataFrame(data, columns=self._names)

full_factorial

full_factorial(levels_per_param)

Full factorial design with equal levels per parameter.

Source code in src/opensatcom/trades/doe.py
def full_factorial(self, levels_per_param: int) -> pd.DataFrame:
    """Full factorial design with equal levels per parameter."""
    grids = []
    for lo, hi in self._bounds:
        grids.append(np.linspace(lo, hi, levels_per_param))

    combos = list(itertools.product(*grids))
    return pd.DataFrame(combos, columns=self._names)

Batch Runner

BatchRunner

BatchRunner(base_config_path=None)

Evaluates a DataFrame of parameter cases against a base config.

Each row in the cases DataFrame represents a parameter combination. The runner modifies the base config for each case, evaluates a link budget, and collects results.

Parameters:

Name Type Description Default
base_config_path str | None

Path to base YAML config. If None, a minimal default is used.

None
Source code in src/opensatcom/trades/batch.py
def __init__(self, base_config_path: str | None = None) -> None:
    self._base_config_path = base_config_path

run

run(cases_df, base_config=None, parallel=False)

Run batch evaluation.

Parameters:

Name Type Description Default
cases_df DataFrame

Parameter combinations. Columns map to config fields.

required
base_config dict | None

Base configuration dict to override with case parameters.

None
parallel bool

Use multiprocessing for parallel evaluation.

False
Source code in src/opensatcom/trades/batch.py
def run(
    self,
    cases_df: pd.DataFrame,
    base_config: dict[str, Any] | None = None,
    parallel: bool = False,
) -> pd.DataFrame:
    """Run batch evaluation.

    Parameters
    ----------
    cases_df : pd.DataFrame
        Parameter combinations. Columns map to config fields.
    base_config : dict | None
        Base configuration dict to override with case parameters.
    parallel : bool
        Use multiprocessing for parallel evaluation.
    """
    if parallel:
        return self._run_parallel(cases_df, base_config)
    return self._run_sequential(cases_df, base_config)

Pareto Extraction

pareto

Pareto front extraction and plotting for trade studies.

extract_pareto_front

extract_pareto_front(df, x_col, y_col, minimize_x=True, minimize_y=False)

Extract the Pareto-optimal (non-dominated) front from results.

Parameters:

Name Type Description Default
df DataFrame

Results DataFrame.

required
x_col str

Column name for x-axis objective.

required
y_col str

Column name for y-axis objective.

required
minimize_x bool

Whether x should be minimized (True) or maximized (False).

True
minimize_y bool

Whether y should be minimized (True) or maximized (False).

False

Returns:

Type Description
DataFrame

Subset of df containing only Pareto-optimal points.

Source code in src/opensatcom/trades/pareto.py
def extract_pareto_front(
    df: pd.DataFrame,
    x_col: str,
    y_col: str,
    minimize_x: bool = True,
    minimize_y: bool = False,
) -> pd.DataFrame:
    """Extract the Pareto-optimal (non-dominated) front from results.

    Parameters
    ----------
    df : pd.DataFrame
        Results DataFrame.
    x_col : str
        Column name for x-axis objective.
    y_col : str
        Column name for y-axis objective.
    minimize_x : bool
        Whether x should be minimized (True) or maximized (False).
    minimize_y : bool
        Whether y should be minimized (True) or maximized (False).

    Returns
    -------
    pd.DataFrame
        Subset of df containing only Pareto-optimal points.
    """
    # Convert objectives: multiply by -1 for maximization so we always minimize
    import numpy as np

    x: np.ndarray = np.asarray(df[x_col].values, dtype=float)
    y: np.ndarray = np.asarray(df[y_col].values, dtype=float)
    if not minimize_x:
        x = -x
    if not minimize_y:
        y = -y

    n = len(df)
    is_pareto = [True] * n

    for i in range(n):
        if not is_pareto[i]:
            continue
        for j in range(n):
            if i == j or not is_pareto[j]:
                continue
            # j dominates i if j is <= in both and < in at least one
            if x[j] <= x[i] and y[j] <= y[i] and (x[j] < x[i] or y[j] < y[i]):
                is_pareto[i] = False
                break

    return df.iloc[[i for i in range(n) if is_pareto[i]]].copy()

plot_pareto

plot_pareto(df, x_col, y_col, pareto_df)

Create a scatter plot with Pareto front highlighted.

Returns matplotlib Figure.

Source code in src/opensatcom/trades/pareto.py
def plot_pareto(
    df: pd.DataFrame,
    x_col: str,
    y_col: str,
    pareto_df: pd.DataFrame,
) -> Figure:
    """Create a scatter plot with Pareto front highlighted.

    Returns matplotlib Figure.
    """
    import matplotlib

    matplotlib.use("Agg")
    import matplotlib.pyplot as plt

    fig, ax = plt.subplots(figsize=(8, 6))
    ax.scatter(df[x_col], df[y_col], alpha=0.4, label="All points", color="#90caf9")

    # Sort Pareto front for line plot
    pf = pareto_df.sort_values(x_col)
    ax.scatter(pf[x_col], pf[y_col], color="#1565c0", s=80, zorder=5, label="Pareto front")
    ax.plot(pf[x_col], pf[y_col], "r--", linewidth=1.5, alpha=0.7)

    ax.set_xlabel(x_col)
    ax.set_ylabel(y_col)
    ax.set_title("Pareto Front Analysis")
    ax.legend()
    ax.grid(True, alpha=0.3)
    fig.tight_layout()
    return fig