The Standard Starting Point
Every parameter study starts the same way. You have a function that takes inputs and returns outputs. You want to know how the outputs change as the inputs vary. So you write a loop:
results = []
for alpha in [0.01, 0.05, 0.1, 0.5, 1.0]:
for beta in [0.5, 1.0, 2.0, 5.0]:
output = simulate(alpha=alpha, beta=beta)
results.append({"alpha": alpha, "beta": beta, **output})
Twenty combinations. Runs in seconds. Works fine.
Then you need more resolution, or more dimensions:
import numpy as np
for alpha in np.logspace(-3, 1, 9): # 0.001 → 10, 9 steps
for beta in np.logspace(-1, 1, 7): # 0.1 → 10, 7 steps
for gamma in np.linspace(0.0, 1.0, 4):
for seed in range(5):
...
Now it is 1,260 combinations, each at several seconds. You are looking at hours. And that is before you add the next dimension your model needs.
What Breaks Before The Compute Does
The compute wall is the obvious problem. But by the time you hit it, three other things have already degraded:
Result traceability is manual. You are building a list in memory, keying each result back to inputs by hand, and hoping nothing fails midway. If the process dies at combination 800 out of 1,260, you have partial results with no clean record of where you left off.
Re-runs are expensive to design. You notice a bug in the simulation after the sweep finishes. Do you re-run everything? Just the failed combinations? The ones with gamma > 0.5? The loop gives you no native answer. You write more code to answer a question the sweep itself should already handle.
Results live in volatile memory. A list in a notebook is not a durable record. Restart the kernel, close the terminal, or lose a connection and the outputs are gone. The sweep ran; the results are not.
These are not compute problems. They are infrastructure problems that accumulate well below any hardware limit.
What The Cloud-Backed Version Looks Like
Here is the same sweep submitted through Combinate:
import combinate as cb
def simulate(alpha: float, beta: float, gamma: float, seed: int) -> dict:
# your existing simulation function — nothing changes here
...
import numpy as np
result = cb.sweep(
simulate,
params={
"alpha": np.logspace(-3, 1, 9).tolist(), # 0.001 → 10, 9 steps
"beta": np.logspace(-1, 1, 7).tolist(), # 0.1 → 10, 7 steps
"gamma": np.linspace(0.0, 1.0, 4).tolist(),
"seed": list(range(5)),
},
)
print(result.describe())
The function definition is unchanged. The parameter specification is a dictionary of lists rather than nested for statements. The output comes back as a SweepResult you can inspect immediately.
What Actually Changed
Looking at that code, it would be easy to assume this is just a neater loop with cloud execution bolted on. The execution model is different in three specific ways that matter.
Each combination runs as an independent task. There is no shared process state between parameter combinations. A failure at combination 800 does not poison combinations 801 through 1,260. You get a per-task status for every combination, and failed tasks are inspectable without affecting the others.
# see what failed
for task in result.failed_tasks:
print(task.task_id, task.error_summary)
# inspect a successful output
for task in result.succeeded_tasks:
print(task.parameter_values, task.inline_output)
The sweep has a durable ID. Every submission gets a sweep_id that persists independently of the notebook or process that submitted it. You can share it with a teammate or reference it in a support request. The record of the run exists outside of your local environment.
print(result.sweep_id)
# → swp_01j8ktx3p2...
Execution fans out across workers. Your 1,260 combinations do not run sequentially. They run in parallel across cloud workers, bounded by your plan's concurrency limit. A sweep that takes three hours single-threaded on a laptop comes back in minutes.
What Does Not Change
This matters as much as what does.
Your simulation function is still plain Python. No special base class, no decorator, no framework-specific conventions. If it runs in a local Python environment, it runs in a cloud worker.
Your inputs are still Python primitives — lists of floats, integers, strings, booleans. No configuration files, no schema registration.
Your result inspection is still Python — a list of task objects you can iterate, filter, and feed into a pandas DataFrame or a plotting library:
import pandas as pd
rows = [
{"alpha": t.parameter_values["alpha"], "beta": t.parameter_values["beta"], **t.inline_output}
for t in result.succeeded_tasks
]
df = pd.DataFrame(rows)
The cloud execution layer is transparent to the model. You are not rewriting a simulation to use a new framework. You are adding structured fan-out and a durable record to a function you already have.
What Still Belongs Local
Not every sweep should move to the cloud.
If you are debugging a model — iterating on the simulation function itself, running three or five combinations to see whether the outputs look sensible — local execution is the right choice. The feedback loop is faster and you do not need persistence during active development.
The signal that you have hit the boundary: the sweep takes longer than one working session to complete, or you find yourself tracking results across runs manually because re-running everything is impractical.
At that point, the overhead cost of local loop management starts to exceed the overhead cost of structured submission.
Combinate is currently in private beta for Python simulation engineers. If this describes your workflow, join the beta list.