rydiqule.sensor.Sensor

class rydiqule.sensor.Sensor(states: int | Sequence[int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...]], *couplings: Dict, vP: float | None = None)[source]

Bases: object

Class that contains minimum information necessary to run the solvers.

Consider this class the theorist’s interface to the solvers. It requires nearly complete, explicit specification of inputs. This allows for very fine control of the solvers, including the ability to solve systems that are not entirely physical.

__init__(states: int | Sequence[int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...]], *couplings: Dict, vP: float | None = None) None[source]

Initializes the Sensor with the specified basis .

Can be specified as either an integer number of states (which will automatically label the states [0,...,basis_size]) or list of valid state specifications.

Parameters:
  • states (int or list of statespec) – The specification of the basis size and labelling for a new Sensor. Can be either a integer or a list of valid state specifications. If specified as an integer n, the created Sensor will have n states labelled as 0,...n. Valid state specifications are tuples containing either numbers or strings, optionally a list of the same. In the case of a list, the tuple will be converted into a list of tuples with each corresponding to one element in the list element element of the specification. See the Examples section for examples on how to specify groups of states. Note that with only a single statespec, it must be passed as an element of a list

  • *couplings (tuple(dict)) – Couplings dictionaries to pass to add_couplings() on sensor construction.

  • vP (float, optional) – Most probable speed of the 3D Maxwell-Boltzmann distribution of the ensemble. Calculated as \(\sqrt{2kT/m}\) and is provided in units of m/s. This parameter is only necessary to perform Doppler-broadened solves.

Raises:
  • RydiquleError – If basis is not an integer or iterable.

  • RydiquleError – If any of the state label specifications of basis are the wrong type.

Examples

Providing an integer will define a sensor with the given basis size, labelled with ascending integers.

>>> s = rq.Sensor(3)
>>> s.states
[0, 1, 2]

States can also be defined with a list of integers:

>>> s = rq.Sensor([0, 1, 2])
>>> s.states
[0, 1, 2]

States can also be strings

>>> s = rq.Sensor([0, 'e1', 'e2'])
>>> s.states
[0, 'e1', 'e2']

States can be defined with tuples. These can be thought of as quantum numbers, although no physics around quantum numbers exist in Sensor, so the values are completely general.

>>> s = rq.Sensor([(1,-1),(1,1)])
>>> s.states
[(1, -1), (1, 1)]

States can be specified in groups with a “state specification”, which will expand lists of quantum numbers

>>> statespec = (1,[-1,0,1])
>>> s = rq.Sensor([(0,0), statespec])
>>> s.states
[(0, 0), (1, -1), (1, 0), (1, 1)]

Methods

__init__(states, *couplings[, vP])

Initializes the Sensor with the specified basis .

add_coupling(states, **kwargs)

Add a coupling between states or groups of states.

add_coupling_group(states1, states2, label)

Adds a group of couplings to a Sensor.

add_couplings(*couplings, **extra_kwargs)

Add any number of couplings between pairs of states.

add_decoherence(statespecs, gamma, **kwargs)

Add a coupling between states or groups of states.

add_decoherence_group(states1, states2, ...)

Adds a group of decoherences to the Sensor.

add_energy_shift(statespec, shift, **kwargs)

Add an energy shift to a single state or a group of states.

add_energy_shift_group(states, shift[, ...])

Add energy shifts to a group of states, optionally with a modifying prefactor for each.

add_energy_shifts(shifts)

Wrapper for Sensor.add_energy shift b().

add_self_broadening(state, gamma[, label, ...])

Specify self-broadening (such as collisional broadening) of a level.

add_self_broadening_group(states, gamma[, ...])

Specify self-broadening (such as collisional broadening) of a group of states.

add_single_coupling(states[, ...])

Adds a single coupling of states to the system.

add_single_decoherence(states, gamma[, ...])

Add decoherent coupling to the graph between two states.

add_single_energy_shift(state, shift[, label])

Add an energy shift to a state.

add_transit_broadening(gamma_transit[, ...])

Adds transit broadening by adding a decoherence from each node to ground.

axis_labels()

Get a list of axis labels for stacked hamiltonians.

coupling_subgraph(coupling)

Returns a subgraph view of the couplings graph corresponding to coupling.

couplings_with(*keys[, method])

Returns a version of self.couplings with only the keys specified.

decoherence_matrix()

Build a decoherence matrix out of the decoherence terms of the graph.

dm_basis()

Generate basis labels of density matrix components.

get_couplings()

Returns the couplings of the system as a dictionary

get_doppler_shifts()

Returns the Hamiltonian with only detunings set to the most probable doppler shift values for each spatial dimension.

get_hamiltonian()

Creates the Hamiltonians from the couplings defined by the fields.

get_hamiltonian_diagonal(values[, no_stack])

Apply addition and subtraction logic corresponding to the direction of the couplings.

get_parameter_mesh()

Returns the parameter mesh of the sensor.

get_rotating_frames()

Determines the rotating frames for the disconnected subgraphs.

get_time_dependence()

Function which returns a list of the time_dependence functions.

get_time_hamiltonian(t)

Get the system hamiltonian at a specific time, t.

get_time_hamiltonian_components()

Get time-dependent components of the hamiltonian.

get_transition_frequencies()

Gets an array of the diagonal elements of the Hamiltonian from the field detunings.

get_value_dictionary(key)

Get subset of dictionary coupling parameters.

group_variable_parameters([apply_mesh])

int_states_map([invert])

Get a dictionary mapping between state labels and their corresponding integer ordering.

set_experiment_values(probe_freq, kappa[, ...])

Sets attributes needed for observable calculations.

set_gamma_matrix(gamma_matrix)

Set the decoherence matrix for the system.

spatial_dim()

Returns the number of spatial dimensions doppler averaging will occur over.

states_with_spec(statespec)

Return a list of all states in the sensor matching the state_spec pattern.

unzip_parameters(zip_label[, verbose])

Remove a set of zipped parameters from the internal zip_labels list.

variable_parameter_sort(par)

Assistance function which determines the sorting order of elements parameters in sensor.

variable_parameters([apply_mesh])

Property to retrieve the values of parameters that were stored on the graph as arrays.

zip_parameters(parameters[, zip_label])

Define 2 scannable parameters as "zipped" so they are scanned in parallel.

zip_zips(*zip_labels[, new_label])

Combine multiple parameter zips into a single zip.

Attributes

atom_mass

Mass of an atom in the vapor cell, in kilograms.

basis_size

Property to return the number of nodes on the Sensor graph.

beam_area

Cross-sectional area of the probing beam, in square meters.

cell_length

Optical path length of the medium, in meters.

eta

Noise density prefactor, in units of root(Hz).

kappa

Differential prefactor, in units of (rad/s)/m.

probe_freq

Probing transition frequency, in rad/s.

probe_tuple

Coupling edge that corresponds to the probing field.

states

Property which gets a list of labels for the sensor in the order defined in __init__().

temp

Temperature of the vapor cell, in Kelvin.

vP

Most probable speed of the 3D Maxwell-Boltzmann distribution.

v_th

Thermal velocity of the atoms in vapor cell, in meters per second.

__sort_couplings(coupling)

Helper function for __str__ to sort couplings by states

_add_coherent_data(states: Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], **field_params) None[source]

Function for internal use which will ensure the supplied couplings is valid, add the field to self.couplings.

Exists to abstract away some of the internally necessary bookkeeping functionality from user-facing classes.

Parameters:
  • states (tuple) – The integer pair of states to be coupled.

  • **field_params (dict) – The dictionary of couplings parameters. For details on the keys of the dictionary see add_coupling().

_coupling_with_label(label: str | Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]]) Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]][source]

Helper function to return the pair of states corresponding to a particular label string. For internal use.

_expand_dims()[source]

Converts the 1-D arrays in the sensor into shapes that allows for rydiqule stacking.

_probe_tuple: Tuple[int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...], int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...]] | None = None
_remove_edge_data(states: Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], kind: str)[source]

Helper function to remove all data that was added with a add_coupling() call or add_decoherence() call. Needed to ensure that two nodes do not have coherent couplings pointing both ways and to invalidate existing zip parameter couplings.

Parameters:
  • states (tuple) – Edge from which to remove data.

  • kind (str) – What type of data to remove. Valid options are coherent coherent couplings or the incoherent key to be cleared (must start with gamma).

Raises:

RydiquleError – If kind is not 'coherent' and doesn’t begin with 'gamma'

_stack_shape(time_dependence: Literal['steady', 'time', 'all'] = 'all') Tuple[int, ...][source]

Internal function to get the shape of the tuple preceding the two hamiltonian axes in get_hamiltonian()

_states_valid(states: Sequence) Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]][source]

Confirms that the provided states are in a valid format.

Typically used internally to validate states added. If provided as a form other than a tuple, first casts to a tuple for consistent indexing.

Checks that states contains 2 elements, can be interpreted as a tuple, and that both states lie inside the basis.

Parameters:

states (iterable) – iterable of to validate. Should be a pair of integers that can be cast to a tuple.

Returns:

Length 2 tuple of validated state labels.

Return type:

tuple

Raises:
  • RydiquleError – If states has more than two elements.

  • TypeError – If states cannot be converted to a tuple.

  • RydiquleError – If either state in states is outside the basis.

_vP: float | None = None
add_coupling(states: Tuple[int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...], int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...]], **kwargs)[source]

Add a coupling between states or groups of states.

Wraps the add_single_coupling() and add_coupling_group() functions, and dispatches to the appropriate one depending on the number of states in the states argument. Additional keyword arguments will be passed unmodified to the relevant method. See documentation of those functions for details on keyword argument options.

If each state specification in states correspond to a single state, the corresponding states will be passed to add_single_coupling(). If either or both specifications correspond to multiple states, the corresponding lists will be passed as the states1 and states2 lists in add_coupling_group().

If this is the first time add_coupling has been called for this Sensor, sets the probe_tuple attribute to the states specification , which is used as the default, for calculating observable values, in a Solution after solving. For this reason, this function is preferred over add_single_coupling() and add_coupling_group() outside of special circumstances. If couplings are added with either of the specific dispatched functions, probe_tuple should be set manually.

Parameters:
  • states (tuple of Statespecs) – The states or state manifolds of the coupling. If both are integers or state specifications matching a single state in the Sensor, add_single_coupling() is dispatched. If either argument is a string pattern matching multiple states, add_coupling_group() is dispatched.

  • **kwargs – Additional keyword arguments passed to the relevant function. See the documentation for add_single_coupling() and add_coupling_group() for details on valid keyword arguments.

Notes

..note:

Outside of specific use cases for users well-versed in the rydiqule code base, this method is preferred over add_single_coupling() and add_coupling_group() since it appropriately handles necessary backend bookkeeping.

Examples

Couplings are added identically regardless of how states are labelled.

>>> s = rq.Sensor(2)
>>> s.add_coupling((0,1), detuning=1, rabi_frequency=2)
>>> print(s.get_hamiltonian())
[[ 0.+0.j  1.+0.j]
 [ 1.-0.j -1.+0.j]]
>>> s = rq.Sensor(['g','e'])
>>> s.add_coupling(('g','e'), detuning=1, rabi_frequency=2)
>>> print(s.get_hamiltonian())
[[ 0.+0.j  1.+0.j]
 [ 1.-0.j -1.+0.j]]

