semi_cr.core.trial package

Submodules

semi_cr.core.trial.machine module

QUAM machine building for gate-sweep trials on the OPX1000.

This module turns a plain description of the physical wiring — which LF-FEM outputs drive which gates, and which output/input pair forms the readout path — into a QUAM machine (NQCPMachine) whose generated config can be loaded onto a quantum machine.

The wiring is expressed with small frozen dataclasses (OPXOutputRef, OPXInputRef, OPXWiring) that address OPX terminals explicitly. The longer-term goal is gate-addressed wiring: once station calibration is trustworthy, an OPXWiring.from_station(...) factory can route from a device gate through the station graph to the real OPX terminal (see semi_cr.apps.spincontrol.calibration_demo for the prototype of that routing).

class semi_cr.core.trial.machine.OPXOutputRef(controller_id: str, fem_id: int, port_id: int, upsampling_mode: str = 'pulse')[source]

Bases: object

Explicit address of an OPX1000 LF-FEM analog output.

controller_id: str
fem_id: int
port_id: int
upsampling_mode: str = 'pulse'
class semi_cr.core.trial.machine.OPXInputRef(controller_id: str, fem_id: int, port_id: int)[source]

Bases: object

Explicit address of an OPX1000 LF-FEM analog input.

controller_id: str
fem_id: int
port_id: int
class semi_cr.core.trial.machine.OPXWiring(gate_outputs: Mapping[str, OPXOutputRef], readout_output: OPXOutputRef, readout_input: OPXInputRef, sweep_gates: tuple[str, str] = ('P1', 'P2'))[source]

Bases: object

How gates and the readout path map onto OPX terminals.

sweep_gates names the two gates a 2D sweep varies; both must be keys of gate_outputs.

gate_outputs: Mapping[str, OPXOutputRef]
readout_output: OPXOutputRef
readout_input: OPXInputRef
sweep_gates: tuple[str, str] = ('P1', 'P2')
semi_cr.core.trial.machine.output_port_from_ref(ref: OPXOutputRef) LFFEMAnalogOutputPort[source]
semi_cr.core.trial.machine.input_port_from_ref(ref: OPXInputRef) LFFEMAnalogInputPort[source]
semi_cr.core.trial.machine.build_gate_channels(gate_ports: Mapping[str, LFFEMAnalogOutputPort]) dict[str, SingleChannel][source]

One sticky single channel per gate, so voltages hold between pulses.

class semi_cr.core.trial.machine.NQCPMachine(*, gate_set: 'GateSet', readout_resonator: 'InOutSingleChannel')[source]

Bases: QuamRoot

gate_set: GateSet
readout_resonator: InOutSingleChannel
config_settings: ClassVar[Dict[str, Any]] = None
generate_config() FullQuaConfig

Generate the QUA configuration from the QUAM object.

Returns:

A dictionary with the QUA configuration.

Note

This function collects all the nested QuamComponent objects and calls QuamComponent.apply_to_config on them.

get_attr_name(attr_val: Any) str

Get the name of an attribute that matches the value.

Parameters:

attr_val – The value of the attribute.

Returns:

The name of the attribute.

Raises:

AttributeError if not found.

get_attrs(follow_references: bool = False, include_defaults: bool = True) Dict[str, Any]

Get all attributes and corresponding values of this object.

Parameters:
  • follow_references – Whether to follow references when getting the value. If False, the reference will be returned as a string.

  • include_defaults – Whether to include attributes that have the default value.

Returns:

A dictionary of attribute names and values.

get_raw_value(attr: str) Any

Get the value of an attribute without following references.

If the value is a reference, the reference string is returned

get_reference(attr: str | None = None, relative_path: str | None = None) str | None

Get the reference path of this object or one of its attributes.

Parameters:
  • attr – The optional attribute to get the reference path for. If None, the reference path of the object itself is returned.

  • relative_path – The optional relative path to join with the reference path.

  • follow_chain – If True and attr is a reference, follow the reference chain to return the ultimate target reference. Default is False for backward compatibility. Only applies when attr is specified.

Returns:

The reference path of this object or the specified attribute.

Raises:

ValueError – If both attr and relative_path are specified, or if follow_chain is True but attr is not a reference.

Examples

We assume a QuamRoot object with a component “elem”. - elem.get_reference() == “#/elem” - elem.get_reference(attr=”child”) == “#/elem/child” - elem.get_reference(relative_path=”#./child”) == “#/elem/child” - elem.get_reference(relative_path=”#../child”) == “#/child” - elem.get_reference(relative_path=”#./child/grandchild”) == “#/elem/child/grandchild”

