Complexa Sweep
NVIDIAOfficialCreated by Ohad MosafiDistributed by NVIDIA
- Skill
- Workflow
- Science and research
- Apache-2.0
- Medium risk
- Has scripts
AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The complexa-sweep skill provides a structured runbook for executing systematic parameter sweeps over Proteina-Complexa design pipelines. It automates the generation of multiple inference configurations based on a sweeper YAML, manages the execution loop across these configurations, and aggregates results into a ranked summary. This process enables users to perform ablations, tuning, and Pareto searches over quality and computational cost metrics, ensuring consistent evaluation across diverse parameter sets.
What it does
- Generates multiple inference configurations from a cartesian-product sweeper YAML.
- Executes the design pipeline across a generated grid of parameter combinations.
- Aggregates per-config success metrics into a ranked summary CSV.
- Identifies the best-performing configuration and Pareto frontiers.
- Provides a manifest for result tracking and experiment reproducibility.
- Includes pre-flight checks to estimate GPU budget and hardware readiness.
When to use it
- When performing hyperparameter tuning or ablations on design pipelines.
- When comparing multiple search algorithms or model configurations.
- When conducting Pareto searches to balance design quality against wall-clock time.
- When you need to rank configurations based on success rates and diversity metrics.
Advantages
- Systematic exploration of parameter space via cartesian-product enumeration.
- Automated aggregation of results simplifies complex multi-run analysis.
- Supports multi-GPU sharding for efficient execution of large sweeps.
- Provides clear visibility into cost and resource requirements before execution.
Usage examples
- “Run a sweep over beam_width and nsteps for the 22_DerF21 task.”
- The agent will generate the necessary inference configs using the specified sweeper YAML, execute the design pipeline for each combination, and produce a ranked sweep_summary.csv.
- “Estimate the GPU cost for a 4-axis sweep with 4 values each.”
- The agent will calculate the total number of configurations and estimate the total GPU-hours based on the per-run baseline, prompting for confirmation before proceeding.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/nvidia/complexa-sweep/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip complexa-sweep.zip -d .agents/skills
Documentation by Ohad Mosafi
complexa-sweep
Run cartesian-product parameter sweeps over Proteina-Complexa design pipelines. Pick or author a sweeper YAML in configs/sweeps/, expand it to N inference configs with script_utils/generate_inference_configs.py, loop complexa design over those configs, then aggregate per-config success metrics into a ranked summary CSV plus a manifest.
Important: The
complexa designCLI does NOT accept--sweeperdirectly. Sweeps are driven byscript_utils/generate_inference_configs.py, which writes oneconfigs/inference_configs/inf_{idx}_{run_name}.yamlper sweep combination. You then loopcomplexa designover those generated configs.
Instructions
Sweeper authoring, Cartesian enumeration, cost estimates, and analysis of supplied CSVs need only the relevant configs and data. Complete those artifacts without setting up inference. Read the relevant axis section in sweep_axes.md, then check the matching keys in the selected source config for current defaults. Summary CSVs with supplied counts and objective columns do not require the full inference or metric guides. For a partial snapshot, enumerate combinations locally and provide the future generator command; running that command requires the full project and its dependencies.
For a requested launch, check readiness and the budget below. A single existing job or a SLURM template does not require a parameter sweep.
Step 1: Pre-flight
Before GPU execution, or when the request conditions its workflow on hardware,
set SKILL_DIR to the directory containing this manifest and run preflight once.
Use a separate shell assignment; an inline environment assignment cannot set
$SKILL_DIR's expansion in the same command.
Reuse an existing probe from the same request if the environment is unchanged:
bash "$SKILL_DIR"/scripts/preflight.shRead ./complexa_setup/preflight.json. A sweep multiplies GPU time by the number
of configs and targets. Compute the budget before launch and obtain confirmation
if the user has not already authorized that scope and cost:
"This sweep produces N configs × ~M minutes per config ≈ TOTAL GPU-hours. OK to proceed? (y / reduce / cancel)"
If gpu.available=false, stop before GPU execution. Sweeper authoring,
Cartesian-product enumeration, cost estimates, and analysis of supplied CSVs
can run on CPU. For those requests, finish the requested artifacts and record
the launch prerequisites. Label synthetic input data as synthetic and keep
estimated GPU time separate from measured runtime.
For a hardware-conditional request, verify the allocated hardware before
selecting the CPU fallback. Missing weights, dependencies, or sufficient VRAM
for an allocated GPU are launch blockers, not evidence of a CPU-only environment.
Report probe or execution failures without reclassifying the selected branch.
The shared preflight's gpu.available=false also covers failed or missing
nvidia-smi; check device/allocation evidence before treating it as GPU absence.
In containers, /proc/driver/nvidia/gpus and loaded kernel modules may describe
the host, not this task's allocation. Check container device exposure and any
available scheduler allocation information together. A CPU allocation can share
a GPU host; an allocated GPU with inaccessible devices is blocked. If allocation
remains unclear, report it as undetermined.
Step 2: Pick the pipeline + target
Use the user's supplied pipeline, targets, and run name. Protein, ligand, and AME
runs use search_binder_local_pipeline, search_ligand_binder_local_pipeline,
and search_ame_local_pipeline, respectively. Ask only for missing values:
pipeline_config_name— e.g.search_binder_local_pipeline(default),search_ligand_binder_local_pipeline,search_ame_local_pipeline.task_name— e.g.02_PDL1,22_DerF21,39_7V11_LIGAND. Passed as--override generation.task_name=<task>.run_name— short tag for output dir naming.
Examples
Step 3: Pick or author the sweeper YAML
Sweeper YAMLs live in configs/sweeps/. Each key is a dot-notation Hydra path; each value is a list. The cartesian product becomes N configs.
Canned sweepers
| File | Axis | Values | Configs |
|---|---|---|---|
configs/sweeps/beam_width.yaml | generation.search.beam_search.beam_width | 1, 2, 4, 8 | 4 |
configs/sweeps/bb_ca_temperature.yaml | generation.model.bb_ca.simulation_step_params.sc_scale_noise | 0.1, 0.4 | 2 |
configs/sweeps/search_replicas.yaml | generation.search.best_of_n.replicas | 1, 4, 16, 64 | 4 |
configs/sweeps/example.yaml | beam_width × nsteps | (2,4) × (200,400) | 4 |
Use a matching sweeper, then check that its axes affect the selected algorithm.
The binder config defaults to best-of-n: a beam-width sweep must also pin
generation.search.algorithm: [beam-search]. A replicas sweep pins
generation.search.algorithm: [best-of-n]. For AME, reward-guided search also
requires a configured reward model; its default is null.
Authoring a new sweeper
Minimal multi-axis example (saved to configs/sweeps/my_sweep.yaml):
# 3 beam widths × 2 nsteps = 6 configs
generation.search.algorithm: [beam-search]
generation.search.beam_search.beam_width:
- 2
- 4
- 8
generation.args.nsteps:
- 200
- 400Rules (from script_utils/generate_inference_configs.py:load_sweeper_file):
Top-level mapping only. Keys are dot-notation Hydra paths.
Keep budgets, launch status, and other planning metadata in the launch plan, outside the sweeper. Every top-level entry becomes an override axis; a separate
targetslist would multiply the grid again.Values must be lists. A scalar is auto-wrapped into a single-element list (which pins a value without adding a dimension).
For a list-valued parameter, nest each candidate list:
metric.sequence_types: [[self, mpnn]]pins one list. The flat form sweeps two strings;--overrideparses scalars only.Cartesian product: total configs = product of list lengths. Multiply by the number of targets unless target names are already one of the axes; do not count them twice.
If a key appears in both the sweeper file and an
--override, the override wins and that axis collapses.Reject empty axes before launch: an axis of
[]produces zero configurations.
See references/sweep_axes.md for the full catalogue of swept keys (typical ranges, cost multipliers, what improves/regresses).
Dry-run preview before generating
With the full project available, confirm the config count first:
python script_utils/generate_inference_configs.py \
--config_name search_binder_local_pipeline \
--sweeper configs/sweeps/my_sweep.yaml \
--override generation.task_name=22_DerF21 \
--run_name my_sweep \
--dryrunThe output lists every axis + value list and prints DRY RUN — would generate N config pair(s).
This previews enumeration only; it returns before Hydra composition and does
not validate the pipeline, parameter types, or launch prerequisites.
When the full project is available, inspect the materialized inference configs before launching. Check the resolved target, selected search algorithm, and requested parameter values across the generated grid. The generator permits unknown keys during merging, so a successful write alone does not establish that a misspelled or misplaced axis changes the intended parameter. With only a partial snapshot, check each dot-path against its source config and report that materialization remains unverified.
Step 4: Generate configs + loop complexa design
Once the dry-run looks right, drop --dryrun to materialize inf_{idx}_{run_name}.yaml + eval_{idx}_{run_name}.yaml pairs under configs/inference_configs/ and configs/eval_configs/. Then loop complexa design over the inference configs.
For multiple targets, put generation.task_name in the sweeper as an axis, or
give each target's generator invocation a distinct --run_name. Indices restart
at zero on each invocation and filenames omit the target; reusing the same run
name and output directories overwrites earlier configs and reuses result paths.
# 1. Generate one inf_*.yaml + one eval_*.yaml per combination.
python script_utils/generate_inference_configs.py \
--config_name search_binder_local_pipeline \
--sweeper configs/sweeps/my_sweep.yaml \
--override generation.task_name=22_DerF21 \
--run_name my_sweep
# 2. Loop complexa design over each generated inference config.
for cfg in configs/inference_configs/inf_*_my_sweep.yaml; do
complexa design "$cfg" || echo "FAILED: $cfg"
doneThis serialises the sweep on a single GPU — be honest with the user that wall-clock = N × per-run.
Multi-GPU host (optional speed-up)
On a host with K GPUs, shard the config list K ways and launch one loop per GPU in parallel. Each complexa design call uses exactly one GPU at default gen_njobs=1 / eval_njobs=1, so pinning via CUDA_VISIBLE_DEVICES keeps them from colliding:
CONFIGS=(configs/inference_configs/inf_*_my_sweep.yaml)
N=${#CONFIGS[@]}; K=4 # 4 GPUs
for gpu in $(seq 0 $((K-1))); do
(
for i in $(seq $gpu $K $((N-1))); do
CUDA_VISIBLE_DEVICES=$gpu complexa design "${CONFIGS[$i]}" \
|| echo "FAILED on GPU $gpu: ${CONFIGS[$i]}"
done
) &
done
waitDrop gen_njobs / eval_njobs overrides into the loop only if you intentionally want each complexa design invocation to consume multiple GPUs (and you have a way to keep them out of each other's way).
Step 5: Collect results
Each swept config writes to its own ./inference/inf_{idx}_{run_name}/ directory; the analyze step writes ./evaluation_results/eval_{idx}_{run_name}/results_*.csv. After the sweep finishes:
ls -d ./inference/inf_*_my_sweep/
ls ./evaluation_results/eval_*_my_sweep/results_*.csvInspect the supplied CSV schema before aggregating. If it already contains one
row per configuration with n_passed and n_designs, compute
success_rate = n_passed / n_designs; flag zero denominators and invalid counts.
Do not treat those summary rows as individual designs. For per-design rows,
use existing pass flags, parsing string booleans explicitly. If flags are absent,
use the run's analysis thresholds, result_type, column names, and units from
EVALUATION_METRICS.md. PAE may be normalized
by 31 and pLDDT may use different scales; do not substitute generic thresholds.
Step 6: Rank configs
Emit sweep_summary.csv to the run directory. One row per config:
| Column | How to compute |
|---|---|
config_id | The {idx} from inf_{idx}_{run_name} |
<axis_1>, <axis_2>, ... | The swept value at this combination (read from the per-config inf_*.yaml) |
n_samples | n_designs for a supplied config summary, otherwise per-design row count |
success_rate | n_passed / n_designs for a config summary, otherwise mean of parsed per-design pass flags |
mean_i_pae | Preserve the supplied config mean, or average per-design values in consistent units (lower = better) |
mean_i_plddt | i_plddt.mean() (higher = better) |
diversity_score | Unique sequence count / n_samples (or use TM-score clustering if available) |
wall_clock_min | From the per-config log timestamps |
Populate only metrics present in the inputs or computable from them. Missing diversity or timing data stays unavailable.
Then report:
Best config = argmax of
success_rate. Tie-break onmean_i_pae(lower).Pareto frontier: choose the requested objectives and directions. For time versus success, minimize time and maximize success; for time versus iPAE, minimize both. A point is dominated when another is no worse on every objective and strictly better on at least one. Keep distinct config IDs at equal nondominated coordinates; report invalid/missing measurements separately.
Save the full table to sweep_summary.csv, then reopen the saved results and
print a compact summary: row count, best config and metric values, and frontier
IDs with their objective values. Report the CSV paths and keep the hardware
probe and configuration dumps separate from this result summary.
Step 7: Emit manifest
Capture the resolved invocation + outputs for replay. The shared helper takes a single --output-dir (it walks for CSVs and pulls the Hydra config from the run's .hydra/), so call it against the best-config's evaluation directory and pass the parent complexa design command for that run:
BEST_ID=4 # from Step 6 ranking
python3 "$SKILL_DIR"/scripts/write_manifest.py \
--output-dir ./evaluation_results/eval_${BEST_ID}_my_sweep \
--command "python script_utils/generate_inference_configs.py --config_name search_binder_local_pipeline --sweeper configs/sweeps/my_sweep.yaml --override generation.task_name=22_DerF21 --run_name my_sweep && for cfg in configs/inference_configs/inf_*_my_sweep.yaml; do complexa design \"\$cfg\"; done" \
--skill complexa-sweep \
--out ./sweep_runs/my_sweep/sweep_manifest.jsonAlongside the manifest, save the ranked sweep_summary.csv from Step 6 to ./sweep_runs/my_sweep/sweep_summary.csv and surface both paths to the user.
Recommended sweep recipes
| Symptom | Sweep this | Why |
|---|---|---|
| Quality not good enough | beam_width × nsteps (use example.yaml as a starting point) | Both raise compute → quality; find the cheapest combination that lands. |
| Too slow / want speed-up | nsteps downward (e.g. [100, 200, 400]) with fixed beam_width=4 | Find the smallest nsteps that retains success rate. |
| Mode collapse / low diversity | generation.model.bb_ca.simulation_step_params.sc_scale_noise (use bb_ca_temperature.yaml) | Higher noise → more diverse backbones. |
| Reward over-fitting (high reward, bad metrics) | generation.reward_model.reward_models.af2folding.reward_weights.{i_pae, plddt} ratio | Re-balance composite reward. |
| Broaden the best-of-N candidate pool | search_replicas.yaml (best_of_n.replicas) | More candidates per search; use separate seeds/runs to measure repeatability. |
| Algorithm shoot-out | generation.search.algorithm over [single-pass, best-of-n, beam-search, fk-steering] | Compare search regimes; pin everything else. |
Hardware
Total GPU-time for a sweep = N_configs × per_run_GPU_time. A single search_binder_local_pipeline run on one A100 is roughly 30–90 min (binder length + nsteps dependent). A 4-axis × 4-value sweep = 256 configs × ~60 min = ~256 GPU-hours — at that scale, plan on a multi-GPU host with the Step-4 sharding pattern.
Refer to references/hardware.md for the per-run baseline + VRAM minima.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Sweeper file not found from generate_inference_configs.py | Path resolved from wrong CWD | Use a path relative to the repo root; or pass an absolute path. |
Sweeper YAML must be a mapping | Top-level YAML is a list or scalar | Rewrite as key: [v1, v2] mapping. |
| Dry-run reports zero config pairs | One of the value lists is empty [] | Supply at least one value before generating or launching; the generator does not reject an empty product. |
complexa design rejects --sweeper | Confusion between entrypoints | The CLI does not accept --sweeper. Use generate_inference_configs.py first, then loop complexa design over the generated configs. |
| Override silently collapses a sweep axis | --override key=v shadowed a sweep key | Drop either the override OR the matching key from the sweeper file. |
| One config in the loop fails, sweep keeps going | The || echo "FAILED…" in Step 4 swallows the error | Re-run the failed inf_*.yaml standalone; check the per-config log under ./logs/. Skip failed config_id when ranking. |
For per-axis reference (typical ranges, cost, what gets better/worse), see references/sweep_axes.md.
For the user-facing sweep system overview (config generation, output layout), see references/SWEEP.md.
Files
- ATTRIBUTION.mdAdded by AutoGineer955 B
- BENCHMARK.md7.9 KiB
- LICENSELicence file added by AutoGineer11.2 KiB
- SKILL.md16.5 KiB
- config/skillspector-baseline.yml1.2 KiB
- evals/evals.json34.9 KiB
- evals/files/launch_review/launch.shScript586 B
- evals/files/launch_review/sweeper.yaml89 B
- evals/files/pareto_results.csv84 B
- evals/files/project/SNAPSHOT.md1.0 KiB
- evals/files/project/configs/pipeline/binder/binder_generate.yaml9.0 KiB
- evals/files/project/configs/pipeline/model_sampling.yaml1.4 KiB
- evals/files/project/configs/search_binder_local_pipeline.yaml1.5 KiB
- evals/files/project/configs/sweeps/example.yaml1.1 KiB
- evals/files/project/script_utils/generate_inference_configs.pyScript15.1 KiB
- evals/files/project/source_manifest.json670 B
- evals/files/summary_review.csv297 B
- evals/files/sweep_results.csv322 B
- references/CONFIGURATION_GUIDE.md34.7 KiB
- references/EVALUATION_METRICS.md55.9 KiB
- references/INFERENCE.md14.9 KiB
- references/SEARCH_METADATA.md7.7 KiB
- references/SWEEP.md10.6 KiB
- references/hardware.md4.8 KiB
- references/sweep_axes.md11.7 KiB
- scripts/preflight.shScript8.4 KiB
- scripts/write_manifest.pyScript9.6 KiB
- skill-card.md4.6 KiB
- skill.oms.sig9.9 KiB
evals/files/project/script_utils/generate_inference_configs.py
"""Generate inference and evaluation config files from a pipeline config.
Given a base pipeline config (e.g., search_binder_pipeline.yaml), this script
applies sweep combinations from a YAML sweeper file and/or CLI overrides, then
writes one inference + evaluation config pair per combination.
Sweep axes (from --sweeper YAML file) are combined via cartesian product.
Overrides (from --override KEY=VAL) are applied to every generated config.
If a key appears in both, the override wins (collapses that sweep axis).
Usage:
# No sweep, single config
python generate_inference_configs.py \\
--config_name search_binder_pipeline --run_name my_run
# Override target
python generate_inference_configs.py \\
--config_name search_binder_pipeline --run_name my_run \\
--override generation.task_name=22_DerF21
# Sweep from YAML file
python generate_inference_configs.py \\
--config_name search_binder_pipeline --run_name my_run \\
--sweeper configs/sweeps/beam_width.yaml
# Sweep + override (override pins a value across all sweep combos)
python generate_inference_configs.py \\
--config_name search_binder_pipeline --run_name my_run \\
--sweeper configs/sweeps/beam_width.yaml \\
--override generation.task_name=22_DerF21 generation.args.nsteps=400
# Dry run to preview
python generate_inference_configs.py \\
--config_name search_binder_pipeline --run_name my_run \\
--sweeper configs/sweeps/beam_width.yaml --dryrun
"""
import argparse
import itertools
import logging
import math
import os
from typing import Any
import hydra
import yaml
from omegaconf import OmegaConf
logger = logging.getLogger(__name__)
# =============================================================================
# Value Parsing
# =============================================================================
def parse_scalar(value: str) -> Any:
"""Parse a string into a typed Python scalar.
Supports int, float, bool (true/false), None (null), and falls back to str.
"""
if value.lower() == "true":
return True
if value.lower() == "false":
return False
if value.lower() == "null" or value.lower() == "none":
return None
try:
return int(value)
except ValueError:
pass
try:
return float(value)
except ValueError:
pass
return value
def parse_override(entry: str) -> tuple[str, Any]:
"""Parse a single KEY=VAL override string into (key, typed_value).
Args:
entry: String in the form "dotted.key=value".
Returns:
Tuple of (key, parsed_value).
Raises:
ValueError: If entry has no '=' or empty key/value.
"""
if "=" not in entry:
raise ValueError(f"Override must be KEY=VALUE, got: {entry!r}")
key, _, raw_value = entry.partition("=")
key = key.strip()
raw_value = raw_value.strip()
if not key:
raise ValueError(f"Empty key in override: {entry!r}")
if not raw_value:
raise ValueError(f"Empty value in override: {entry!r}")
return key, parse_scalar(raw_value)
# =============================================================================
# Sweeper Construction
# =============================================================================
def load_sweeper_file(path: str) -> dict[str, list]:
"""Load a sweeper YAML file and validate that all values are lists.
Scalar values are automatically wrapped in a single-element list.
Returns:
Dictionary mapping dot-notation keys to lists of values.
Raises:
FileNotFoundError: If the file does not exist.
"""
if not os.path.isfile(path):
raise FileNotFoundError(f"Sweeper file not found: {path}")
with open(path) as f:
raw = yaml.safe_load(f) or {}
if not isinstance(raw, dict):
raise ValueError(f"Sweeper YAML must be a mapping, got {type(raw).__name__}")
sweep_dict: dict[str, list] = {}
for key, val in raw.items():
if not isinstance(key, str):
raise ValueError(f"Sweeper keys must be strings, got {type(key).__name__}: {key}")
sweep_dict[key] = val if isinstance(val, list) else [val]
return sweep_dict
def build_sweeper(
sweeper_file: str | None = None,
overrides: list[str] | None = None,
) -> tuple[dict[str, list], dict[str, Any]]:
"""Build sweep dict and override dict from CLI inputs.
If a key appears in both the sweeper file and overrides, the override wins
and that key is removed from the sweep (collapsing the axis to one value).
Args:
sweeper_file: Path to a YAML sweep file (optional).
overrides: List of "KEY=VAL" strings (optional).
Returns:
(sweep_dict, override_dict) where sweep_dict has list values and
override_dict has scalar values.
"""
sweep_dict: dict[str, list] = {}
if sweeper_file:
sweep_dict = load_sweeper_file(sweeper_file)
override_dict: dict[str, Any] = {}
for entry in overrides or []:
key, value = parse_override(entry)
override_dict[key] = value
# Override wins: remove conflicting keys from sweep
for key in override_dict:
sweep_dict.pop(key, None)
return sweep_dict, override_dict
# =============================================================================
# Config Helpers
# =============================================================================
def dot_key_dict_to_nested_dict(input_dict: dict) -> dict:
"""Convert dictionary with dot-notation keys to a nested dictionary.
Example::
{"A.B.x": 1, "A.B.y": 2, "C": 3}
-> {"A": {"B": {"x": 1, "y": 2}}, "C": 3}
"""
output_dict: dict = {}
child_keys: set = set()
for key, value in input_dict.items():
if "." not in key:
output_dict[key] = value
else:
child_keys.add(key.split(".")[0])
for child_key in child_keys:
child_input = {".".join(k.split(".")[1:]): v for k, v in input_dict.items() if k.startswith(child_key + ".")}
child_output = dot_key_dict_to_nested_dict(child_input)
if child_key in output_dict:
assert isinstance(output_dict[child_key], dict), (
f"Key {child_key} should map to dict, got {type(output_dict[child_key])}"
)
output_dict[child_key].update(child_output)
else:
output_dict[child_key] = child_output
return output_dict
def get_task_name_from_config(cfg) -> str | None:
"""Extract task_name from a pipeline config.
Checks generation.task_name first, then
generation.dataloader.dataset.task_name.
"""
try:
if "generation" in cfg and "task_name" in cfg.generation:
return cfg.generation.task_name
except Exception:
pass
try:
if "generation" in cfg and "dataloader" in cfg.generation:
ds = cfg.generation.dataloader.dataset
if "task_name" in ds:
return ds.task_name
except Exception:
pass
return None
def create_eval_config(infer_cfg) -> OmegaConf:
"""Derive an evaluation config from a pipeline config.
Deep-copies the pipeline config and sets the evaluation/analysis output
paths based on the inference root_path.
"""
eval_cfg = OmegaConf.create(OmegaConf.to_container(infer_cfg, resolve=False))
OmegaConf.set_struct(eval_cfg, False)
root_path = infer_cfg.get("root_path", "./inference/default")
eval_output_dir = root_path.replace("./inference/inf_", "./evaluation_results/eval_")
if eval_output_dir == root_path:
eval_output_dir = root_path.replace("inference", "evaluation_results", 1)
eval_cfg["sample_storage_path"] = root_path
eval_cfg["output_dir"] = eval_output_dir
eval_cfg["results_dir"] = eval_output_dir
eval_cfg["eval_njobs"] = infer_cfg.get("eval_njobs", infer_cfg.get("gen_njobs", 1))
task_name = get_task_name_from_config(infer_cfg)
if task_name:
if "dataset" not in eval_cfg:
eval_cfg["dataset"] = {}
eval_cfg["dataset"]["task_name"] = task_name
if "generation" in infer_cfg and "target_dict_cfg" in infer_cfg.generation:
eval_cfg["dataset"]["target_dict_cfg"] = infer_cfg.generation.target_dict_cfg
OmegaConf.set_struct(eval_cfg, True)
return eval_cfg
# =============================================================================
# Core: Apply Sweep + Overrides and Save Configs
# =============================================================================
def apply_sweeper_and_save_configs(
base_cfg,
sweep_dict: dict[str, list],
override_dict: dict[str, Any],
infer_output_dir: str,
eval_output_dir: str,
run_name: str | None = None,
start_index: int = 0,
) -> int:
"""Apply cartesian-product sweep and per-config overrides, then save.
For each combination in the cartesian product of ``sweep_dict`` values,
merges the combination into ``base_cfg``, applies ``override_dict`` on top,
sets output paths, derives the eval config, and saves both YAML files.
Args:
base_cfg: Base pipeline OmegaConf config.
sweep_dict: ``{dotted_key: [val1, val2, ...]}`` for sweep axes.
override_dict: ``{dotted_key: scalar}`` applied to every config.
infer_output_dir: Directory for generated inference configs.
eval_output_dir: Directory for generated evaluation configs.
run_name: Optional run name suffix for config filenames.
start_index: Starting index for sequential config numbering.
Returns:
Number of config pairs generated.
"""
# Build cartesian product of sweep axes
keys = list(sweep_dict.keys())
value_lists = [sweep_dict[k] for k in keys]
if keys:
combinations = [dict(zip(keys, combo, strict=False)) for combo in itertools.product(*value_lists)]
else:
combinations = [{}]
os.makedirs(infer_output_dir, exist_ok=True)
os.makedirs(eval_output_dir, exist_ok=True)
for i, combo in enumerate(combinations):
# Deep-copy base config so we never mutate the caller's object
cfg_copy = OmegaConf.create(OmegaConf.to_container(base_cfg, resolve=False))
# Merge sweep combination into the copy
nested_combo = dot_key_dict_to_nested_dict(combo)
override_cfg = OmegaConf.create(nested_combo)
OmegaConf.set_struct(cfg_copy, False)
infer_cfg = OmegaConf.merge(cfg_copy, override_cfg)
# Apply scalar overrides on top
for key, value in override_dict.items():
OmegaConf.update(infer_cfg, key, value, merge=True)
# Build filename: {idx}[_{run_name}]
# Task name is intentionally NOT included — the bash launcher builds
# CONFIG_SUFFIX from its own CLI args and must be able to predict
# the filename without inspecting config content.
idx = start_index + i
name_parts = [str(idx)]
if run_name:
name_parts.append(run_name)
base_name = "_".join(name_parts)
# Set inference output path
infer_cfg["root_path"] = f"./inference/inf_{base_name}"
OmegaConf.set_struct(infer_cfg, True)
# Derive eval config
eval_cfg = create_eval_config(infer_cfg)
# Save both configs
inf_filename = f"inf_{base_name}.yaml"
eval_filename = f"eval_{base_name}.yaml"
OmegaConf.save(config=infer_cfg, f=os.path.join(infer_output_dir, inf_filename))
OmegaConf.save(config=eval_cfg, f=os.path.join(eval_output_dir, eval_filename))
logger.info(f"Generated: {inf_filename}, {eval_filename}")
return len(combinations)
# =============================================================================
# CLI
# =============================================================================
def build_parser() -> argparse.ArgumentParser:
"""Build the argument parser. Separated for testability."""
parser = argparse.ArgumentParser(
description="Generate inference and evaluation config files from a pipeline config.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--config_name",
type=str,
required=True,
help="Name of the pipeline config file (without .yaml)",
)
parser.add_argument(
"--infer_dir_cfgs",
type=str,
default="./configs/inference_configs",
help="Directory to write generated inference config files",
)
parser.add_argument(
"--eval_dir_cfgs",
type=str,
default="./configs/eval_configs",
help="Directory to write generated evaluation config files",
)
parser.add_argument(
"--run_name",
type=str,
default=None,
help="Run name suffix embedded in config filenames",
)
parser.add_argument(
"--sweeper",
type=str,
default=None,
help="Path to a YAML file defining sweep axes (cartesian product)",
)
parser.add_argument(
"--override",
nargs="*",
default=[],
help="One or more KEY=VAL overrides applied to every generated config",
)
parser.add_argument(
"--dryrun",
action="store_true",
help="Show what would be generated without creating files",
)
return parser
def main(argv: list[str] | None = None):
"""Entry point for config generation."""
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
parser = build_parser()
args = parser.parse_args(argv)
# Build sweep + overrides
sweep_dict, override_dict = build_sweeper(args.sweeper, args.override)
# Compute expected config count
if sweep_dict:
config_count = math.prod(len(v) for v in sweep_dict.values())
else:
config_count = 1
# Log configuration summary
logger.info(f"Pipeline config: {args.config_name}")
if sweep_dict:
logger.info(f"Sweep axes ({config_count} combinations):")
for key, vals in sweep_dict.items():
logger.info(f" {key}: {vals}")
else:
logger.info("No sweep axes — generating single config")
if override_dict:
logger.info("Overrides (applied to every config):")
for key, val in override_dict.items():
logger.info(f" {key} = {val!r}")
if args.dryrun:
logger.info(f"DRY RUN — would generate {config_count} config pair(s)")
return
# Load pipeline config via Hydra
base_config_path = "../configs"
with hydra.initialize(base_config_path, version_base=hydra.__version__):
base_cfg = hydra.compose(config_name=args.config_name)
logger.info(f"Loaded config: {args.config_name}")
# Generate configs
num_generated = apply_sweeper_and_save_configs(
base_cfg=base_cfg,
sweep_dict=sweep_dict,
override_dict=override_dict,
infer_output_dir=args.infer_dir_cfgs,
eval_output_dir=args.eval_dir_cfgs,
run_name=args.run_name,
start_index=0,
)
logger.info(f"Generated {num_generated} config pair(s)")
logger.info(f"Inference configs: {args.infer_dir_cfgs}")
logger.info(f"Evaluation configs: {args.eval_dir_cfgs}")
if __name__ == "__main__":
main()
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Declares allowed tools, Needs Python
Security
What our scanner noticed· Medium risk
- Large base64-encoded blobs
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Created by
- Ohad Mosafi
- Publisher
- NVIDIA (Official)
- Distributed by
- NVIDIA
- Source
- NVIDIA/skills · skills/bionemo-complexa-sweep · commit fce3cca
- Licence
- Apache License 2.0 (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE-APACHE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Ohad Mosafi or NVIDIA.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download complexa-sweep.zip