Couplings can have list-like parameters, in which case the resulting rydiqule will compute hamiltonians for all values simultaneously. Here 101 x 21 = 2,121 2x2 Hamiltonians are generated simultaneously, with one for every combination of parameters, and arranged into a single array.

>>> s = rq.Sensor(2)
>>> det=np.linspace(-10, 10, 101)
>>> rabi = np.linspace(-1, 1, 21)
>>> s.add_coupling((0,1), detuning=det, rabi_frequency=rabi, label="laser")
>>> print(s.get_hamiltonian().shape)
(101, 21, 2, 2)

Couplings can be be defined between manifolds of states with state specifications. The values for rabi frequencies of individual states are modified by the coupling_coefficients keyword argument. To avoid cumbersome numbers of nested brackets, it is advisable to name manifolds with variables. Note that StateSpec`s can be expanded with :func:`~.sensor_utils.expand_statespec for this purpose.

>>> g = (0,0) #statespec for ground
>>> excited = (1,[-1,0,1]) #statespec for excited
>>> [e1,e2,e3] = rq.sensor_utils.expand_statespec(excited)
>>> cc = {
...     (g, e1): 0.25,
...     (g, e2): 0.5,
...     (g, e3): 0.25,
... } # coupling coefficients
>>> s = rq.Sensor([g, excited])
>>> s.add_coupling((g, excited), rabi_frequency=10, detuning=1, coupling_coefficients=cc, label="laser")
>>> print(s.get_hamiltonian())
[[ 0.  +0.j  1.25+0.j  2.5 +0.j  1.25+0.j]
 [ 1.25-0.j -1.  +0.j  0.  +0.j  0.  +0.j]
 [ 2.5 -0.j  0.  +0.j -1.  +0.j  0.  +0.j]
 [ 1.25-0.j  0.  +0.j  0.  +0.j -1.  +0.j]]

This function sets the probe_tuple for the first call, but not subsequent calls. This makes it preferred over add_single_coupling() and add_coupling_group(), which do not have this behavior.

>>> g = (0,0) #statespec for ground
>>> e1 = (1,[-1,0,1]) #statespec for 1st excited
>>> e2 = (2,0)
>>> s = rq.Sensor([g, e1, e2])
>>> print(s.probe_tuple)
None
>>> s.add_coupling((g, e1), rabi_frequency=10, detuning=1, label="red")
>>> print(s.probe_tuple)
((0, 0), (1, [-1, 0, 1]))
>>> s.add_coupling((e1,e2), rabi_frequency=1, detuning=2, label="blue")
>>> print(s.probe_tuple)
((0, 0), (1, [-1, 0, 1]))

For state manifolds, list-like parameters are automatically zipped. See Sensor.zip_parameters() for more details on the mechanics of zipping parameters.

>>> g = (0,0) #statespec for ground
>>> e1 = (1,[-1,0,1]) #statespec for 1st excited
>>> s = rq.Sensor([g, e1])
>>> det = np.linspace(-1,1,11)
>>> s.add_coupling((g, e1), rabi_frequency=10, detuning=det, label="red")
>>> print(s.couplings.edges)
[((0, 0), (1, -1)), ((0, 0), (1, 0)), ((0, 0), (1, 1))]
>>> print(s._zip_labels)
['red_detuning']
>>> print(s.get_hamiltonian().shape)
(11, 4, 4)
add_coupling_group(states1: List[int | str | Tuple[float, ...]], states2: List[int | str | Tuple[float, ...]], label: str, rabi_frequency: float | List[float] | ndarray | None = None, detuning: float | List[float] | ndarray | None = None, transition_frequency: float | None = None, coupling_coefficients: dict | None = None, time_dependence: Callable | Dict[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], Callable | None] | None = None, **kwargs)[source]

Adds a group of couplings to a Sensor.

Given 2 lists of states, iterates over each combination of states in the two lists, add performs the add_single_coupling() on that pair of states. All additional parameters are passed directly to the add_single_coupling function.

Additionally, a multiplicative factor can be applied to the rabi frequency of each coupling (i.e. Clebsch-Gordon coefficients). These factors are provided by the cc`(coupling coefficient) parameter as a dictionary with keys corresponding to state pairs in the groups, and the value being the multiplicative factor applied to `rabi_frequency when the Hamiltonian is generated. Note that these cc values cannot be arrays. The corollary for state energy (e.g. for detuning or transition_frequency) is handled via add_energy_shifts(). If no dictionary is supplied for for coherent_cc, all coupling coefficients are set to 1.0, effectively meaning that the base rabi_frequency supplied is what is added to the Hamiltonian. If a dictionary is supplied, any couplings whose coefficient is not specified by the dictionary will be left off the graph.

If any of the parameters are specified as arrays, the associated couplings will be applied to all couplings and automatically zipped together with the label specified by label. For the purposes of axis labelling, the parameters will be zipped in the order`rabi_frequency`, detuning, transition_frequency.

Note that unlike add_coupling(), this function does not set the probe_tuple attribute, so if used to add the first coupling, probe_tuple must be set manually.

Parameters:
  • states1 (list[str or int]) – List of states in the lower energy group of states. Must be integers or string values which correspond to states in the Sensor.

  • states2 (list[str or int]) – List of states in the higher energy group of states. Must be integers or string values which correspond to states in the Sensor.

  • label (str) – Required string label denoting what the group of couplings is called. Used to apply a label in zip_parameters().

  • rabi_frequency (ScannableParameter, optional) – Floating point value or list of values for the base rabi frequency of the coupling group. Multiplied by values specified in cc for individual couplings, often accounting for variations in dipole moment. Default is None.

  • detuning (ScannableParameter, optional) – Base detuning for the coupling group. If specified, every coupling in the group will be treated in the rotating frame. Can be modified through energy level shifts on individual states specified by add_energy_shift(). Default is None.

  • transition_frequency (float, optional) – Base transition frequency for the coupling group. Individual states can be shifted via add_energy_shift(). Default is None.

  • coupling_coefficients (dict, optional) – Individual coupling coefficients passed to the add_single_coupling() method. If provided, defined by a dictionary keyed with tuples of states corresponding to couplings in this group, with values equal to the coupling coefficient to be passed to the add_single_coupling call for that coupling. If any entries are absent in the provided dictionary, they are assumed to not be coupled, and no coupling will be added for that transition. If None, defaults to a dictionary containing every coupling in coupling in the group with None for all values (defaulting to 1.0 when passed to add_single coupling). Defaults to None.

  • time_dependence (scalar function or dict of scalar functions, optional) – Time-dependent scalar factor that is multiplied by the rabi frequency in Hamiltonian generation. Can be specified as a single function, in which case the function will be used as the time_dependence argument for each coupling in the group (see add_single_coupling()), Can also be specified as a dictionary mapping state pairs in the coupling to individual functions which will be applied to the associated coupling in the same manner. In the case of a dictionary specification, each unspecified coupling will default to time_dependence=None.

Raises:

RydiquleError – If states1 and states2 only have one state. Use add_coupling() instead.

Note

Note

The add_coupling() is typically preferred over this method, since it allows for shorthand specification of groups, and sets the Sensor.probe_tuple attribute.

Note

If a CouplingNotAllowedError is raised while adding the individual couplings for the group, couplings that raised the error will be ignored.

Examples

Energy shifts added to remove degenerate energy levels. If no clebsch-gordon coefficients are supplied, ALL default to 1

>>> s = rq.Sensor(['a1', 'a2', 'b1', 'b2'])
>>> s.add_energy_shifts({'a2':0.1, 'b2':0.1})
>>> s.add_coupling_group(['a1','a2'], ['b1','b2'], detuning=1, rabi_frequency=1, label='example')
>>> s.get_hamiltonian()
array([[ 0. +0.j,  0. +0.j,  0.5+0.j,  0.5+0.j],
       [ 0. +0.j,  0.1+0.j,  0.5+0.j,  0.5+0.j],
       [ 0.5-0.j,  0.5-0.j, -1. +0.j,  0. +0.j],
       [ 0.5-0.j,  0.5-0.j,  0. +0.j, -0.9+0.j]])

If the cc dictionary is specified, any unspecified terms are skipped on the graph. Note that although (0,3) is in the coupling group, it is omitted from the graph since it is not in coupling_coefficients.

>>> s = rq.Sensor(4)
>>> cc = {(0,1):0.5, (0,2):0.5}
>>> s.add_coupling_group([0],[1,2,3], detuning=1, rabi_frequency=1, coupling_coefficients=cc, label='foo')
>>> print(s)
<class 'rydiqule.sensor.Sensor'> object with 4 states and 2 coherent couplings.
States: [0, 1, 2, 3]
Coherent Couplings:
    (0,1): {rabi_frequency: 1, detuning: 1, phase: 0, kvec: (0, 0, 0), label: foo_0, coherent_cc: 0.5}
    (0,2): {rabi_frequency: 1, detuning: 1, phase: 0, kvec: (0, 0, 0), label: foo_1, coherent_cc: 0.5}
Decoherent Couplings:
    None
Energy Shifts:
    None

For list-like parameters, the couplings are treated as originating from a single laser and that parameter is zipped across all couplings in the group.

>>> g = (0,0) #statespec for ground
>>> e1 = (1,[-1,0,1]) #statespec for 1st excited
>>> s = rq.Sensor([g, e1])
>>> det = np.linspace(-1,1,11)
>>> s.add_coupling_group([(0,0)], [(1,-1), (1,0), (1,1)],
...                       rabi_frequency=10, detuning=det, label="red")
>>> print(s)
<class 'rydiqule.sensor.Sensor'> object with 4 states and 3 coherent couplings.
States: [(0, 0), (1, -1), (1, 0), (1, 1)]
Coherent Couplings:
    ((0, 0),(1, -1)): {rabi_frequency: 10, detuning: <parameter with 11 values>, phase: 0, kvec: (0, 0, 0), label: red_0, coherent_cc: 1.0, red_detuning: detuning}
    ((0, 0),(1, 0)): {rabi_frequency: 10, detuning: <parameter with 11 values>, phase: 0, kvec: (0, 0, 0), label: red_1, coherent_cc: 1.0, red_detuning: detuning}
    ((0, 0),(1, 1)): {rabi_frequency: 10, detuning: <parameter with 11 values>, phase: 0, kvec: (0, 0, 0), label: red_2, coherent_cc: 1.0, red_detuning: detuning}
Decoherent Couplings:
    None
Energy Shifts:
    None
Zip Labels:
    ['red_detuning']
>>> print(s.get_hamiltonian().shape)
(11, 4, 4)
add_couplings(*couplings: Dict, **extra_kwargs) None[source]

Add any number of couplings between pairs of states.

Acts as an alternative to calling add_coupling() individually for each pair of states. Can be used interchangeably up to preference, and all of keyword add_coupling() are supported dictionary keys for dictionaries passed to this function.

Note that since this function wraps add_coupling(), the first element of couplings will be used to set probe_tuple.

Parameters:
  • couplings (tuple of dicts) – Any number of dictionaries, each specifying the parameters of a single field coupling 2 states. For more details on the keys of each dictionary see the arguments for add_coupling(). Equivalent to passing each dictionaries keys and values to add_coupling() individually.

  • **extra_kwargs (dict) – Additional keyword-only arguments to pass to the relevant add_coupling method. The same arguments will be passed to each call of add_coupling(). Often used for warning suppression. Can also be used to define a common coupling parameter for each coupling.