With follow_chain=True (if attr contains a reference to another reference): - elem.get_reference(attr=”chain_ref”, follow_chain=True) # returns ultimate target

get_root() QuamRootType

Get the QuamRoot object of this object, i.e. the object itself.

This QuamRoot function overrides the QuamBase function to return the object itself, rather than following the parent chain.

Returns:

The current QuamRoot object (self).

classmethod get_serialiser() AbstractSerialiser

Get the serialiser for the QuamRoot class, which is the JSONSerialiser.

This method can be overridden by subclasses to provide a custom serialiser.

get_unreferenced_value(attr: str) Any

Deprecated method. Use get_raw_value instead.

property inferred_id: str | int

Get the id of this object inferred from its id field or parent position.

If this object has a dataclass field named id with a concrete (non-reference, non-None) value, that value is returned. Otherwise the attribute name or key under which this object is stored in its parent is returned.

Returns:

The explicit id if set, or the attribute name / key in the parent as a string.

Raises:

AttributeError – If no explicit id is set and this object has no parent.

iterate_components(skip_elems: Sequence[QuamBase] | None = None) Generator[QuamBase, None, None]

Iterate over all QuamBase objects in this object, including nested objects.

Parameters:

skip_elems – A sequence of QuamBase objects to skip. This is used to prevent infinite loops when iterating over nested objects.

Returns:

A generator of QuamBase objects.

classmethod load(filepath_or_dict: str | Path | dict | None = None, validate_type: bool = True, fix_attrs: bool = True) QuamRootType

Load a QuamRoot object from a file.

Parameters:
  • filepath_or_dict – The path to the file/folder to load, or a dictionary. The dictionary would be the result from a call to QuamRoot.save() Can be omitted, in which case the serialiser will use the default state path, which is typically defined in the quam config file.

  • validate_type – Whether to validate the type of all attributes while loading.

  • fix_attrs – Whether attributes can be added to QuamBase objects that are not defined as dataclass fields.

Returns:

A QuamRoot object instantiated from the file/folder/dict.

parent: ClassVar[QuamBase]

Descriptor for the parent attribute of QuamBase.

This descriptor is used to ensure that the parent attribute of a QuamBase object is not overwritten. This is to prevent the following situation:

``` parent1 = QuamBase() parent2 = QuamBase()

child = QuamBase() child.parent = parent1 # This is fine child.parent = parent2 # This raises an AttributeError ```

print_summary(indent: int = 0)

Print a summary of the QuamBase object.

Parameters:

indent – The number of spaces to indent the summary.

save(path: Path | str | None = None, content_mapping: Dict[str, str] | None = None, include_defaults: bool | None = None, ignore: Sequence[str] | None = None)

Save the entire QuamRoot object to a file. This includes nested objects.

Parameters:
  • path – The path to save the file to. If None, the path will be extracted from the state_path attribute of the serialiser, which could be set by the quam config file or environment variable.

  • content_mapping – Optional mapping of component names to filenames. This can be used to save different parts of the QuamRoot object to different files.

  • include_defaults – Whether to include attributes that have the default value.

  • ignore – A list of components to ignore.

set_at_reference(attr: str, value: Any, allow_non_reference: bool = True)

Follow the reference of an attribute and set the value at the reference.

This method follows reference chains recursively. If an attribute contains a reference to another reference, both references are preserved while the ultimate target value is updated.

Parameters:
  • attr – The attribute to set the value at the reference of.

  • value – The value to set.

  • allow_non_reference – Whether to allow the attribute to be a non-reference. If True (default), non-reference attributes are allowed. If False, the attribute must be a reference or an error is raised.

Raises:
  • ValueError – If the attribute is not a reference and allow_non_reference is False.

  • ValueError – If the reference is invalid, e.g. “#./” since it has no attribute.

to_dict(follow_references: bool = False, include_defaults: bool = True) Dict[str, Any]

Convert this object to a dictionary.

Parameters:
  • follow_references – Whether to follow references when getting the value. If False, the reference will be returned as a string.

  • include_defaults – Whether to include attributes that have the default value.

Returns:

A dictionary representation of this object. Any QuamBase objects will be recursively converted to dictionaries.

Note

If the value of an attribute does not match the annotation, the “__class__” key will be added to the dictionary. This is to ensure that the object can be reconstructed when loading from a file.

