Automate file sweeps with GridSpec variant declarations
Introduction
In Sweeping Over External Data, you built FileSweepParameter objects by hand – listing each .jshd path explicitly. That works well for a few files, but scales poorly. A real climate project might have 24 monthly files per scenario across three SSP pathways: 72 paths to manage, and they all need to switch together per scenario.
GridSpec variants solve this. You declare variant axes (e.g., pattern, scenario) in your grid.yaml, use template_path placeholders in file entries, and let variant_sweep() generate the entire CompoundSweepParameter automatically.
This tutorial demonstrates:
Writing a grid.yaml with variant declarations
Exploring the variant API (file_mappings, file_mappings_for, variant_sweep)
Combining variant_sweep() with ConfigSweepParameter for a cartesian sweep
Using a .josh.j2 model template with GridSpec.template_vars
A grid.yaml with variants has three sections: grid geometry, variant axes, and file inventory. Here we declare a pattern axis with three values – each corresponding to a soil quality .jshd file created in the preprocessing tutorial:
file_mappings resolves template_path entries using each axis’s default value:
for name, path in grid.file_mappings.items():print(f" {name}: {path.name}")
soil_quality: soil_quality_gradient.jshd
Scenario-specific mappings
file_mappings_for() resolves for a specific variant value:
for pattern in grid.variants["pattern"]["values"]: mappings = grid.file_mappings_for(pattern=pattern)print(f" pattern={pattern}: {mappings['soil_quality'].name}")
Each label corresponds to one scenario. All inner FileSweepParameter objects are zipped – they switch together, not cartesian.
Step 2: Configure the Sweep
Now combine variant_sweep() with a ConfigSweepParameter. The variant axis and config parameter expand as a cartesian product: 3 patterns x 3 growth rates = 9 jobs.
from joshpy.jobs import JobConfig, SweepConfig, ConfigSweepParameterfrom joshpy.strategies import CartesianStrategySOURCE_TEMPLATE = Path("../../examples/variant_sweep.josh.j2")CONFIG_TEMPLATE = Path("../../examples/templates/sweep_config.jshc.j2")config = JobConfig(# Model template -- grid geometry injected from GridSpec source_template_path=SOURCE_TEMPLATE, template_vars={**grid.template_vars,"simulation_name": "Main", },# Config template -- maxGrowth swept per job template_path=CONFIG_TEMPLATE,# Data files -- defaults from GridSpec, overridden per scenario by sweep file_mappings=grid.file_mappings, simulation="Main", replicates=2,# Sweep: 3 growth rates x 3 soil patterns = 9 jobs x 2 replicates = 18 runs sweep=SweepConfig( config_parameters=[ ConfigSweepParameter(name="maxGrowth", values=[5, 10, 15]), ], compound_parameters=[ grid.variant_sweep("pattern"), ], strategy=CartesianStrategy(), ),)n_jobs =len(config.sweep.config_parameters[0].values) *len(grid.variants["pattern"]["values"])print(f"Config parameter: maxGrowth = {config.sweep.config_parameters[0].values}")
if results.failed >0: errors = []for job, result in results:ifnot result.success: error_msg = result.stderr.strip() if result.stderr else"No error message" errors.append(f"Job {job.run_hash}: {error_msg[:500]}") error_detail ="\n".join(errors)raiseRuntimeError(f"Sweep failed: {results.failed} job(s) failed\n\n{error_detail}")
Step 4: Load Results
manager.load_results()
Loading patch results from: /tmp/variant_sweep_{run_hash}_{replicate}.csv
Loaded 34782 rows from variant_sweep_63b30c830241_0.csv
Loaded 34782 rows from variant_sweep_63b30c830241_1.csv
Loaded 34782 rows from variant_sweep_e41f328be84d_0.csv
Loaded 34782 rows from variant_sweep_e41f328be84d_1.csv
Loaded 34782 rows from variant_sweep_9f582a08bf21_0.csv
Loaded 34782 rows from variant_sweep_9f582a08bf21_1.csv
Loaded 34782 rows from variant_sweep_e3703e25c1d9_0.csv
Loaded 34782 rows from variant_sweep_e3703e25c1d9_1.csv
Loaded 34782 rows from variant_sweep_7c94c563a360_0.csv
Loaded 34782 rows from variant_sweep_7c94c563a360_1.csv
Loaded 34782 rows from variant_sweep_9797a10bb51c_0.csv
Loaded 34782 rows from variant_sweep_9797a10bb51c_1.csv
Loaded 34782 rows from variant_sweep_c45d4d6aa44e_0.csv
Loaded 34782 rows from variant_sweep_c45d4d6aa44e_1.csv
Loaded 34782 rows from variant_sweep_270a6ddc1b28_0.csv
Loaded 34782 rows from variant_sweep_270a6ddc1b28_1.csv
Loaded 34782 rows from variant_sweep_c7038b2fe88b_0.csv
Loaded 34782 rows from variant_sweep_c7038b2fe88b_1.csv
Results:
Jobs in sweep: 9
Jobs with results loaded: 9
Total rows loaded: 626076
626076
from joshpy.diagnostics import SimulationDiagnosticsdiag = SimulationDiagnostics(manager.registry)diag.plot_comparison("average_height", group_by="pattern", title="Tree Height by Soil Quality Pattern",)
Figure 1: Tree height trajectories grouped by soil quality pattern
Compare growth rates (config parameter)
diag.plot_comparison("average_height", group_by="maxGrowth", title="Tree Height by Max Growth Rate",)
Figure 2: Tree height trajectories grouped by maximum growth rate
Cross-tabulation via SQL
The variant label (pattern) and config parameter (maxGrowth) are both stored in config_parameters, so you can query the full cartesian product:
result = manager.registry.query(""" SELECT cp.pattern, cp.maxGrowth, MAX(cd.step) as final_step, AVG(cd.average_height) as mean_final_height FROM cell_data cd JOIN config_parameters cp ON cd.run_hash = cp.run_hash WHERE cd.step = (SELECT MAX(step) FROM cell_data) GROUP BY cp.pattern, cp.maxGrowth ORDER BY cp.pattern, cp.maxGrowth""")result.df()
# Paths declared once in grid.yaml, resolved automaticallygrid = GridSpec.from_yaml("data/grids/dev_fine/grid.yaml")sweep=SweepConfig( compound_parameters=[ grid.variant_sweep("pattern"), # one line ],)
The GridSpec approach scales to any number of template_path files per axis. With 24 monthly climate files across 3 SSP scenarios, variant_sweep("scenario") generates 24 zipped FileSweepParameter objects – all switching together per scenario – in a single call.
When to use which
Approach
Best for
FileSweepParameter (manual)
Ad-hoc sweeps, files without a naming convention
variant_sweep() (GridSpec)
Structured projects where files follow {axis} naming patterns
variant_sweep(axes=[...])
Multi-axis cross-products (e.g., scenario x GCM)
Cleanup
import osmanager.cleanup()manager.close()for f in [REGISTRY_PATH, f"{REGISTRY_PATH}.wal"]:if os.path.exists(f): os.remove(f)if grid_yaml_path.exists(): grid_yaml_path.unlink()