Examples

>>> s = rq.Sensor(3)
>>> blue = {"states":(0,1), "rabi_frequency":1, "detuning":2}
>>> red = {"states":(1,2), "rabi_frequency":3, "detuning":4}
>>> s.add_couplings(blue, red)
>>> print(s)
<class 'rydiqule.sensor.Sensor'> object with 3 states and 2 coherent couplings.
States: [0, 1, 2]
Coherent Couplings:
    (0,1): {rabi_frequency: 1, detuning: 2, phase: 0, kvec: (0, 0, 0), coherent_cc: 1.0, label: (0,1)}
    (1,2): {rabi_frequency: 3, detuning: 4, phase: 0, kvec: (0, 0, 0), coherent_cc: 1.0, label: (1,2)}
Decoherent Couplings:
    None
Energy Shifts:
    None
add_decoherence(statespecs: Tuple[int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...], int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...]], gamma: float | List[float] | ndarray, **kwargs)[source]

Add a coupling between states or groups of states.

Wraps the add_single_decoherence() and add_decoherence_group() functions, and dispatches to the appropriate one depending on the formatting of the states argument. Additional keyword arguments will be passed unmodified to the relevant method. See documentation of those functions for details on keyword argument options.

Parameters:
  • states (tuple of StateSpec) – The states or state manifolds of the decoherent coupling. If both are integers or string patterns matching a single state in the Sensor, add_single_decoherence() is dispatched. If either argument is a specification matching multiple states, add_decoherence_group() is dispatched.

  • gamma (float or Sequence) – The decoherence rate, in Mrad/s.

  • **kwargs – Additional keyword arguments passed to the appropriate function. See documentation for add_single_decoherence() and add_decoherence_group() for more details on valid keyword arguments.

Examples

>>> s = rq.Sensor(3)
>>> s.add_coupling(states=(0,1), detuning=1, rabi_frequency=1)
>>> s.add_coupling(states=(1,2), detuning=1, rabi_frequency=1)
>>> s.add_decoherence((2,0), 0.1, label="misc")
>>> print(s.decoherence_matrix())
[[0.  0.  0. ]
 [0.  0.  0. ]
 [0.1 0.  0. ]]

To add multiple decoherence effects to the same term, use a different label for each.

>>> s = rq.Sensor(3)
>>> s.add_coupling(states=(0,1), detuning=1, rabi_frequency=1)
>>> s.add_coupling(states=(1,2), detuning=1, rabi_frequency=1)
>>> s.add_decoherence((2,0), 0.1, label='foo')
>>> s.add_decoherence((2,0), 0.15, label='bar')
>>> print(s.decoherence_matrix())
[[0.   0.   0.  ]
 [0.   0.   0.  ]
 [0.25 0.   0.  ]]

Just like coherent coupling parameters, decoherence values can be passed as list-like objects and scanned. This adjusts the hamiltonian shape for clear broadcasting.

>>> s = rq.Sensor(3)
>>> gamma = np.linspace(0,0.5,11)
>>> s.add_coupling(states=(0,1), detuning=1, rabi_frequency=1)
>>> s.add_coupling(states=(1,2), detuning=1, rabi_frequency=1)
>>> s.add_decoherence((2,0), gamma)
>>> print(s.decoherence_matrix().shape)
(11, 3, 3)
>>> print(s.get_hamiltonian().shape)
(11, 3, 3)

Upper and lower states can also be regex strings matched against states in the Sensor just like for coherent couplings.

>>> s = rq.Sensor(['g','e1','e2'])
>>> s.add_coupling(states=('g','e1'), detuning=1, rabi_frequency=1)
>>> s.add_coupling(states=('e1','e2'), detuning=1, rabi_frequency=1)
>>> gamma = np.linspace(0, 0.3, 3)
>>> cc = {('e1','g'):0.25, ('e2','g'):0.75}
>>> s.add_decoherence((['e1','e2'],'g'), gamma, label="test", coupling_coefficients=cc)
>>> print(s.decoherence_matrix())
[[[0.     0.     0.    ]
  [0.     0.     0.    ]
  [0.     0.     0.    ]]

 [[0.     0.     0.    ]
  [0.0375 0.     0.    ]
  [0.1125 0.     0.    ]]

 [[0.     0.     0.    ]
  [0.075  0.     0.    ]
  [0.225  0.     0.    ]]]

Also just like coherent couplings, decoherent coupling can be defined over manifolds using state specifications. The interface is identical.

>>> g = (0,[-1,1])
>>> e = (1,[-1,1])
>>> cc = {
...    ((1,-1),(0,-1)):1,
...    ((1,1),(0,-1)):2,
...    ((1,-1),(0,1)):1,
...    ((1,1),(0,1)):2,
... }
>>> s = rq.Sensor([g,e])
>>> s.add_coupling((g,e), detuning=1, rabi_frequency=1, label='foo')
>>> s.add_decoherence((e,g), 0.1, coupling_coefficients=cc, label='bar')
>>> print(s.decoherence_matrix())
[[0.  0.  0.  0. ]
 [0.  0.  0.  0. ]
 [0.1 0.1 0.  0. ]
 [0.2 0.2 0.  0. ]]
add_decoherence_group(states1: List[int | str | Tuple[float, ...]], states2: List[int | str | Tuple[float, ...]], gamma: float | List[float] | ndarray, label: str, coupling_coefficients: Dict[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], float] | None = None)[source]

Adds a group of decoherences to the Sensor.

Given 2 lists of states, adds a single coupling across each combination of states between the first and second lists. Then, if gamma is a array-like of values, automatically performs zip_parameters() on all decoherences added as part of this function so they share an axis when decoherence_matrix() is called.

Scaling multiplicative factors for gamma must be applied per pair of states using decoherent_cc, a dictionary of coefficients determining coupling strengths. If a pair is not in decoherent_cc, it is assumed to have a coupling coefficient of zero, and will be omitted from the graph. If decoherent_cc is None, all couplings are assumed to have a relative strength of 1.

Parameters:
  • states1 (List of State) – The list of states out of which population is decaying. Each element of the list must be a state in this Sensor.

  • states2 (List of State) – The list of states into which population is decaying. Each element of the list must be a state in this Sensor.

  • label (str) – Required string label denoting what the group of dephasings is called. Used to apply a label to the zip.

  • gamma (ScannableParameter) – Base decoherence rate between the two groups of states, in units of Mrad/s. Multiplied by the corresponding values in the decoherent_coupling dictionary.

  • coupling_coefficients (dict, optional) – Coefficients describing the relative coupling strengths for decoherences in the group. Treated as modifications to the “base” dephasing rate specified by the gamma argument. The gamma of individual decoherences will be the gamma argument multiplied by the corresponding value in this dictionary. If None, all couplings in the group are assumed to have a coefficient of 1.0 if specified, all unspecified coupling pairs are ignored.

Raises:
  • ValueError – If the either of the states strings provided cannot be parsed as a regex pattern

  • ValueError – If states1 and states2 only have one state. Use add_decoherence() instead.

Examples

>>> s = rq.Sensor(['g','e1','e2'])
>>> s.add_coupling(states=('g','e1'), detuning=1, rabi_frequency=1)
>>> s.add_coupling(states=('e1','e2'), detuning=1, rabi_frequency=1)
>>> cc = {('e1','g'):0.25, ('e2','g'):0.75}
>>> s.add_decoherence_group(['e1','e2'],['g'], 0.1, "test", coupling_coefficients=cc)
>>> print(s.decoherence_matrix())
[[0.    0.    0.   ]
 [0.025 0.    0.   ]
 [0.075 0.    0.   ]]

Unlike Sensor.add_decoherence(), this function does not accept state specifications. Upper and lower states must be passed as lists. As this tends to be a little clunkier, Sensor.add_decoherence() is usually preferred.

>>> g = (0,[-1,1])
>>> e = (1, [-1,1])
>>> list_g = [(0, -1), (0, 1)]
>>> list_e = [(1, -1), (1, 1)]
>>> cc = {
...    ((1,1),(0,1)): 0.4,
...    ((1,1),(0,-1)): 0.1,
...    ((1,-1),(0,1)): 0.1,
...    ((1,-1),(0,-1)): 0.4
... }
>>> s = rq.Sensor([g,e])
>>> print(s.states)
[(0, -1), (0, 1), (1, -1), (1, 1)]
>>> s.add_decoherence_group(list_e, list_g, 0.1, "foo", coupling_coefficients=cc)
>>> print(s.decoherence_matrix())
[[0.   0.   0.   0.  ]
 [0.   0.   0.   0.  ]
 [0.04 0.01 0.   0.  ]
 [0.01 0.04 0.   0.  ]]
add_energy_shift(statespec: int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...], shift: float | List[float] | ndarray, **kwargs)[source]

Add an energy shift to a single state or a group of states.

statespec can be provided either as a single state in the Sensor or as a valid state specification matching a group of states. When statespec matches a single state, add_single_energy_shift() method will be dispatched. In the case of a multi-state specification, the add_energy_shift_group() method will be dispatched applying an individual shift to all states with labels matching the specification provided.

Note that an energy shifts are applied to the underlying graph as a self-edge connecting a node to its self, not as data on the node its self.

Additional arguments for either dispatched function are passed normally via **kwargs

Parameters:
  • state_spec (StateSpec) – Integer or string label matching a state in the Sensor, or state specification matching one or more states in the Sensor. The number of states this corresponds to will affect which internal function is dispatched.

  • shift (float or array-like) – The energy shift to apply to the matching state or states in Mrad/s. Note that if it corresponds to multiple states, the prefactors argument of add_energy_shift_group() will be multiplied by this value for the corresponding state.

Raises:

RydiquleError – If the state provided does not match any state in the Sensor.

Examples

The basic use of add_energy_shift is to add terms to the diagonal of the hamiltonian.

>>> s = rq.Sensor(3)
>>> s.add_energy_shift(1, 1)
>>> s.add_energy_shift(2, 2.5)
>>> print(s.couplings.edges(data=True))
[(1, 1, {'e_shift': 1, 'label': '1'}), (2, 2, {'e_shift': 2.5, 'label': '2'})]
>>> print(s.get_hamiltonian())
[[0. +0.j 0. +0.j 0. +0.j]
 [0. +0.j 1. +0.j 0. +0.j]
 [0. +0.j 0. +0.j 2.5+0.j]]

add_energy_shift can be used with state specifications.

>>> s = rq.Sensor([(0,0), (1,[-1,0,1])])
>>> prefactors = {(1,i):i for i in [-1,0,1]}
>>> s.add_energy_shift((1, [-1,0,1]), 0.1, prefactors=prefactors)
>>> print(s.couplings.edges(data="e_shift"))
[((1, -1), (1, -1), -0.1), ((1, 0), (1, 0), 0.0), ((1, 1), (1, 1), 0.1)]
>>> print(s.get_hamiltonian())
[[ 0. +0.j  0. +0.j  0. +0.j  0. +0.j]
 [ 0. +0.j -0.1+0.j  0. +0.j  0. +0.j]
 [ 0. +0.j  0. +0.j  0. +0.j  0. +0.j]
 [ 0. +0.j  0. +0.j  0. +0.j  0.1+0.j]]
