Tutorial 4: Parametric Sweep Options#
| Author: | Zachary Parrott |
| Purpose: | Demonstrate all available sweep types in the HFSS Optimetrics framework, and how they integrate with the pyEPR workflow. |
Series navigation: ← Tutorial 3: Circuit QED Parameters | Tutorial 5: Generic Junction Potential & Fluxonium →
Introduction: Why parametric sweeps?#
In superconducting qubit design, you rarely simulate a single geometry once. Instead, you sweep design variables — junction inductance \(L_J\), capacitor gap width, resonator length — to map out how the Hamiltonian parameters (qubit frequency, anharmonicity, dispersive shift) depend on geometry. This is the design space exploration step that precedes fabrication.
Two approaches to sweeping#
1. Python loop (Tutorial 1 approach): Set a design variable with pinfo.design.set_variable(var, val), call pinfo.setup.analyze(), and iterate. This is flexible (any Python logic), but runs HFSS serially.
2. HFSS Optimetrics (this tutorial): Define a parametric sweep inside HFSS and let it manage the parallel or serial solution. This integrates with the HFSS licensing and HPC infrastructure, and is the standard approach for production sweeps.
Key settings for EPR compatibility#
Setting |
Value for EPR |
Reason |
|---|---|---|
|
Required |
EPR analysis needs the field solution for every variation |
|
Only when geometry is unchanged |
Valid for \(L_J\) sweeps (changes boundary condition, not mesh); must be False for geometric variables |
|
Recommended |
Falls back to full solve if copied mesh fails |
Warning: If you forget save_fields=True, pyEPR will fail silently on variations without saved field data. Always verify this setting before running a large sweep.
%load_ext autoreload
%autoreload 2
import pyEPR as epr
Connect to the example project#
We connect to the same single-transmon design from Tutorial 1. This design has one junction with inductance variable Lj_1 and geometric variables such as height (resonator height) and pad_gap (qubit capacitor gap).
project_path = '..\\_example_files'
project_name = 'pyEPR_tutorial1'
design_name = '1. single_transmon'
pinfo = epr.ProjectInfo(project_path = project_path,
project_name = project_name,
design_name = design_name)
# Qubit junction
pinfo.junctions['junction'] = {'Lj_variable' : 'Lj_1',
'rect' : 'rect_jj1',
'line' : 'line_jj1'}
pinfo.validate_junction_info()
# Already existing setup
setup_name = 'Setup1'
Sweep types#
HFSS Optimetrics supports six parametric sweep types, each suited to a different design-space exploration strategy. Below we create one sweep of each type for the tutorial design.
1. Single value#
Simulates a single specified value. Useful for:
Verifying that a specific design point (e.g., the target \(L_J\) from your design spec) is simulated and saved
Quick one-off re-simulations after a design change
Parameters: swp_params = 'value' (a string with unit, e.g., '12nH')
opti_name = "single_value"
swp_params = ('12nH')
swp_variable = 'Lj_1'
sweep_settings = dict(
variable = swp_variable,
swp_type = 'single_value',
swp_params = swp_params,
name = opti_name,
setup_name = setup_name,
save_fields = True,
copy_mesh = True,
solve_with_copied_mesh_only = False,
setup_type = 'parametric'
)
# setup_name=None will use the first setup
if opti_name not in pinfo.design.optimetrics.get_setup_names():
opti_setup = pinfo.design.optimetrics.create_setup(**sweep_settings)
2. Linear step#
Sweeps linearly from start to stop with step size step. Use for:
Geometric variables where you want uniform sampling (e.g., varying a gap from 80 to 120 μm in 5 μm steps)
When you have a clear physical reason to believe the response is approximately linear
Parameters: swp_params = (start, stop, step) — all strings with units.
Note: For geometric variables like height, copy_mesh=False is mandatory — the mesh must be re-generated for each new geometry.
opti_name = "linear_step"
swp_variable = 'height'
swp_params = ('30mm','36mm','1mm')
# 'height' is a geometric variable so we cannot copy the mesh between passes
sweep_settings = dict(
variable = swp_variable,
swp_type = 'linear_step',
swp_params = swp_params,
name = opti_name,
setup_name = setup_name,
save_fields = True,
copy_mesh = False,
solve_with_copied_mesh_only = False,
setup_type = 'parametric'
)
if opti_name not in pinfo.design.optimetrics.get_setup_names():
opti_setup = pinfo.design.optimetrics.create_setup(**sweep_settings)
3. Linear count#
Sweeps linearly from start to stop, automatically computing the step size to give exactly count points (including both endpoints). Use for:
When you care more about total number of simulations than step size
Budget-constrained sweeps: fix
countto control simulation time
Parameters: swp_params = (start, stop, count) — count is an integer.
opti_name = "linear_count"
swp_variable = 'pad_gap'
swp_params = ('80um', '120um', 5)
sweep_settings = dict(
variable = swp_variable,
swp_type = 'linear_count',
swp_params = swp_params,
name = opti_name,
setup_name = setup_name,
save_fields = True,
copy_mesh = False,
solve_with_copied_mesh_only = False,
setup_type = 'parametric'
)
if opti_name not in pinfo.design.optimetrics.get_setup_names():
opti_setup = pinfo.design.optimetrics.create_setup(**sweep_settings)
4. Decade count#
Logarithmic sweep (base 10): generates count points per decade between start and stop. Use for:
Variables that span orders of magnitude (e.g., \(L_J\) from 10 nH to 1 μH)
Exploring qubit designs across very different frequency regimes
When the physical response scales logarithmically (e.g., \(\omega_q \propto 1/\sqrt{L_J}\))
Parameters: swp_params = (start, stop, count_per_decade).
opti_name = "decade_count"
swp_variable = 'Lj_1'
swp_params = ('12nH', '100nH', 5)
sweep_settings = dict(
variable = swp_variable,
swp_type = 'decade_count',
swp_params = swp_params,
name = opti_name,
setup_name = setup_name,
save_fields = True,
copy_mesh = True,
solve_with_copied_mesh_only = False,
setup_type = 'parametric'
)
if opti_name not in pinfo.design.optimetrics.get_setup_names():
opti_setup = pinfo.design.optimetrics.create_setup(**sweep_settings)
5. Octave count#
Logarithmic sweep (base 2): count points per octave (factor of 2 in the variable). Use for:
Variables related to frequency where octave spacing is physically natural
Closely related to decade count but with finer resolution for moderate ranges
Parameters: swp_params = (start, stop, count_per_octave).
opti_name = "octave_count"
swp_variable = 'Lj_1'
swp_params = ('12nH', '100nH', 8)
sweep_settings = dict(
variable = swp_variable,
swp_type = 'octave_count',
swp_params = swp_params,
name = opti_name,
setup_name = setup_name,
save_fields = True,
copy_mesh = True,
solve_with_copied_mesh_only = False,
setup_type = 'parametric'
)
if opti_name not in pinfo.design.optimetrics.get_setup_names():
opti_setup = pinfo.design.optimetrics.create_setup(**sweep_settings)
6. Exponential count#
Exponential (\(e\)-based) sweep: count points total, spaced exponentially. Use for:
When the variable itself enters exponentiated (e.g., critical current \(I_c \propto e^{-d/\xi}\) in tunneling junctions)
Fine-grained sampling near one end of the range
Parameters: swp_params = (start, stop, count).
opti_name = "exponential_count"
swp_variable = 'Lj_1'
swp_params = ('12nH', '20nH', 4)
sweep_settings = dict(
variable = swp_variable,
swp_type = 'exponential_count',
swp_params = swp_params,
name = opti_name,
setup_name = setup_name,
save_fields = True,
copy_mesh = True,
solve_with_copied_mesh_only = False,
setup_type = 'parametric'
)
if opti_name not in pinfo.design.optimetrics.get_setup_names():
opti_setup = pinfo.design.optimetrics.create_setup(**sweep_settings)
Sweep from file#
For maximum flexibility — arbitrary, non-uniform sets of parameter values — you can provide the sweep points in a CSV or plain-text file. This is the recommended approach when:
Values are determined by an external optimisation loop (e.g., Bayesian optimisation)
You want to reproduce a specific set of literature values
The sweep is irregular (e.g., dense near a resonance, sparse elsewhere)
The file format is one value per line (with unit string if needed, depending on the HFSS version).
import os
cwd = os.getcwd()
opti_name = "param_file"
swp_variable = 'Lj_1'
filepath = cwd[:-len("_tutorial_notebooks")] + "_example_files\\"
filename = "Lj_sweep_values.csv"
swp_params = filepath + filename
sweep_settings = dict(
variable = swp_variable,
swp_type = 'file',
swp_params = swp_params,
name = opti_name,
setup_name = setup_name,
save_fields = True,
copy_mesh = True,
solve_with_copied_mesh_only = False,
setup_type = 'parametric_file'
)
if opti_name not in pinfo.design.optimetrics.get_setup_names():
opti_setup = pinfo.design.optimetrics.create_setup(**sweep_settings)
Running the sweeps and running EPR analysis#
Once the Optimetrics setups are created, solve a single HFSS setup and one Optimetrics sweep:
analysis_setup = pinfo.design.get_setup(setup_name)
analysis_setup.solve(setup_name)
pinfo.design.optimetrics.solve_setup("param_file")
Running EPR analysis after a parametric sweep#
After the sweep completes (all variations solved with field solutions saved), run the EPR analysis exactly as in Tutorial 1:
# Create distributed analysis and run EPR on all saved variations
eprh = epr.DistributedAnalysis(pinfo)
eprh.do_EPR_analysis() # Iterates over all HFSS variations automatically
# Quantum Hamiltonian analysis across the sweep
epra = epr.QuantumAnalysis(eprh.data_filename)
epra.analyze_all_variations(cos_trunc=8, fock_trunc=15)
# Plot results vs. sweep variable
epra.plot_hamiltonian_results(swp_variable='Lj_1')
The plot_hamiltonian_results function automatically handles numeric sorting of variation labels, so a sweep from Lj_1 = 6 nH to 12 nH in 6 steps will display in the correct numerical order.
pinfo.disconnect()