semi_cr.core.trial.machine.build_machine(gate_ports: Mapping[str, LFFEMAnalogOutputPort], readout_output_port: LFFEMAnalogOutputPort, readout_input_port: LFFEMAnalogInputPort, *, readout_length: int) NQCPMachine[source]

Build the QUAM machine.

readout_length (ns) is a calibration parameter, not a property of the machine wiring, hence it is passed in rather than baked in here.

semi_cr.core.trial.machine.build_machine_from_wiring(wiring: OPXWiring, *, readout_length: int) NQCPMachine[source]

semi_cr.core.trial.shapes module

class semi_cr.core.trial.shapes.PulseShape[source]

Bases: ABC

Abstract base class for pulse shapes.

abstractmethod get_shape() ndarray[tuple[int, ...], dtype[floating]][source]

Get the value of the pulse shape. :return: The value of the pulse shape at the given time.

integrate()[source]
class semi_cr.core.trial.shapes.YizhiPulse(amplitude: float, steps: int)[source]

Bases: PulseShape

A pulse shape that linearly ramps up to a specified amplitude, then “asymptotically” goes on from that amplitude’s complement to the negative of that amplitude, then back to zero. Sort of like this:

get_shape() ndarray[tuple[int, ...], dtype[floating]][source]

Get the value of the pulse shape. :return: The value of the pulse shape at the given time.

integrate()

semi_cr.core.trial.sweep module

Gate-voltage sweep trials for the OPX1000.

Sweep1D sweeps one gate; Sweep2D sweeps two. Both run continuously (infinite QUA loop) and stream I/Q readout frames; consume them via stream().

The voltage trajectory is a YizhiPulse (ramp up, jump to the negative complement, ramp back) so the net charge on the sticky gate channels stays zero. Raw buffers therefore arrive in trajectory order; post-processing reorders them so frames are monotonic in voltage and line up with axes.

class semi_cr.core.trial.sweep.Sweep1D(qmm: QuantumMachinesManager, wiring: OPXWiring, *, gate: str | None = None, steps: int = 101, half_width: float = 0.05, readout_length: int = 10000)[source]

Bases: _GateSweep

Continuously sweep one gate voltage, streaming I/Q per step.

Frames have shape (steps,), complex I + jQ volts, ordered to match the (monotonic) voltage axis in axes.

property axes: tuple[ndarray, ...]

Coordinate arrays, one per dimension of the post-processed result, so consumers can plot without knowing sweep internals.

build_program() Program[source]

Build the (continuous, infinite-loop) QUA program for this trial.

post_process(raw) ndarray[source]

Turn raw stream buffers (keyed by stream_names) into a physical-units array, conventionally complex I + jQ volts.

Must be a pure function so it can be tested with canned arrays.

POLL_INTERVAL_S = 0.01

Delay between successive fetch attempts in stream().

fetch(job) dict[str, ndarray] | None

Pull one raw frame from the job, or None if not all streams have produced data yet.

machine_config()

The QUA machine configuration the program runs on.

stream() Generator[ndarray, None, None]

Run the trial and yield post-processed frames until closed.

Owns the quantum-machine/job lifecycle: opens the machine, executes the program, and on generator close cancels the job and closes the machine. Frames are polled; the same buffer may be yielded more than once — deduplication is the consumer’s concern.

stream_names: tuple[str, ...] = ('i', 'q')

Names of the output streams the QUA program saves. fetch() pulls exactly these; override in subclasses whose programs save different buffers.

class semi_cr.core.trial.sweep.Sweep2D(qmm: QuantumMachinesManager, wiring: OPXWiring, *, gates: tuple[str, str] | None = None, steps: int = 101, half_width: float = 0.05, readout_length: int = 10000)[source]

Bases: _GateSweep

Continuously raster two gate voltages, streaming an I/Q frame per raster.

Frames have shape (steps, steps), complex I + jQ volts. Row index follows axes[0] (the second sweep gate, outer loop), column index follows axes[1] (the first sweep gate, inner loop); both monotonic.

property axes: tuple[ndarray, ...]

Coordinate arrays, one per dimension of the post-processed result, so consumers can plot without knowing sweep internals.

build_program() Program[source]

Build the (continuous, infinite-loop) QUA program for this trial.

post_process(raw) ndarray[source]

Turn raw stream buffers (keyed by stream_names) into a physical-units array, conventionally complex I + jQ volts.

Must be a pure function so it can be tested with canned arrays.

POLL_INTERVAL_S = 0.01