add_energy_shift_group(states: List[int | str | Tuple[float, ...]], shift: float | List[float] | ndarray, prefactors: dict | None = None, zip_label: str | None = None)[source]

Add energy shifts to a group of states, optionally with a modifying prefactor for each.

Given a list of states, calls add_single_energy_shift() on each one with the provided energy shift. Shifts are modified by a multiplicative factor defined by the prefactors dictionary. The dictionary is keyed with states that are elements of states with entries corresponding to a factor multiplied by the base shift argument for each state. When energy shifts are array-like, the e_shift attribute corresponding to each self-edge will be zipped with zip_parameters().

Parameters:
  • states (list of states) – List of states to include in the group.

  • shift (float or array-like) – The base value of the energy shift to apply the states. Will be modified by entries of the prefactors dictionary.

  • prefactors (dict or None, optional) – Dictionary of values by which to multiply the base shift parameter for each each state. Keys are elements of the states list, entries are the corresponding factor by which to multiply shift for that state. If None, all prefactors are set to 1. If not None, the prefactors for any non-specified values will be set to zero. Default is None.

  • zip_label (str or None, optional) – Label passed to zip_parameters() when the shift is provided as an array-like when all states in the group are zipped together. Defaults to None.

Raises:

RydiquleError – If the supplied energy shift is not a float and cannot be interpreted as a numpy

Examples

>>> s = rq.Sensor(['g','e1','e2'])
>>> factors = {'e1':1, 'e2':2}
>>> s.add_energy_shift_group(["e1","e2"], 0.1, prefactors=factors)
>>> print(s.couplings.edges(data='e_shift'))
[('e1', 'e1', 0.1), ('e2', 'e2', 0.2)]
>>> print(s.get_hamiltonian())
[[0. +0.j 0. +0.j 0. +0.j]
 [0. +0.j 0.1+0.j 0. +0.j]
 [0. +0.j 0. +0.j 0.2+0.j]]
add_energy_shifts(shifts: dict)[source]

Wrapper for Sensor.add_energy shift b().

Shifts are specified with the shifts dictionary, which is keyed with states and has values corresponding to the energy shift applied to the state in Mrad/s. Error handling and validation is done with the add_energy_shift() function.

Parameters:

shifts (dict) – Dictionary keyed with states with values corresponding to the energy shift, in Mrad/s, of the corresponding state.

add_self_broadening(state: int | str | Tuple[float, ...], gamma: float | List[float] | ndarray, label: str = 'self', decoherent_cc: Dict[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], float] | None = None)[source]

Specify self-broadening (such as collisional broadening) of a level.

Equivalent to calling add_decoherence() and specifying both states to be the same, with the “self” label. For more complicated systems, it may be useful to further specify the source of self-broadening as, for example, “collisional” for easier bookkeeping and to ensure no values are overwritten.

Parameters:
  • state (State) – State or states to which the broadening will be added. Using a regular expression allows for specifying self broadening of a group of states. In this case, mult-factor is used to define relative amplitudes.

  • gamma (float or sequence) – The broadening width to be added in Mrad/s.

  • label (str, optional) – Optional label for the state. By default, decay will be stored on the graph edge as "gamma_self". Otherwise, will cast as a string and decay will be stored on the graph edge as "gamma_"+label

  • mult-factor (dict) – Dictionary mapping of the scaling factors to apply to the self broadening of each state in a group specified via regular expression.

Notes

Note

Just as with the add_decoherence() function, adding a decoherence value with a label that already exists will overwrite an existing decoherent transition with that label. The “self” label is applied to this function automatically to help avoid an unwanted overwrite.

Examples

>>> s = rq.Sensor(3)
>>> s.add_self_broadening(1, 0.1)
>>> print(s.couplings.edges(data=True))
[(1, 1, {'gamma_self': 0.1, 'label': '(1,1)'})]
>>> print(s.decoherence_matrix())
[[0.  0.  0. ]
 [0.  0.1 0. ]
 [0.  0.  0. ]]
add_self_broadening_group(states: List[int | str | Tuple[float, ...]], gamma: float | List[float] | ndarray, label: str = 'self', decoherent_cc: dict | None = None)[source]

Specify self-broadening (such as collisional broadening) of a group of states.

Equivalent to calling add_decoherence_group() and specifying both state groups to be the same, with the “self” label. For more complicated systems, it may be useful to use label to label the source of self-broadening as, for example, “collisional” for easier bookkeeping and to ensure no values are overwritten.

Note that this function applies decoherence terms to every combination of states in the group, not just from each state to its self.

Parameters:
  • states (list of State) – List of states to which the self-broadening is applied.

  • gamma (ScannableParameter) – The broadening width to be added in Mrad/s.

  • label (str, optional) – Optional label for the state. By default, decay will be stored on the graph edge as "gamma_self". Otherwise, will cast as a string and decay will be stored on the graph edge as "gamma_"+label

  • decoherent_cc (dict, optional) – Clebsch-Gordon-like coefficients for how gamma scales to different pairs of states within the group. Unspecified pairs are assumed to have coefficients of 0. Default value is None, which applies 0 to all coefficients.

add_single_coupling(states: Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], rabi_frequency: float | List[float] | ndarray | None = None, detuning: float | List[float] | ndarray | None = None, transition_frequency: float | None = None, phase: float | List[float] | ndarray | None = None, kvec: Sequence[float] = (0, 0, 0), time_dependence: Callable[[float], complex] | None = None, label: str | None = None, coherent_cc: float | None = None, **extra_kwargs) None[source]

Adds a single coupling of states to the system.

One or more of these parameters can be a list or array-like of values to represent a laser that can take on a set of discrete values during a field scan. Designed to be a user-facing wrapper for _add_coupling() with arguments for states and coupling parameters.

Note that unlike add_coupling(), this function does not set the probe_tuple attribute, so if used to add the first coupling, probe_tuple must be set manually.

Parameters:
  • states (tuple of States) – The pair of states of the sensor which the state couples. Must be a tuple of length 2, where each element is a string, integer, or tuple corresponding to a state in the Sensor as defined in the constructor. Tuple order indicates which state to has higher energy; the second state is always assumed to have higher energy.

  • rabi_frequency (float or complex, or list-like of float or complex) – The rabi frequency of the field being added. Defined in units of Mrad/s. List-like values will invoke Rydiqule’s stacking convention when relevant quantities are calculated.

  • detuning (float or list-like of floats, optional) – The frequency difference between the transition frequency and the field frequency in units of Mrad/s. List-like values will invoke Rydiqule’s stacking convention when relevant quantities are calculated. If specified, the coupling is treated with the rotating-wave approximation rather than in the lab frame, and transition_frequency is ignored if present. A positive number always indicates a blue detuning, and a negative number indicates a blue detuning.

  • transition_frequency (float, optional) – The transition frequency between a particular pair of states. Must be a positive number. Only used directly in calculations if detuning is None, ignored otherwise. Note that on its own, it only defines the spacing between two energy levels and not the field its self. To define a field, the time_dependence argument must be specified, or else the off-diagonal terms to drive transitions will not be generated in the Hamiltonian matrix.

  • phase (float, optional) – Static phase offset in the rotating frame. Cannot be used outside the rotating frame, ie when detuning is not defined. Default is undefined, which is interpreted as 0 for couplings in the rotating frame.

  • kvec (iterable, optional) – A three-element iterable that defines the k-vector of a particular coupling field. It should have units of Mrad/m, such that \(vP*k_vec\) gives the most probable doppler shift along each axis. Note that the vP class attribute must be defined to perform doppler-broadened solves. If equal to (0,0,0), solvers will ignore doppler shifts on this field. Defaults to (0,0,0).

  • time_dependence (scalar function, optional) – A scalar function specifying a time-dependent field. The time dependence function is defined as a python function that returns a unit-less value as a function of time (in microseconds) that is multiplied by the rabi_frequency parameter to get a field strength scaled to units of Mrad/s.

  • coherent_cc (float, optional) – Additional information regarding coupling strength for the states coupling. Does not modify the rabi_frequency before adding to the graph. Rather, when the Hamiltonians and physical observables in Solution are computed, first multiplies the rabi_frequency by this value. The rabi_frequency can be thought of as a “base” field power, while this is a modification based on the coupling strength. If None, the coherent_cc added to the graph will be set to 1. Defaults to None.

  • label (str or None, optional) – Name of the coupling. This does not change any calculations, but can be used to help track individual couplings, and will be reflected in the output of axis_labels(), and to specify zipping for zip_couplings(). If None, the label is generated as the value of states cast to a string with whitespace removed. Defaults to None.

Raises:
  • RydiquleError – If states cannot be interpreted as a tuple.

  • RydiquleError – If states does not have a length of 2.

  • RydiquleError – If the states specified in the states argument are not in the basis of the Sensor

  • RydiquleError – If both rabi_frequency and dipole_moment are specified or if neither are specified.

  • RydiquleError – If both detuning and transition_frequency are specified or if neither are specified.

  • RydiquleError – If a coupling is added in the non-rotating frame (detuning=None) and no time dependence function is specified.

  • RydiquleError – If kvec is not a three element sequence of floats.

Warns:
  • RWAWarning – Raised if large transition_frequency is passed, which can lead to very long time-dependent solves and is often not intended.

  • RydiquleWarning – Raised if ‘kvec’ is likely incorrectly defined (field k-vector in Mrad/m). Either by incorrect units or as most probable velocity vector (rq v1 convention). Triggers if the implied wavelength is less than 210 nm or greater than 2095 nm.

Examples

>>> s = rq.Sensor(2)
>>> s.add_single_coupling((0,1), detuning=1, rabi_frequency=2)
>>> print(s.get_hamiltonian())
[[ 0.+0.j  1.+0.j]
 [ 1.-0.j -1.+0.j]]
>>> s = rq.Sensor(['g','e'])
>>> s.add_single_coupling(('g','e'), detuning=1, rabi_frequency=2)
>>> print(s.get_hamiltonian())
[[ 0.+0.j  1.+0.j]
 [ 1.-0.j -1.+0.j]]
>>> s = rq.Sensor(2)
>>> s.add_single_coupling((0,1), detuning=np.linspace(-10, 10, 101), rabi_frequency=2, label="laser")
>>> print(s.get_hamiltonian().shape)
(101, 2, 2)

The coherent_cc attribute does not modify the rabi_frequency that is stored on the graph, but rather in the computed hamiltonian

>>> s = rq.Sensor(2)
>>> s.add_single_coupling((0,1), detuning=1, rabi_frequency=2, coherent_cc=1/3)
>>> print(s.couplings.edges.data("rabi_frequency")) #shows the rabi_frequency on the graph is 2
[(0, 1, 2)]
>>> print(s.get_hamiltonian())
[[ 0.      +0.j  0.333333+0.j]
 [ 0.333333-0.j -1.      +0.j]]
>>> s = rq.Sensor(2)
>>> step = lambda t: 1 if t>=1 else 0
>>> s.add_single_coupling((0,1), transition_frequency=1000, rabi_frequency=2, time_dependence=step)
>>> print(s.get_hamiltonian())
[[   0.+0.j    0.+0.j]
 [   0.+0.j 1000.+0.j]]
>>> print(s.get_time_hamiltonian_components()[0])
[array([[0.+0.j, 2.+0.j],
       [2.-0.j, 0.+0.j]])]
