writing-experiment-scripts
Skill69 starsChanged 7 months agoApache-2.0View on GitHub
Author new experiment scripts that are compatible with the lab system's auto-discovery. Use when the user asks to create a new experiment type, add a custom measurement, or write a Python script that should be discoverable via the `lab` and `run_experiment` tools. Covers required function signatures, type hints, docstrings, and return formats.
In this repository5 other files it gives its agents.
- analysis-scriptsSkill
- experiment-executionSkill
- vlm-configurationSkill
- workflow-executionSkill
- workflow-planningSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 3 are distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 220
After creating or modifying an experiment script, always validate it using the CLI tool:
- Distinctive: line 288
After validation passes, verify the experiment appears in the list:
- Distinctive: line 295
Before running your script:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Writing Experiment Scripts
How to create experiment scripts compatible with the lab system.
Quick Reference
| Requirement | Details |
| Location | Scripts directory (see system prompt) |
| Function | ONE public function per file (no _ prefix) |
| Type hints | Required with Annotated bounds |
| Docstring | Google-style with Args/Returns |
| Return | Dict with status, results, arrays, plots |
Function Signature
from typing import Annotated
def experiment_name(
param1: Annotated[float, (min, max)] = default,
param2: Annotated[int, (min, max)] = default,
target: str = "qubit_0",
) -> dict:
"""Short description of the experiment.
Args:
param1: Description with units
param2: Description with units
target: Target qubit identifier
Returns:
Measurement results with data and plots
"""
# Implementation
return {...}Return Format
Success
{
"status": "success",
"results": {
"peak_frequency": 5042.3, # Scalar results
"linewidth_mhz": 1.2,
},
"arrays": {
"frequencies": [4900.0, 4902.0, ...], # Array data
"amplitudes": [0.01, 0.02, ...],
},
"plots": [
{"name": "spectrum", "format": "png", "data": "base64..."},
],
"metadata": {
"target": "qubit_0",
"experiment_type": "qubit_spectroscopy",
},
}Failure
{
"status": "failed",
"error": "No peak found in frequency range"
}Parameter Types
Supported Types
float- Floating point numbersint- Integersstr- Strings (no range validation)bool- Boolean flagslist- Lists (no range validation)
Range Constraints
from typing import Annotated
# Float with range [0.0, 100.0]
amplitude: Annotated[float, (0.0, 100.0)] = 50.0
# Int with range [1, 10000]
num_averages: Annotated[int, (1, 10000)] = 1000Important: Parameters without defaults are required. Parameters with defaults are optional.
Complete Example
"""Simulated qubit spectroscopy experiment."""
from typing import Annotated
import numpy as np
import base64
import io
def qubit_spectroscopy(
start: Annotated[float, (3000.0, 8000.0)] = 4900.0,
stop: Annotated[float, (3000.0, 8000.0)] = 5100.0,
step: Annotated[float, (0.1, 100.0)] = 2.0,
num_avs: Annotated[int, (1, 10000)] = 1000,
target: str = "qubit_0",
) -> dict:
"""Qubit spectroscopy frequency sweep.
Performs a frequency sweep to find the qubit resonance frequency.
Args:
start: Start frequency in MHz
stop: Stop frequency in MHz
step: Frequency step in MHz
num_avs: Number of averages
target: Target qubit identifier
Returns:
Measurement results with frequency estimate and data
"""
# Generate frequency array
frequencies = np.arange(start, stop + step, step)
# Simulate measurement (replace with real hardware calls)
center_freq = (start + stop) / 2
linewidth = 3.0
signal = 1.0 / (1.0 + ((frequencies - center_freq) / linewidth) ** 2)
noise = np.random.normal(0, 0.05, len(frequencies))
magnitude = signal + noise
# Find peak
peak_idx = np.argmax(magnitude)
frequency_guess = float(frequencies[peak_idx])
# Generate plot (optional)
plots = []
try:
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(frequencies, magnitude, 'b-')
ax.axvline(frequency_guess, color='r', linestyle='--')
ax.set_xlabel('Frequency (MHz)')
ax.set_ylabel('Magnitude')
ax.set_title(f'Spectroscopy - {target}')
buf = io.BytesIO()
fig.savefig(buf, format='png', dpi=100)
buf.seek(0)
plots.append({
"name": "spectroscopy",
"format": "png",
"data": base64.b64encode(buf.read()).decode('ascii')
})
plt.close(fig)
except ImportError:
pass
return {
"status": "success",
"results": {
"frequency_guess": frequency_guess,
"linewidth_mhz": linewidth,
},
"arrays": {
"frequencies": frequencies.tolist(),
"magnitude": magnitude.tolist(),
},
"plots": plots,
"metadata": {
"target": target,
"experiment_type": "qubit_spectroscopy",
},
}Common Patterns
Sweep Experiment
def frequency_sweep(
start: Annotated[float, (1000.0, 10000.0)] = 4000.0,
stop: Annotated[float, (1000.0, 10000.0)] = 6000.0,
step: Annotated[float, (0.1, 100.0)] = 1.0,
) -> dict:Time-Domain Measurement
def t1_measurement(
delay_start: Annotated[float, (0.0, 1000.0)] = 0.0,
delay_stop: Annotated[float, (0.0, 1000.0)] = 100.0,
num_points: Annotated[int, (10, 500)] = 50,
num_averages: Annotated[int, (100, 50000)] = 1000,
) -> dict:Amplitude Calibration
def rabi_amplitude(
amp_start: Annotated[float, (0.0, 1.0)] = 0.0,
amp_stop: Annotated[float, (0.0, 1.0)] = 1.0,
num_points: Annotated[int, (10, 200)] = 50,
drive_frequency: Annotated[float, (3000.0, 8000.0)] = 5000.0,
) -> dict:Validation
After creating or modifying an experiment script, always validate it using the CLI tool:
qca experiments validate <path-to-script> --humanThis command checks:
- File exists and is a valid Python file
- Contains a public function (no underscore prefix)
- Function has
-> dictreturn type annotation - Parameters have type hints (ideally with
Annotatedranges) - Syntax is valid
Example output (valid script):
Validation Result: VALID
File: scripts/my_experiment.py
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Check ┃ Status ┃ Details ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ file_exists │ ✓ PASS │ File exists │
│ python_file │ ✓ PASS │ File has .py extension │
│ not_private │ ✓ PASS │ File is public (no underscore prefix) │
│ syntax_valid │ ✓ PASS │ Python syntax is valid │
│ has_public_function │ ✓ PASS │ Found public function: my_experiment │
│ return_type_dict │ ✓ PASS │ Return type is dict │
│ has_docstring │ ✓ PASS │ Function has docstring │
│ has_typed_parameters │ ✓ PASS │ Found 3 typed parameter(s) │
│ has_annotated_ranges │ ✓ PASS │ 3 parameter(s) have range constraints │
│ optional_parameters │ ✓ PASS │ 3 optional parameter(s) with defaults │
└────────────────────────┴──────────┴─────────────────────────────────────────┘
Experiment Schema:
Name: my_experiment
Description: My experiment description.
Module Path: /path/to/scripts/my_experiment.py
Parameters:
┏━━━━━━━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━┓
┃ Name ┃ Type ┃ Required ┃ Default ┃ Range ┃
┡━━━━━━━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━┩
│ amplitude │ float │ No │ 0.5 │ 0.0 to 1.0 │
│ num_points │ int │ No │ 51 │ 10 to 200 │
│ target │ str │ No │ qubit_0 │ - │
└───────────────┴───────┴──────────┴─────────┴──────────────┘Example output (invalid script):
Validation Result: INVALID
File: scripts/bad_experiment.py
┏━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Check ┃ Status ┃ Details ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ file_exists │ ✓ PASS │ File exists │
│ python_file │ ✓ PASS │ File has .py extension │
│ not_private │ ✓ PASS │ File is public │
│ syntax_valid │ ✓ PASS │ Python syntax is valid │
│ has_public_function │ ✓ PASS │ Found public function: my_func │
│ return_type_dict │ ✗ FAIL │ No return type annotation │
└────────────────────────┴──────────┴─────────────────────────────────┘
Errors:
• Function must have return type annotation '-> dict'After validation passes, verify the experiment appears in the list:
qca experiments list --humanChecklist
Before running your script:
- File is in the scripts directory
- Only ONE public function (no
_prefix) - All parameters have type hints with
Annotated - All parameters have default values (or are intentionally required)
- Function has Google-style docstring
- Returns dict with
statuskey - Arrays are converted to lists (
.tolist()) - Plots are base64-encoded PNG or Plotly JSON
- Run
qca experiments validate <path> --humanto verify - Run
qca experiments list --humanto confirm discovery
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