Delay between successive fetch attempts in stream().

fetch(job) dict[str, ndarray] | None

Pull one raw frame from the job, or None if not all streams have produced data yet.

machine_config()

The QUA machine configuration the program runs on.

stream() Generator[ndarray, None, None]

Run the trial and yield post-processed frames until closed.

Owns the quantum-machine/job lifecycle: opens the machine, executes the program, and on generator close cancels the job and closes the machine. Frames are polled; the same buffer may be yielded more than once — deduplication is the consumer’s concern.

stream_names: tuple[str, ...] = ('i', 'q')

Names of the output streams the QUA program saves. fetch() pulls exactly these; override in subclasses whose programs save different buffers.

semi_cr.core.trial.trial module

The Trial abstraction: one methodology for acquiring data from the OPX1000.

A Trial wraps a particular way of getting data (e.g. a 1D or 2D gate sweep): it builds the QUA program, knows which output streams that program saves, and knows how to turn the raw stream buffers into a physically meaningful NumPy array.

Consumers only use Trial.stream():

for frame in trial.stream():        # Jupyter, scripts, ...
    plot(np.abs(frame))

or hand the generator to a background thread for GUI/”on-line” use. Closing the generator (.close(), break, or garbage collection) cancels the QUA job and closes the quantum machine.

Currently only continuous (infinite-loop) acquisition is supported; a oneshot mode will be revisited when a concrete use case exists, since oneshot QUA programs are structured differently.

class semi_cr.core.trial.trial.Trial(qmm: QuantumMachinesManager)[source]

Bases: ABC

One methodology for acquiring data (e.g. a 1D or 2D gate sweep).

Subclasses provide the QUA program, the machine configuration it runs on, and pure post-processing from raw stream buffers to a NumPy array. The base class provides the acquisition machinery (fetch(), stream()).

By convention post_process() returns complex I + jQ in volts; consumers pick the component or magnitude they care about.

stream_names: tuple[str, ...] = ('i', 'q')

Names of the output streams the QUA program saves. fetch() pulls exactly these; override in subclasses whose programs save different buffers.

POLL_INTERVAL_S = 0.01

Delay between successive fetch attempts in stream().

abstractmethod build_program() Program[source]

Build the (continuous, infinite-loop) QUA program for this trial.

abstractmethod machine_config() FullQuaConfig[source]

The QUA machine configuration the program runs on.

abstractmethod post_process(raw: Mapping[str, ndarray]) ndarray[source]

Turn raw stream buffers (keyed by stream_names) into a physical-units array, conventionally complex I + jQ volts.

Must be a pure function so it can be tested with canned arrays.

abstract property axes: tuple[ndarray, ...]

Coordinate arrays, one per dimension of the post-processed result, so consumers can plot without knowing sweep internals.

fetch(job) dict[str, ndarray] | None[source]

Pull one raw frame from the job, or None if not all streams have produced data yet.

stream() Generator[ndarray, None, None][source]

Run the trial and yield post-processed frames until closed.

Owns the quantum-machine/job lifecycle: opens the machine, executes the program, and on generator close cancels the job and closes the machine. Frames are polled; the same buffer may be yielded more than once — deduplication is the consumer’s concern.

class semi_cr.core.trial.trial.ThreadedTrialRunner(trial: Trial, on_frame: Callable[[ndarray], None])[source]

Bases: object

Consume a trial’s stream on a daemon thread for “on-line” (GUI) use.

Frames are delivered to on_frame from the background thread; the callback is responsible for its own thread safety. stop() closes the stream, which cancels the QUA job and closes the quantum machine.

Trial parameters changed? Stop this runner and start a new one with a freshly built trial.

property is_running: bool
start() None[source]
stop() None[source]

Module contents

Experiment trials for the spin qubit lab

A trial is one self-contained methodology for acquiring data: it builds the QUA program, knows which streams that program saves, and turns the raw stream buffers into physically meaningful arrays. You drive everything through the Trial abstraction and never touch QUA directly.

Contents

Example:

Build a 1D sweep and consume readout frames as they arrive:

from semi_cr.core.trial.sweep import Sweep1D

trial = Sweep1D(...)              # configure gate, span, wiring
for frame in trial.stream():      # infinite generator of I/Q frames
    plot(np.abs(frame))
    if done:
        break                     # closing the generator cancels the job

Note

Sweeps run continuously (an infinite QUA loop) and stream frames until the generator is closed. See Trial for the full lifecycle.