>>> s = rq.Sensor(2, vP=10)
>>> kp = 25*np.array([1,0,0])
>>> s.add_single_coupling((0,1), detuning=1, rabi_frequency=2, kvec=kp)
>>> s.get_hamiltonian()
array([[ 0.+0.j,  1.+0.j],
       [ 1.-0.j, -1.+0.j]])
add_single_decoherence(states: Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], gamma: float | List[float] | ndarray, decoherent_cc: float = 1.0, label: str | None = None)[source]

Add decoherent coupling to the graph between two states.

If gamma is list-like, the array generated by decoherence_matrix() will contain decoherence matrices for every combination of decoherence values provided. This functionality mirrors hamiltonian generation when parameters of add_coupling() are list-like. Note that if gamma is 0 or an array of zeros, the associated edge key will be left off the graph.

Parameters:
  • states (tuple of State) – Length-2 tuple of integers corresponding to the two states. The first value is the number of state out of which population decays, and the second is the number of the state into which population decays.

  • gamma (float or sequence) – The decay rate, in Mrad/s.

  • decoherent_cc (float) – The value by which gamma is multiplied before it is added to the graph. Typically only used by add_decoherence_group(), but made transparent for scripting purposes. Defaults to 1.0

  • label (str or None, optional) – Optional label for the decay. If None, decay will be stored on the graph edge as "gamma". Otherwise, will cast as a string and decay will be stored on the graph edge as "gamma_"+label.

Notes

Note

Adding a decoherence with a particular label (including None) will override an existing decoherent transition with that label.

Examples

>>> s = rq.Sensor(3)
>>> s.add_coupling(states=(0,1), detuning=1, rabi_frequency=1)
>>> s.add_coupling(states=(1,2), detuning=1, rabi_frequency=1)
>>> s.add_single_decoherence((2,0), 0.1, label="misc")
>>> print(s.decoherence_matrix())
[[0.  0.  0. ]
 [0.  0.  0. ]
 [0.1 0.  0. ]]

To add multiple decoherence effects to the same term, provide a different label for each.

>>> s = rq.Sensor(3)
>>> s.add_coupling(states=(0,1), detuning=1, rabi_frequency=1)
>>> s.add_coupling(states=(1,2), detuning=1, rabi_frequency=1)
>>> s.add_single_decoherence((2,0), 0.1, label='foo')
>>> s.add_single_decoherence((2,0), 0.15, label='bar')
>>> print(s.decoherence_matrix())
[[0.   0.   0.  ]
 [0.   0.   0.  ]
 [0.25 0.   0.  ]]

Decoherence values can also be scanned. Here decoherence from states 2->0 is scanned between 0 and 0.5 for 11 values. We can also see how the Hamiltonian shape accounts for this to allow for clean broadcasting, indicating that the hamiltonian is identical across all decoherence values.

>>> s = rq.Sensor(3)
>>> gamma = np.linspace(0,0.5,11)
>>> s.add_coupling(states=(0,1), detuning=1, rabi_frequency=1)
>>> s.add_coupling(states=(1,2), detuning=1, rabi_frequency=1)
>>> s.add_single_decoherence((2,0), gamma)
>>> print(s.decoherence_matrix().shape)
(11, 3, 3)
>>> print(s.get_hamiltonian().shape)
(11, 3, 3)
add_single_energy_shift(state: int | str | Tuple[float, ...], shift: float | List[float] | ndarray, label=None)[source]

Add an energy shift to a state.

First performs validation that the provided state is actually a node in the graph, then adds the shift specified by shift to a self-loop edge keyed with "e_shift". This value will be added to the corresponding diagonal term when the hamiltonian is generated.

Parameters:
  • state (int, str, or tuple) – The label corresponding to the atomic state to which the shift will be added.

  • shift (float or list-like of float) – The magnitude of the energy shift, in Mrad/s

Raises:

RydiquleError – If the supplied state is not in the system.

add_transit_broadening(gamma_transit: float | List[float] | ndarray, repop: Dict[int | str | Tuple[float, ...], float] | List[int | str | Tuple[float, ...]] | None = None, label: str = 'transit')[source]

Adds transit broadening by adding a decoherence from each node to ground.

For each state n, adds a decoherent transition from n to each state in the keys of the repop dictionary using the add_decoherence() method with provided label ("transit"`by default) See :meth:`~.Sensor.add_decoherence for more details on labeling.

If an array of transit values are provided, they will be automatically zipped together into a single scanning element.

Parameters:
  • gamma_transit (float or sequence) – The transit broadening rate in Mrad/s.

  • repop (dict, optional) – Dictionary of states for transit to repopulate in to. The keys represent the state labels. The values represent the fractional amount that goes to that state. If the sum of value does not equal 1, population will not be conserved. Default is to repopulate everything into the ground state (either state 0 or the first state in the basis passed to the __init__() method). If None, all population decays to the ground state, defined as the first state in the state list passed to the constructor. Defaults to None.

  • label (str, optional) – Label to be passed to add_decoherence(). Defaults to “transit”

Warns:

PopulationNotConservedWarning – If the values of the repop parameter do not sum to 1, thus meaning population will not be conserved.

Examples

>>> s = rq.Sensor(3)
>>> s.add_transit_broadening(0.1)
>>> print(s.couplings.edges(data=True))
[(0, 0, {'gamma_transit': 0.1, 'label': '(0,0)'}),
 (1, 0, {'gamma_transit': 0.1, 'label': '(1,0)'}),
 (2, 0, {'gamma_transit': 0.1, 'label': '(2,0)'})]
>>> print(s.decoherence_matrix())
[[0.1 0.  0. ]
 [0.1 0.  0. ]
 [0.1 0.  0. ]]
>>> s = rq.Sensor(['g', 'e1', 'e2'])
>>> repop = {'g':0.75, 'e1': 0.25}
>>> s.add_transit_broadening(0.2, repop=repop)
>>> print(s.decoherence_matrix())
[[0.15 0.05 0.  ]
 [0.15 0.05 0.  ]
 [0.15 0.05 0.  ]]
atom_mass: float | None = None

Mass of an atom in the vapor cell, in kilograms.

axis_labels() List[str][source]

Get a list of axis labels for stacked hamiltonians.

The axes of a hamiltonian stack are defined as the axes preceding the usual hamiltonian, which are always the last 2. These axes only exist if one of the parameters used to define a Hamiltonian are lists.

Be default, labels which have been zipped using zip_parameters() will be combined into a single label, as this is how get_hamiltonian() treats these axes.

The ordering of axis labels is as follows:

  • Zipped parameter (shared axes) appear before single parameters.

  • Zipped parameters are ordered alphabetically by label.

  • Single axes are sorted first by lower state, then by upper state, then alphabetically by parameter.

Returns:

Strings corresponding to the label of each axis on a stack of multiple hamiltonians.

Return type:

list of str

Examples

There are no preceding axes if there are no list-like parameters.

>>> s = rq.Sensor(3)
>>> blue = {"states":(0,1), "rabi_frequency":1, "detuning":2}
>>> red = {"states":(1,2), "rabi_frequency":3, "detuning":4}
>>> s.add_couplings(blue, red)
>>> print(s.get_hamiltonian().shape)
(3, 3)
>>> print(s.axis_labels())
[]

Adding list-like parameters expands the hamiltonian

>>> s = rq.Sensor(3)
>>> det = np.linspace(-10, 10, 11)
>>> blue = {"states":(0,1), "rabi_frequency":1, "detuning":det, "label":"blue"}
>>> red = {"states":(1,2), "rabi_frequency":3, "detuning":det}
>>> s.add_couplings(blue, red)
>>> print(s.get_hamiltonian().shape)
(11, 11, 3, 3)
>>> print(s.axis_labels())
['blue_detuning', '(1,2)_detuning']

The ordering of labels doesn’t change if string state names are used. For single couplings, the ordering of axes is determined purely by the ordering of the states, regardless of coupling labels or string names of states.

>>> s = rq.Sensor(['g', 'e1', 'e2'])
>>> det = np.linspace(-10, 10, 11)
>>> blue = {"states":('g','e1'), "rabi_frequency":1, "detuning":det, "label":"blue"}
>>> red = {"states":('e1','e2'), "rabi_frequency":3, "detuning":det}
>>> s.add_couplings(blue, red)
>>> print(s.get_hamiltonian().shape)
(11, 11, 3, 3)
>>> print(s.axis_labels())
['blue_detuning', '(e1,e2)_detuning']

Zipping parameters combines labels onto a single axis, since their Hamiltonians now lie on a single axis of the stack. The name of that axis will be the label provided to zipped_parameters(). Note that this will default to ‘zip_<int>’. Here the axis of length 7 (axis 1) corresponds to the rabi frequencies and the axis of shape 11 (axis 0) corresponds to the zipped detunings

>>> s = rq.Sensor(3)
>>> s.add_coupling(states=(0,1), detuning=np.arange(11), rabi_frequency=np.linspace(-3, 3, 7))
>>> s.add_coupling(states=(1,2), detuning=0.5*np.arange(11), rabi_frequency=1)
>>> s.zip_parameters({(0,1):"detuning", (1,2):"detuning"}, zip_label="detunings")
>>> print(s.get_hamiltonian().shape)
(11, 7, 3, 3)
>>> print(s.axis_labels())
['detunings', '(0,1)_rabi_frequency']
property basis_size: int

Property to return the number of nodes on the Sensor graph.

Returns:

The number of nodes on the graph, corresponding to the basis size for the system.

Return type:

int

beam_area: float | None = None

Cross-sectional area of the probing beam, in square meters.

cell_length: float | None = None

Optical path length of the medium, in meters.

coupling_subgraph(coupling: Tuple[int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...], int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...]]) Graph[source]

Returns a subgraph view of the couplings graph corresponding to coupling.

Parameters:

coupling (StateSpecs) – Coupling specification

Returns:

View of the corresponding subgraph

Return type:

networkx.Graph

couplings_with(*keys: str, method: Literal['all', 'any', 'not any'] = 'all') Dict[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], Dict][source]

Returns a version of self.couplings with only the keys specified.

Can be specified with a several criteria, including all, none, or any of the keys specified.

Parameters:
  • str) (keys(tuple of) – parameter names for a state. See add_coupling() for which names are valid for a Sensor object.

  • method ({'all','any', 'not any'}) – Method to see if a given field matches the keys given. Choosing “all” will return couplings which have keys matching all of the values provided in the keys argument, while choosing “any”, will return all couplings with keys matching at least one of the values specified by keys. For example, sensor.couplings_with("rabi_frequency") returns a dictionary of all couplings for which a rabi_frequency was specified. sensor.couplings_with("rabi_frequency", "detuning", method="all") returns all couplings for which both rabi_frequency and detuning are specified. ‘sensor.couplings_with(“rabi_frequency”, “detuning”, method=”any”)` returns all couplings for which either rabi_frequency or detuning are specified. Defaults to “all”.

Returns:

A copy of the sensor.couplings dictionary with only couplings containing the specified parameter keys.

Return type:

dict

Examples

Can be used, for example, to return couplings in the rotating wave approximation.

>>> s = rq.Sensor(3)
>>> sinusoid = lambda t: 0 if t<1 else sin(100*t)
>>> f2 = {"states": (0,1), "detuning": 1, "rabi_frequency":2}
>>> f1 = {"states": (1,2), "transition_frequency":100, "rabi_frequency":1, "time_dependence": sinusoid}
>>> s.add_couplings(f1, f2)
>>> gamma = np.array([[.2,0,0],
...                  [.1,0,0],
...                  [0.05,0,0]])
>>> s.set_gamma_matrix(gamma)
>>> print(s.couplings_with("detuning"))
{(0, 1): {'rabi_frequency': 2, 'detuning': 1, 'phase': 0, 'kvec': (0, 0, 0), 'coherent_cc': 1.0, 'label': '(0,1)'}}
decoherence_matrix() ndarray[source]

Build a decoherence matrix out of the decoherence terms of the graph.

For each edge, sums all parameters with a key that begins with “gamma”, and places it on the appropriate location in an adjacency matrix for the couplings graph.

Returns:

The decoherence matrix stack of the system.

Return type:

numpy.ndarray

Examples

>>> s = rq.Sensor(3)
>>> s.add_decoherence((1,0), 0.2, label="foo")
>>> s.add_decoherence((1,0), 0.1, label="bar")
>>> s.add_decoherence((2,0), 0.05)
>>> s.add_decoherence((2,1), 0.05)
>>> print(s.couplings.edges(data=True))
[(1, 0, {'gamma_foo': 0.2, 'label': '(1,0)', 'gamma_bar': 0.1}), (2, 0, {'gamma': 0.05, 'label': '(2,0)'}), (2, 1, {'gamma': 0.05, 'label': '(2,1)'})]
>>> print(s.decoherence_matrix())
[[0.   0.   0.  ]
 [0.3  0.   0.  ]
 [0.05 0.05 0.  ]]

Decoherences can be stacked just like any parameters of the Hamiltonian:

>>> s = rq.Sensor(3)
>>> gamma = np.linspace(0,0.5, 11)
>>> s.add_decoherence((1,0), gamma)
>>> print(s.decoherence_matrix().shape)
(11, 3, 3)

Defining decoherences between states labelled with string values works just like coherent couplings:

>>> s = rq.Sensor(['g', 'e1', 'e2'])
>>> s.add_decoherence(('e1', 'g'), 0.1)
>>> s.add_decoherence(('e2', 'g'),0.1)
>>> print(s.decoherence_matrix())
[[0.  0.  0. ]
 [0.1 0.  0. ]
 [0.1 0.  0. ]]
dm_basis() ndarray[source]

Generate basis labels of density matrix components.

The basis corresponds to the elements in the solution. This is not the complex basis of the sensor class, but rather the real basis of a solution after calling one of rydiqule’s solvers. This means that the ground state population has been removed and it has been transformed to the real basis.

Returns:

Array of string labels corresponding to the solving basis. Is a 1-D array of length n**2-1.

Return type:

numpy.ndarray

Examples

>>> s = rq.Sensor(3)
>>> print(s.dm_basis())
['10_real' '20_real' '10_imag' '11_real' '21_real' '20_imag' '21_imag'
 '22_real']
eta: float | None = None

Noise density prefactor, in units of root(Hz). Must be specified when using Sensor. Automatically calculated when using Cell.

get_couplings() Dict[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], Dict][source]

Returns the couplings of the system as a dictionary

Deprecating in favor of calling the couplings.edges attribute directly.

Returns:

A dictionary of key-value pairs with the keys corresponding to levels of transition, and the values being dictionaries of coupling attributes.

Return type:

dict

get_doppler_shifts() ndarray[source]

Returns the Hamiltonian with only detunings set to the most probable doppler shift values for each spatial dimension.

Determining if a float should be treated as zero is done using numpy.isclose, which has default absolute tolerance of 1e-08.

Returns:

Array of shape (used_spatial_dim,n,n), Hamiltonians with only the doppler shifts present along each non-zero spatial dimension specified by the fields’ “kvec” parameter.

Return type:

numpy.ndarray

get_hamiltonian() ndarray[source]

Creates the Hamiltonians from the couplings defined by the fields.

They will only be the steady state hamiltonians, i.e. will only contain terms which do not vary with time. Implicitly creates hamiltonians in “stacks” by creating a grid of all supported coupling parameters which are lists. This grid of parameters will not contain rabi-frequency parameters which vary with time and are defined as list-like. Rather, the associated axis will be of length 1, with the scanning over this value handled by the get_time_couplings() function.

For m list-like parameters x1,x2,…,xm with shapes N1,N2,…,Nm, and basis size n, the output will be shape (N1,N2,...,Nm, n, n). The dimensions N1,N2,…Nm are labeled by the output of axis_labels().

If any parameters have been zipped with the _zip_parameters() method, those parameters will share an axis in the final hamiltonian stack. In this case, if axis N1 and N2 above are the same shape and zipped, the final Hamiltonian will be of shape (N1,...,Nm, n, n).

In the case where the basis of the Sensor was explicitly defined with a list of states, the ordering of rows and columns in the hamiltonian corresponds to the ordering of states passed in the basis.

See rydiqule’s conventions for matrix stacking for more details.

Returns:

The complex hamiltonian stack for the sensor.

Return type:

np.ndarray

Examples

>>> s = rq.Sensor(3)
>>> det = np.linspace(-1,1,11)
>>> blue = {"states":(0,1), "rabi_frequency":1, "detuning":det}
>>> red = {"states":(1,2), "rabi_frequency":3, "detuning":det}
>>> s.add_couplings(red, blue)
>>> print(s.get_hamiltonian().shape)
(11, 11, 3, 3)

Time dependent couplings are handled separately. The axis that contains array-like parameters with time dependence is length 1 in the steady-state Hamiltonian.

>>> s = rq.Sensor(3)
>>> rabi = np.linspace(-1,1,11)
>>> step = lambda t: 0 if t<1 else 1
>>> blue = {"states":(0,1), "rabi_frequency":rabi, "detuning":1}
>>> red = {"states":(1,2), "rabi_frequency":rabi, "detuning":0, 'time_dependence': step}
>>> s.add_couplings(red, blue)
>>> print(s.get_hamiltonian().shape)
(11, 1, 3, 3)

Zipping parameters means they share an axis in the Hamiltonian.

>>> s = rq.Sensor(3)
>>> s.add_coupling(states=(0,1), detuning=np.arange(11), rabi_frequency=2)
>>> s.add_coupling(states=(1,2), detuning=0.5*np.arange(11), rabi_frequency=1)
>>> s.zip_parameters({(0,1):"detuning", (1,2):"detuning"})
>>> H = s.get_hamiltonian()
>>> print(H.shape)
(11, 3, 3)

If the basis is provided as a list of string labels, the ordering of Hamiltonian rows and columns will correspond to the order of states provided.

>>> s = rq.Sensor(['g', 'e1', 'e2'])
>>> s.add_coupling(('g', 'e1'), detuning=1, rabi_frequency=1)
>>> s.add_coupling(('e1', 'e2'), detuning=1.5, rabi_frequency=1)
>>> print(s.get_hamiltonian())
[[ 0. +0.j  0.5+0.j  0. +0.j]
 [ 0.5-0.j -1. +0.j  0.5+0.j]
 [ 0. +0.j  0.5-0.j -2.5+0.j]]
get_hamiltonian_diagonal(values: dict, no_stack: bool = False) ndarray[source]

Apply addition and subtraction logic corresponding to the direction of the couplings.

For a given state n, the path from ground will be traced to n. For each edge along this path, values will be added where the path direction and coupling direction match, and subtracting values where they do not. The sum of all such values along the path is the n th term in the output array.

Designed for internal functions which help generate hamiltonians. Most commonly used to calculate total detunings for ranges of couplings under the RWA

Parameters:
  • values (dict) – Key-value pairs where the keys correspond to transitions (agnostic to ordering of states) and values corresponding to the values to which the logic will be applied.

  • no_stack (bool, optional) – Whether to ignore variable parameters in the system and use only basic math operations rather than reshape the output. Typically only True for calculating doppler shifts.

Returns:

The diagonal of the hamiltonian of the system of shape (*l,n), where l is the shape of the hamiltonian stack for the sensor.

Return type:

numpy.ndarray

get_parameter_mesh() List[ndarray][source]

Returns the parameter mesh of the sensor.

The parameter mesh is the flattened grid of variable parameters in all the couplings of a sensor. Wraps numpy.meshgrid with the indexing argument always "ij" for matrix indexing.

Returns:

list of mesh grids for every variable parameter

Return type:

list of numpy.ndarray

Examples

>>> s = rq.Sensor(3)
>>> rabi1 = np.linspace(-1,1,11)
>>> rabi2 = np.linspace(-2,2,21)
>>> s.add_coupling(states=(0,1), rabi_frequency=rabi1, detuning=1)
>>> s.add_coupling(states=(1,2), rabi_frequency=rabi2, detuning=1)
>>> for p in s.get_parameter_mesh():
...     print(p.shape)
(11, 1)
(1, 21)
get_rotating_frames() dict[source]

Determines the rotating frames for the disconnected subgraphs.

Each returned path gives the states traversed, and the sign gives the direction of the coupling. If the sign is negative, the coupling is going to a lower energy state. Choice of frame depends on graph distance to lowest indexed node on subgraph, ties broken by lowest indexed path traversed first.

Returns:

Dictionary keyed by disconnected subgraphs, values are path dictionaries for each node of the subgraph. Each path shows the node indexes traversed, where a negative sign denotes a transition to a lower energy state.

Return type:

dict

get_time_dependence() List[Callable[[float], complex]][source]

Function which returns a list of the time_dependence functions.

The list is returned with in the order that matches with the time hamiltonians from get_time_couplings() such that the ith element of of the return of this functions corresponds with the ith Hamiltonian terms returned by that function.

Returns:

List of scalar functions, representing all couplings specified with a time_dependence.

Return type:

list

Examples

>>> s = rq.Sensor(3)
>>> step = lambda t: 0 if t<1 else 1
>>> wave = lambda t: np.sin(2000*np.pi*t)
>>> f1 = {"states": (0,1), "transition_frequency":10, "rabi_frequency": 1, "time_dependence":wave}
>>> f2 = {"states": (1,2), "transition_frequency":10, "rabi_frequency": 2, "time_dependence":step}
>>> s.add_couplings(f1, f2)
>>> print(s.get_time_dependence())
[<function <lambda> at ...>, <function <lambda> at ...>]
get_time_hamiltonian(t: float) ndarray[source]

Get the system hamiltonian at a specific time, t.

This sums the steady-state hamiltonians with the time-dependent parts, evaluated at a specific time, t. If there is no time dependence in the system, function is equivalent to get_hamiltonian().

Parameters:

t (float) – Time to evaluate the time-dependence function at when building the hamiltonians

Returns:

System hamiltonian, evaluated at time t.

Return type:

numpy.ndarray

get_time_hamiltonian_components() Tuple[List[ndarray], List[ndarray]][source]

Get time-dependent components of the hamiltonian.

Returns the list of matrices of all couplings in the system defined with a time_dependence key. The output will be two lists of matrices representing terms of the hamiltonian which are dependent on each time-dependent coupling. The lists will be of length M and shape (*l_time, n, n), where M is the number of time-dependent couplings, l_time is time-dependent stack shape (possibly all ones), and n is the basis size. Each matrix will have terms equal to the rabi frequency (or half the rabi frequency under RWA) in positions that correspond to the associated transition. For example, in the case where there is a time_dependence function defined for the (2,3) transition with a rabi frequency of 1, the associated time coupling matrix will be all zeros, with a 1 in the (2,3) and (3,2) positions.

Typically, this function is called internally and multiplied by the output of the get_time_dependence() function.

Returns:

  • list of numpy.ndarray – The list of M (*l,n,n) matrices representing the real-valued time-dependent portion of the hamiltonian. For 0 <= i <= M, the ith value along the first axis is the portion of the matrix which will be multiplied by the output of the ith time_dependence function.

  • list of numpy.ndarray – The list of M (*l,n,n) matrices representing the imaginary-valued time-dependent portion of the hamiltonian. For 0 <= i <= M, the ith value along the first axis is the portion of the matrix which will be multiplied by the output of the ith time_dependence function.

Examples

>>> s = rq.Sensor(3)
>>> step = lambda t: 0 if t<1 else 1
>>> wave = lambda t: np.sin(2000*np.pi*t)
>>> f1 = {"states": (0,1), "transition_frequency":10, "rabi_frequency": 1, "time_dependence":wave}
>>> f2 = {"states": (1,2), "transition_frequency":10, "rabi_frequency": 2, "time_dependence":step}
>>> s.add_couplings(f1, f2)
>>> time_hams, time_hams_i = s.get_time_hamiltonian_components()
>>> for H in time_hams:
...     print(H)
[[0.+0.j 1.+0.j 0.+0.j]
 [1.-0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 0.+0.j]]
[[0.+0.j 0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j 2.+0.j]
 [0.+0.j 2.-0.j 0.+0.j]]

To handle stacking across the steady-state and time hamiltonians, the dimensions are matched in a way that broadcasting works in a numpy-friendly way

>>> s = rq.Sensor(3)
>>> rabi = np.linspace(-1,1,11)
>>> step = lambda t: 0 if t<1 else 1
>>> blue = {"states":(0,1), "rabi_frequency":rabi, "detuning":1}
>>> red = {"states":(1,2), "rabi_frequency":rabi, "detuning":0, 'time_dependence': step}
>>> s.add_couplings(red, blue)
>>> time_hams, time_hams_i = s.get_time_hamiltonian_components()
>>> print(s.get_hamiltonian().shape)
(11, 1, 3, 3)
>>> print(time_hams[0].shape)
(1, 11, 3, 3)
>>> print(time_hams_i[0].shape)
(1, 11, 3, 3)
get_transition_frequencies() ndarray[source]

Gets an array of the diagonal elements of the Hamiltonian from the field detunings.

Wraps the get_hamiltonian_diagonal() function using both transition frequencies and detunings. Primarily for internal use.

Returns:

N-D array of the hamiltonian diagonal. For an n-level system with stack shape *l, will be shape (*l, n)

Return type:

numpy.ndarray

get_value_dictionary(key: str) dict[source]

Get subset of dictionary coupling parameters.

Return a dictionary of key value pairs where the keys are couplings added to the system and the values are the value of the parameter specified by key. Produces an output that can be passed directly to get_hamiltonian_diagonal(). Only couplings whose parameter dictionaries contain “key” will be in the returned dictionary.

Parameters:

key (str) – String value of the parameter name to build the dictionary. For example, get_value_dictionary("detuning") will return a dictionary with keys corresponding to transitions and values corresponding to detuning for each transition which has a detuning.

Returns:

Coupling dictionary with couplings as keys and corresponding values set by input key.

Return type:

dict

Examples

>>> s = rq.Sensor(4)
>>> f1 = {"states": (0,1), "detuning": 2, "rabi_frequency": 1}
>>> f2 = {"states": (1,2), "detuning": 3, "rabi_frequency": 2}
>>> step = lambda t: 1 if t>1 else 0
>>> f3 = {"states": (2,3), "rabi_frequency": 3, "transition_frequency": 3, "time_dependence":step}
>>> s.add_couplings(f1, f2, f3)
>>> print(s.get_value_dictionary("detuning"))
{(0, 1): 2, (1, 2): 3}
group_variable_parameters(apply_mesh: bool = False) List[List[Tuple[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], str, ndarray, str | None]]][source]
int_states_map(invert: bool = False) Dict[int | str | Tuple[float, ...], int] | Dict[int, int | str | Tuple[float, ...]][source]

Get a dictionary mapping between state labels and their corresponding integer ordering. Can be returned with key:value pairs defined either by label:int or int:label, controlled via optional invert argument.

Parameters:

invert (bool, optional) – Whether to switch the role of keys and values. Labels are keys if False, and values if True, by default False

Returns:

Dictionary mapping between state labels and integer ordering

Return type:

dict

kappa: float | None = None

Differential prefactor, in units of (rad/s)/m. Must be specified when using Sensor. Automatically calculated when using Cell.

probe_freq: float | None = None

Probing transition frequency, in rad/s.

property probe_tuple: Tuple[int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...], int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...]] | None

Coupling edge that corresponds to the probing field. Defaults to None and gets set to the first coupling added to the system with add_coupling(). Can be modified directly.

set_experiment_values(probe_freq: float, kappa: float, eta: float | None = None, cell_length: float | None = None, beam_area: float | None = None, v_th: float | None = None, temp: float | None = None, atom_mass: float | None = None)[source]

Sets attributes needed for observable calculations.

Parameters:
  • probe_tuple (tuple of StateSpec) – Coupling that corresponds to the probing field. If None, corresponding Sensor attribute remains unchanged. Defaults to None.

  • probe_freq (float) – Frequency of the probing transition, in rad/s. If None, corresponding Sensor attribute remains unchanged. Defaults to None.

  • kappa (float) – Numerical prefactor that defines susceptibility, in (rad/s)/m. If None, corresponding Sensor attribute remains unchanged. Defaults to None. See get_susceptibility() and Cell.kappa for details.

  • eta (float) – Noise-density prefactor, in root(Hz). If None, corresponding Sensor attribute remains unchanged. Defaults to None. See Cell.eta for details.

  • cell_length (float, optional) – The optical path length through the medium, in meters. If None, corresponding Sensor attribute remains unchanged. Defaults to None.

  • beam_area (float, optional) – The cross-sectional area of the beam, in m^2. If None, corresponding Sensor attribute remains unchanged. Defaults to None.

  • v_th (float, optional) – Thermal velocity of the Maxwell-Boltzmann distribution: v_th = (k_B*T/m)^(1/2) in units of m/s. Defaults to None.

  • temp (float, optional) – Temperature of the vapor cell in units of Kelvin. Defaults to None.

  • atom_mass (float, optional.) – Mass of a sensing atom in kg. Defaults to None.

set_gamma_matrix(gamma_matrix: ndarray)[source]

Set the decoherence matrix for the system.

Works by first removing all existing decoherent data from graph edges, then individually adding all nonzero terms of a provided gamma matrix to the corresponding graph edges. Can be used to set all decoherence attributes to edges simultaneously, but add_decoherence() is preferred.

Unlike add_decoherence(), does not support scanning multiple decoherence values, rather should be used to set the decoherences of the system to individual static values.

Parameters:

gamma_matrix (numpy.ndarray) – Array of shape (basis_size, basis_size). Element (i,j) describes the decoherence rate, in Mrad/s, from state i to state j.

Raises:
  • RydiquleError – If gamma_matrix is not a numpy array.

  • ValueError – If gamma_matrix is not a square matrix of the appropriate size

  • ValueError – If the shape of gamma_matrix is not compatible with self.basis_size.

Examples

>>> s = rq.Sensor(2)
>>> f1 = {"states": (0,1), "detuning":1, "rabi_frequency": 1}
>>> s.add_couplings(f1)
>>> gamma = np.array([[.1,0],[.1,0]])
>>> s.set_gamma_matrix(gamma)
>>> print(s.decoherence_matrix())
[[0.1 0. ]
 [0.1 0. ]]
spatial_dim() int[source]

Returns the number of spatial dimensions doppler averaging will occur over.

Determining if a float should be treated as zero is done using numpy.isclose, which has default absolute tolerance of 1e-08.

Returns:

Number of dimensions, between 0 and 3, where 0 means no doppler averaging k-vectors have been specified or are too small to be calculates.

Return type:

int

Examples

No spatial dimensions specified

>>> s = rq.Sensor(2)
>>> s.add_coupling((0,1), detuning = 1, rabi_frequency=1)
>>> print(s.spatial_dim())
0

One spatial dimension specified

>>> s = rq.Sensor(2)
>>> s.add_coupling((0,1), detuning = 1, rabi_frequency=1, kvec=(0,0,4))
>>> print(s.spatial_dim())
1

Multiple spatial dimensions can exist in a single coupling or across multiple couplings

>>> s = rq.Sensor(2)
>>> s.add_coupling((0,1), detuning = 1, rabi_frequency=1, kvec=(3,0,3))
>>> print(s.spatial_dim())
2
>>> s = rq.Sensor(3)
>>> s.add_coupling((0,1), detuning = 1, rabi_frequency=1, kvec=(3,0,3))
>>> s.add_coupling((1,2), detuning = 2, rabi_frequency=2, kvec=(0,4,0))
>>> print(s.spatial_dim())
3
property states: List[int | str | Tuple[float, ...]]

Property which gets a list of labels for the sensor in the order defined in __init__(). This is also the order corresponding the rows and columns in the system Hamiltonian and decoherence matrix.

Returns:

List of states of the system defined the constructor, in the order corresponding to rows and columns of the Hamiltonian.

Return type:

list

states_with_spec(statespec: int | str | Tuple[float, ...] | List[int | str | Tuple[float, ...]] | Tuple[float | List[float], ...]) List[int | str | Tuple[float, ...]][source]

Return a list of all states in the sensor matching the state_spec pattern.

A state is considered a “match” if, for each element of the state, the corresponding element of statespec is either exactly the floating point or string value, or a list containing that element of state. In this way, groups of states can be specified more tersely than a complete list of all states.

Parameters:

statespec – The StateSpec against which state labels in sensor are to be matched

Returns:

All the states in the sensor matching the given specification.

Return type:

list of State

Examples

>>> states = [
...    (0,0),
...    (1,-1),
...    (1,0),
...    (1,1)
... ]
>>> s = rq.Sensor(states)
>>> s.states_with_spec((1,[-1,0,1]))
[(1, -1), (1, 0), (1, 1)]
temp: float | None = None

Temperature of the vapor cell, in Kelvin.

unzip_parameters(zip_label: str, verbose: bool | None = True)[source]

Remove a set of zipped parameters from the internal zip_labels list.

If an element of the internal _zip_labels array matches the label provided, removes it from _zip_labels. If no such element is present in _zip_labels, does nothing, and prints a message (disabled with verbose=False)

Parameters:
  • zip_label (str) – The string label corresponding the key to be deleted in the _zip_labels attribute.

  • verbose (bool) – Whether to print a message if the unzip fails due to the specified zip_label not being a zip in the sensor. If True prints a message to std out if zip_label is not an element of the internal self._zip_labels. Otherwise, fails silently. Can be used if unzipping as part of an automated script.

Notes

Note

This function should always be used rather than modifying the _zip_labels attribute directly.

Examples

>>> s = rq.Sensor(3)
>>> det = np.linspace(-1,1,11)
>>> s.add_coupling(states=(0,1), detuning=det, rabi_frequency=1, label="probe")
>>> s.add_coupling(states=(1,2), detuning=det, rabi_frequency=1)
>>> s.zip_parameters({"probe":"detuning", (1,2):"detuning"}, zip_label="demo1")
>>> print(s._zip_labels) #NOT modifying directly
['demo1']
>>> print(s.couplings.edges(data="demo1"))
[(0, 1, 'detuning'), (1, 2, 'detuning')]
>>> s.unzip_parameters("demo1")
>>> print(s._zip_labels) #NOT modifying directly
[]
>>> print(s.couplings.edges(data="demo1"))
[(0, 1, None), (1, 2, None)]

If the labels provided are not a match, a message is printed and nothing is altered. In the case where simulations are scripted and the printed message is annoying, the print behavior can be modified with verbose=False, potentially useful for scripting cases where the desired behavior is to silently continue over non-existent zip labels.

>>> s = rq.Sensor(3)
>>> det = np.linspace(-1,1,11)
>>> s.add_coupling(states=(0,1), detuning=det, rabi_frequency=1, label="probe")
>>> s.add_coupling(states=(1,2), detuning=det, rabi_frequency=1)
>>> s.zip_parameters({"probe":"detuning", (1,2):"detuning"})
>>> print(s._zip_labels) #NOT modifying directly
['zip_0']
>>> print(s.couplings.edges(data="zip_0"))
[(0, 1, 'detuning'), (1, 2, 'detuning')]
>>> s.unzip_parameters("zipp0")
No label matching zipp0, no action taken
>>> print(s._zip_labels) #NOT modifying directly
['zip_0']
>>> print(s.couplings.edges(data="zip_0"))
[(0, 1, 'detuning'), (1, 2, 'detuning')]
property vP: float

Most probable speed of the 3D Maxwell-Boltzmann distribution.

This is defined as \(\sqrt{2kT/m}\) and is given in units of m/s.

This must be defined manually when performing Doppler-broadened solves. Accessing it before definition will raise an error.

v_th: float | None = None

Thermal velocity of the atoms in vapor cell, in meters per second.

variable_parameter_sort(par: tuple) tuple[source]

Assistance function which determines the sorting order of elements parameters in sensor.

Called in variable_parameters() to ensure a consistent sort order. Provided as the key parameter in python’s sorted() function before parameters are returned.

Sorts first by zip_label, then by states, then by parameter. Ensures all parameters zipped with one another are grouped together in a list. Zipped parameters will always come first. From there, parameters are sorted alphabetically by zip_label (including case), then by state pair (as determined by ordering in the sensor, NOT alphabetically), then alphabetically by parameter.

Parameters:

par (tuple) – 4-element list of information on each parameter. Consists of (states, parameter, value, zip_label)

Returns:

3-element tuple that defines a particular parameter’s position in the final sorting order.

Return type:

tuple

variable_parameters(apply_mesh: bool = False) List[Tuple[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], str, ndarray, str | None]][source]

Property to retrieve the values of parameters that were stored on the graph as arrays.

Values are returned as a list of tuples in the standard order of pythons default sorting, applied first to the tuple indicating states and then to the key of the parameter itself. This means that couplings are sorted first by lower state, then by upper state, then alphabetically by the name of the parameter.To determine order, all state labels treated as their integer position in the basis as determined by ordering in the constructor __init__().

Returns:

A list of tuples corresponding to the parameters of the systems that are variable (i.e. stored as an array). They are ordered according to states, then according to variable name. Tuple entries of the list take the form (states, param_name, value)

Return type:

list of tuples

Examples

>>> s = rq.Sensor(3)
>>> vals = np.linspace(-1,2,3)
>>> s.add_coupling(states=(1,2), rabi_frequency=vals, detuning=1)
>>> s.add_coupling(states=(0,1), rabi_frequency=vals, detuning=vals)
>>> print(s.variable_parameters())
[((0, 1), 'detuning', array([-1. ,  0.5,  2. ]), None),
 ((0, 1), 'rabi_frequency', array([-1. ,  0.5,  2. ]), None),
 ((1, 2), 'rabi_frequency', array([-1. ,  0.5,  2. ]), None)]

The order is important; in the unzipped case, it will sort as though all state labels were cast to strings, meaning integers will always be treated as first.

>>> s = rq.Sensor([0, 'e1', 'e2'])
>>> det1 = np.linspace(-1, 1, 3)
>>> det2 = np.linspace(-1, 1, 5)
>>> blue = {"states":(0,'e1'), "rabi_frequency":1, "detuning":det1}
>>> red = {"states":('e1','e2'), "rabi_frequency":3, "detuning":det2}
>>> s.add_couplings(blue, red)
>>> print(s.variable_parameters())
[((0, 'e1'), 'detuning', array([-1.,  0.,  1.]), None),
 (('e1', 'e2'), 'detuning', array([-1. , -0.5,  0. ,  0.5,  1. ]), None)]
>>> print(f"Axis Labels: {s.axis_labels()}")
Axis Labels: ['(0,e1)_detuning', '(e1,e2)_detuning']
zip_parameters(parameters: Dict[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]] | str, str], zip_label: str | None = None)[source]

Define 2 scannable parameters as “zipped” so they are scanned in parallel.

Zipped parameters will share an axis when quantities relevant to the equations of motion, such as the gamma_matrix and hamiltonian are generated. So for 2 list-like parameters, the first elements in each are solved at the same time, then the second, etc Note that calling this function does not affect internal quantities directly, but flags them to be zipped at calculation time for relevant quantities.

Internally, adds the label value to the internal list of zipped parameter labels, and adds a flag in the form of <label>:<parameter_name> to each edge of the graph.

Parameters:
  • parameters (dict) – Parameter labels to scan together. Parameters are specified with a dictionary keyed by the either pair of states defining the coupling (e.g. (0,1)) or a previously specified label (e.g. "probe") with items corresponding to the respective parameter name (e.g. "detuning").

  • zip_label (optional, str) – String label shorthand for the zipped parameters. The label for the axis of these parameters in axis_labels(). Does not affect functionality of the Sensor. If None (the default), the label used will be "zip_" + <number>, where <number> is the one index beyond the current length of the zip_parameters list.

Raises:
  • RydiquleError – If fewer than 2 labels are provided.

  • RydiquleError – If any of the 2 labels are the same.

  • RydiquleError – If the label contains the substring "gamma", as this is used internally for decoherence matrix generation.

  • RydiquleError – If any elements of labels are not labels of couplings in the sensor.

  • RydiquleError – If any of the parameters specified by labels are already zipped.

  • RydiquleError – If any of the parameters specified are not list-like.

  • RydiquleError – If all list-like parameters are not the same length.

Notes

Note

This function should be called last after all Sensor couplings and dephasings have been added. Changing a coupling that has already been zipped removes it from the self.zipped_parameters list.

Note

Modifying the Sensor._zip_labels attribute directly can break some functionality and should be avoided. Use this function or unzip_parameters() instead.

Note

When defining the zip strings for states labelled with strings, be sure to additional ' or " characters on either side of the labels, as demonstrated in the second example below.

Examples

>>> s = rq.Sensor(3)
>>> det = np.linspace(-1,1,11)
>>> s.add_coupling(states=(0,1), detuning=det, rabi_frequency=1, label="probe")
>>> s.add_coupling(states=(1,2), detuning=det, rabi_frequency=1)
>>> s.zip_parameters({"probe":"detuning", (1,2):"detuning"}, zip_label="detunings")
>>> print(s._zip_labels) #NOT modifying directly
['detunings']
>>> print(s.couplings.edges(data="detunings"))
[(0, 1, 'detuning'), (1, 2, 'detuning')]
>>> print(s.get_hamiltonian().shape)#zipped parameters share an axis
(11, 3, 3)

Especially when states are labelled with tuples, specifying zips parameters with the states they couple can be cumbersome. In this case, it can be useful to either assign variables to the tuples defining the states, or to label the couplings.

>>> g = (0,0)
>>> e1, e2 = (1,-1), (1, 1)
>>> s = rq.Sensor([g, e1, e2])
>>> arr = np.linspace(-1,1,11)
>>> s.add_coupling((g,e1), detuning=arr, rabi_frequency=1, label="probe")
>>> s.add_coupling((e1,e2), detuning=arr, rabi_frequency=1, label="coupling")
>>> s.zip_parameters({((0,0),(1,-1)):"detuning", ((1,-1), (1, 1)):"detuning"}, zip_label="foo") #clunky
>>> print(s._zip_labels)
['foo']
>>> s.unzip_parameters("foo")
>>> s.zip_parameters({"probe":"detuning", "coupling":"detuning"}, zip_label="bar") #readable
>>> print(s._zip_labels)
['bar']

For maximum flexibility, any parameters specified as arrays with matching lengths can be zipped. This should be used with care, as some parameter combinations can be nonsensical.

>>> s = rq.Sensor(3)
>>> arr = np.linspace(-1,1,11)
>>> s.add_energy_shift(0, 0.5*arr)
>>> s.add_coupling(states=(0,1), detuning=arr, rabi_frequency=1, label="probe")
>>> s.add_coupling(states=(1,2), detuning=arr, rabi_frequency=1)
>>> s.add_decoherence((1,0), 0.1*arr)
>>> s.zip_parameters({(0,0):"e_shift", "probe":"detuning", (1,2):"detuning", (1,0):"gamma"}, zip_label="foo")
>>> print(s._zip_labels) #NOT modifying directly
['foo']
>>> print(s.couplings.edges(data="foo"))
[(0, 0, 'e_shift'), (0, 1, 'detuning'), (1, 2, 'detuning'), (1, 0, 'gamma')]
>>> print(s.get_hamiltonian().shape)
(11, 3, 3)
zip_zips(*zip_labels: str, new_label: str | None = None)[source]

Combine multiple parameter zips into a single zip.

Given any number of labels of zips in the sensor, combines them so that they will all share a single axis in the stack. Note that this will override all previous zips in zip_labels, and they cannot be recovered.

Parameters:

new_label (string, optional) – Label for the new zip that will replace the ones provided in zip_labels. If None, will be generated by joining all the strings of zip_labels with a “_” character, by default None.

Raises:
  • RydiquleError – If any of zip_labels do not exist in the Sensor.

  • RydiquleError – If new_label contains a protected substring (such as “gamma”).

  • RydiquleError – If any of zip_labels are the same.

  • RydiquleError – If any of the dimensions of the axes specified by zip_labels do not match.

Examples

>>> s = rq.Sensor(5)
>>> det = np.linspace(-1, 1, 11)
>>> s.add_coupling((0, [1,2]), rabi_frequency=1, detuning=det, label="foo")
>>> s.add_coupling((0, [3,4]), rabi_frequency=1, detuning=det, label="bar")
>>> print(s.get_hamiltonian().shape)
(11, 11, 5, 5)
>>> print(s.axis_labels())
['bar_detuning', 'foo_detuning']
>>> s.zip_zips("foo_detuning", "bar_detuning", new_label="foobar_detuning")
>>> print(s.get_hamiltonian().shape)
(11, 5, 5)
>>> print(s.axis_labels())
['foobar_detuning']