rydiqule.cell.Cell¶
- class rydiqule.cell.Cell(atom_flag: Literal['H', 'Li6', 'Li7', 'Na', 'K39', 'K40', 'K41', 'Rb85', 'Rb87', 'Cs'], atomic_states: List[A_QState], cell_length: float = 0.001, gamma_transit: float | None = None, gamma_mismatch: str | dict = 'ground', beam_area: float = 1e-06, beam_diam: float | None = None, temp: float = 300.0)[source]¶
Bases:
Sensor
Subclass of
Sensor
that creates a Sensor with additional physical properties corresponding to a specific Rydberg atom.In addition to the core functionality of
~.Sensor
, this class requires labelling states withnamedtuple`s of quantum numbers, automatically calculating of state lifetimes and decoherences and tracking of of some physical laser parameters. A key distictinction between a :class:`~.Cell
and aSensor
is that a cell supports (and requires) and absolute ordering of energy between states, which allows for implicit calculation of decay rates and transition frequencies.- __init__(atom_flag: Literal['H', 'Li6', 'Li7', 'Na', 'K39', 'K40', 'K41', 'Rb85', 'Rb87', 'Cs'], atomic_states: List[A_QState], cell_length: float = 0.001, gamma_transit: float | None = None, gamma_mismatch: str | dict = 'ground', beam_area: float = 1e-06, beam_diam: float | None = None, temp: float = 300.0) None [source]¶
Initialize the Rydberg cell from the given parameters.
- Parameters:
atom_flag (str) – Which atom is used in the cell for calculating physical properties with ARC Rydberg. One of {‘H’, ‘Li6’, ‘Li7’, ‘Na’, ‘K39’, ‘K40’, ‘K41’, ‘Rb85’, ‘Rb87’, ‘Cs’}.
atomic_states (list of A_QState) –
List of
A_QState
representing the states of the atom. More details about theA_QState
class can be found in its documentation, but it includes the elemends (n
,l
,j
,m_j
,f
,m_f
). These represent the usual Hydrogen-like atom quantum numbers with the usual restrictions:n
must be a positive integer.l
must be a non-negative integer less thann
.j
must be a positive half-integer such that \(j=l \pm \frac{1}{2}\)m_j
must be a half integer such that \(-j \leq m_j \leq +j\)f
must be an integer satisfying \(|j-I| \leq f \leq (j+I)\)m_f
must be an integer such that \(-f \leq m_f \leq +f\)
Additionally,
n
,`l`, andj
must always be specified, in addition to the following restrictions on other quantum numbers:m_j
cannot be specified withf
If
m_f
is specified,f
must also be specified.
All quantum numbers can be specified with lists of valid values, in which case the they will be expanded into a list of states, with one corresponding to each value. If multiple quantum numbers are specified with lists, the resulting list of states will contain all combinations of values. Furthermore, the
j
,m_j
,f
, andm_f
quantum numbers can each be specifed using"all"
, which corresponds to a list of all physically allowed values for that quantum number. This convention allows quick specifications of entire manifolds of states to be added to the sensor. See theExamples
section to see how to use these powerful specifications.cell_length (float) – Length of the atomic vapor in meters.
gamma_transit (float, optional) – Decoherence due to atom transit through the optical beams. Specified in units of Mrad/s. If
None
, will calculate based on value ofbeam_area
. Seeadd_transit_broadening()
for details on how transit broadening is treated. Default is None.gamma_mismatch (str or dict) –
How to resolve discrepancies between calculated eacb state lifetime and the sum of all transtion rates out of each state. In practice, these discrepancies are a result of transition pathways which exist between states when one is accounted for is
states
and one is not. For example, if there is a decoherent transition between states 3->1 and states 3->2, butstates
only includes states 0, 1, and 3, the calculatedgamma_lifetime
of state 3 will be greater than the sum of all computedgamma_transition
values. In many cases, it is desirable to account for this decoherence in other ways. The options for handling the dicrepancy are:"ground"
which adds a decoherent coupling coupling between a states
with a discrepancy \(\Delta \gamma\) and divides \(\Delta \gamma\) among all the ground states (states matching then,l,j
values of the lowest energy state)."all"
which divides \(\Delta \gamma\) amongst all states in theCell
which already have a"gamma_transition"
value. The fraction each transition gets is weighted by the fraction of the total thdat transitions"gamma_transition"
value accounts for."none"
which will not account for this discrepancy at all. In this case this physics is not guaranteed to be accurate and it is assumed the decoherence will be accounted for manually in other ways usingadd_decoherence()
In all cases, the accounting is done by adding a
"gamma_mismatch"
value to each relevant edge, which will subsequently be accounted for whendecoherence_matrix()
is called. Note that older versions ofrydiqule
did not have this option and implicitly used the"ground"
option, and"ground"
is the current default.beam_area (float, optional) – Area of probing field cross-section in m^2. Used to calculate
kappa
andgamma_transit
. Default is 1e-6.beam_diam (float, optonal) – Diameter of the probing field cross section in meters. Used to calculate
gamma_transit
. IfNone
, it is calculated frombeam_area
assuming the beam cross-section is a circle. Default isNone
.temp (float, optional) – Temperature of the gas in Kelvin. Used in calculations of enery level lifetime. Default is 300 K.
- Raises:
RydiquleError – If at least two atomic states are not provided.
AtomError – If atom_flag is not one of ARC’s supported alkali atoms.
- Warns:
NLJWarning – If called using old-style state specification
Examples
All the hyperfine states for the D1 line of Rubidium-87 can be defined as follows.
>>> from rydiqule import A_QState >>> D1_g = A_QState(5, 0, 0.5, f="all", m_f="all") >>> D1_e = A_QState(5, 1, 0.5, f="all",m_f="all") >>> c = rq.Cell("Rb87",[D1_e, D1_g]) >>> for state in c.states: ... print(state) (5, 1, 0.5, f=1.0, m_f=-1.0) (5, 1, 0.5, f=1.0, m_f=0.0) (5, 1, 0.5, f=1.0, m_f=1.0) (5, 1, 0.5, f=2.0, m_f=-2.0) (5, 1, 0.5, f=2.0, m_f=-1.0) (5, 1, 0.5, f=2.0, m_f=0.0) (5, 1, 0.5, f=2.0, m_f=1.0) (5, 1, 0.5, f=2.0, m_f=2.0) (5, 0, 0.5, f=1.0, m_f=-1.0) (5, 0, 0.5, f=1.0, m_f=0.0) (5, 0, 0.5, f=1.0, m_f=1.0) (5, 0, 0.5, f=2.0, m_f=-2.0) (5, 0, 0.5, f=2.0, m_f=-1.0) (5, 0, 0.5, f=2.0, m_f=0.0) (5, 0, 0.5, f=2.0, m_f=1.0) (5, 0, 0.5, f=2.0, m_f=2.0)
Methods
__init__
(atom_flag, atomic_states[, ...])Initialize the Rydberg cell from the given parameters.
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 dechorences 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[, ...])Overload of
add_single_coupling()
, which allows for alternate specifications and automatic calculations of some parameter.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[, ...])Overload of the
add_transit_broadening()
method ofSensor
which automatically populates therepop
dictionary with a equal decay to all sublevels of the ground(n, l, j)
state.Get a list of axis labels for stacked hamiltonians.
couplings_with
(*keys[, method])Returns a version of self.couplings with only the keys specified.
Build a decoherence matrix out of the decoherence terms of the graph.
dm_basis
()Generate basis labels of density matrix components.
Returns the couplings of the system as a dictionary
Returns the Hamiltonian with only detunings set to the most probable doppler shift values for each spatial dimension.
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.
Returns the parameter mesh of the sensor.
Determines the rotating frames for the disconnected subgraphs.
Function which returns a list of the
time_dependence
functions.Get the system hamiltonian at a specific time, t.
Get time-dependent components of the hamiltonian.
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.
Return a list of the states in the
Cell
in ascending energy order.set_experiment_values
(probe_freq, kappa[, ...])Sensor
specific method.set_gamma_matrix
(gamma_matrix)Set the decoherence matrix for the system.
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.
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
Mass of an atom in the vapor cell, in kilograms.
Property to return the number of nodes on the Sensor graph.
Cross-sectional area of the probing beam, in square meters.
Optical path length of the medium, in meters.
Get the eta value for the system.
Property to calculate the kappa value of the system.
Get the probe transition frequency, in rad/s.
Coupling edge that corresponds to the probing field.
Property which gets a list of labels for the sensor in the order defined in
__init__()
.Temperature of the vapor cell, in Kelvin.
Most probable speed of the 3D Maxwell-Boltzmann distribution.
Thermal velocity of the atoms in vapor cell, in meters per second.
- _add_coherent_data(states: Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], **field_params) None ¶
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 dictionry of couplings parameters. For details on the keys of the dictionry see
add_coupling()
.
- _add_decoherence_rates()[source]¶
Helper function to add natural decay rates to all transitions in the cell. Values for decay rates are calculated with arc, and skipped if selection rules prohibit the transition or if the second state is a higher energy than the first state
- _add_gamma_mismatch_to_all(state: A_QState)[source]¶
Helper function which implements the “all” option of
_add_gamma_mismatches()
for a single statestate
.
- _add_gamma_mismatch_to_ground(state: A_QState)[source]¶
Helper function which implements the “ground” option of
_add_gamma_mismatches()
for a single statestate
.
- _add_gamma_mismatches(method: str | dict = 'ground')[source]¶
Adds couplings to the graph accounting for differences between computed lifetimes and decay rates.
In a cell in which all atomic states are accounted for, the computed values of
gamma_lifetime
for a particular state will be equal to the sum of allgamma_transition
values on edges leaving that state. However, it is not always desirable to account for all states in this way for simplicity or computational complexity reasons. This function allows theCell
to account for any differences in these values that arise as a result of excluding physicalstates from aCell
. There are multiple ways to resolve these discrepancies, specified by themethod
argument, which is detailed in theParameters
section.- Parameters:
method (str or dict mapping states to str) –
The method by which discrepancies in computed values are resolved. The available methods are as follows:
"ground"
which adds a decoherent coupling coupling between a states
with a discrepancy \(\Delta \gamma\) and divides \(\Delta \gamma\) among all the ground states (states matching then,l,j
values of the lowest energy state)."all"
which divides \(\Delta \gamma\) amongst all states in theCell
which already have a"gamma_transition"
value. The fraction each transition gets is weighted by the fraction of the total thdat transitions"gamma_transition"
value accounts for. If this method is used, every state in theCell
must have at least one dipole-allowed decay path."none"
which will not account for this discrepancy at all. In this case this physics is not guaranteed to be accurate and it is assumed the decoherence will be accounted for manually in other ways using"~.add_decoherence"()
Note that in addition to one of these strings
method
can be a dictionary which maps states to one of these strings. In this case, the discrepancy is resolved separately for each state using the method specified for that state. Defaults to “ground”- Raises:
ValueError – If the method is not one of the allowed strings or a dictionary mapping states to one of the allowed strings.
RydiquleError – If the “all” option is selected for
method
and any of the states have no lower state to decay to.
- _add_state_energies()[source]¶
Helper function to add all the “energy” key to all the nodes of the graph for the state energies relative to the first state.
All energies are relative to the ground state, defined as the \(nS^{1/2}\) state, where n is the principle quantum number of the lowest energy atomic state.
- _add_state_lifetimes()[source]¶
Helper function to add all “gamma_lifetime” key-value appropriate to the atom to all nodes on the graph.
- _coupling_with_label(label: str | Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]]) Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]] ¶
Helper function to return the pair of states corresponding to a particular label string. For internal use.
- _expand_dims()¶
Converts the 1-D arrays in the sensor into shapes that allows for rydiqule stacking.
- _remove_edge_data(states: Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], kind: str)¶
Helper function to remove all data that was added with a
add_coupling()
call oradd_decoherence()
call. Needed to ensure that two nodes do not have coherent couplings pointing both ways and to invalidate existing zip parameter couplings.- Parameters:
- Raises:
RydiquleError – If
kind
is not'coherent'
and doesn’t begin with'gamma'
- _stack_shape(time_dependence: Literal['steady', 'time', 'all'] = 'all') Tuple[int, ...] ¶
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, ...]] ¶
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:
- 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.
- _validate_input_states(atomic_states: List[A_QState])[source]¶
Helper function to check that input states are compatible and defined
- 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)¶
Add a coupling between states or groups of states.
Wraps the
add_single_coupling()
andadd_coupling_group()
functions, and dispatches to the appropriate one depending on the number of states in thestates
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 toadd_single_coupling()
. If either or both specifications correspond to multiple states, the corresponding lists will be passed as thestates1
andstates2
lists inadd_coupling_group()
.If this is the first time
add_coupling
has been called for thisSensor
, sets theprobe_tuple
attribute to thestates
specification , which is used as the default, for calculating observable values, in aSolution
after solving. For this reason, this function is preferred overadd_single_coupling()
andadd_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 argumets passed to the relevant function. See the documentation for
add_single_coupling()
andadd_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 overadd_single_coupling()
andadd_coupling_group()
since it appropriately handles necessary backend bookeeping.
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 thatStateSpec`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 coefficiens >>> 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 overadd_single_coupling()
andadd_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)¶
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 theadd_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 thesecc
values cannot be arrays. The corrollary for state energy (e.g. fordetuning
ortransition_frequency
) is handled viaadd_energy_shifts()
. If no dictionary is supplied for forcoherent_cc
, all coupling coefficients are set to 1.0, effectively meaning that the baserabi_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 theprobe_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 coeffients 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 coeffient to be passed to theadd_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. IfNone
, defaults to a dictionary containing every coupling in coupling in the group withNone
for all values (defaulting to 1.0 when passed toadd_single coupling
). Defaults toNone
.time_dependence (scalar function or dict of scalar functions, optional) – Time-dependendent 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 (seeadd_single_coupling()
), Can also bespecified as a dictionary mapping state pairs in the coupling to individual functionswhich will be applied to the associated coupling in the same manner. In the case of a dictionary specification, each unspecified coupling will default totime_dependence=None
.
- Raises:
RydiquleError – If
states1
andstates2
only have one state. Useadd_coupling()
instead.
Note
Note
The
add_coupling()
is typically preferred over this method, since it allows for shorthand specification of groups, and sets theSensor.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 incoupling_coeffiecients
.>>> 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 ¶
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 interchangably up to preference, and all of keywordadd_coupling()
are supported dictionary keys for dictionaries passed to this function.Note that since this function wraps
add_coupling()
, the first element ofcouplings
will be used to setprobe_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 dictionry see the arguments for
add_coupling()
. Equivalent to passing each dictiories keys and values toadd_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 ofadd_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)¶
Add a coupling between states or groups of states.
Wraps the
add_single_decoherence()
andadd_decoherence_group()
functions, and dispatches to the appropriate one depending on the formatting of thestates
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()
andadd_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)¶
Adds a group of dechorences 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 funtion so they share an axis whendecoherence_matrix()
is called.Scaling multiplicative factors for
gamma
must be applied per pair of states usingdecoherent_cc
, a dictionary of coefficients determining coupling strengths. If a pair is not indecoherent_cc
, it is assumed to have a coupling coefficient of zero, and will be omitted from the graph. Ifdecoherent_cc
isNone
, 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. Mutliplied by the corresponding values in the
decoherent_coupling
dictionary.coupling_coefficients (dict, optional) – Coefficiants 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 thegamma
argument multiplied by the corresponding value in this dictionary. IfNone
, 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
andstates2
only have one state. Useadd_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)¶
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. Whenstatespec
matches a single state,add_single_energy_shift()
method will be dispatched. In the case of a multi-state specification, theadd_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 itsself, not as data on the node itsself.
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 matchingone or more states in theSensor
. 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 ofadd_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)¶
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 theprefactors
dictionary. The dictionary is keyed with states that are elements ofstates
with entries corresponding to a factor multiplied by the baseshift
argument for each state. When energy shifts are array-like, thee_shift
attribute corresponding to each self-edge will be zipped withzip_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 baseshift
parameter for each each state. Keys are elements of thestates
list, entries are the corresponding factor by which to multiplyshift
for that state. IfNone
, all prefactors are set to 1. If notNone
, the prefactors for any nonspecified values will be set to zero. Default isNone
.zip_label (str or
None
, optional) – Label passed tozip_parameters()
when the shift is provided as an array-like when all states in the group are zipped together. Defaults toNone
.
- 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)¶
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 theadd_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)¶
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)¶
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 uselabel
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 decohernce terms to every combiniation of states in the group, not just from each state to itsself.
- 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[A_QState, A_QState], 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, kunit: Sequence[float] = (0, 0, 0), time_dependence: Callable[[float], complex] | None = None, label: str | None = None, e_field: float | List[float] | ndarray | None = None, beam_power: float | None = None, beam_waist: float | None = None, coherent_cc: float | None = None, q: Literal[-1, 0, 1] = 0, **extra_kwargs) None [source]¶
Overload of
add_single_coupling()
, which allows for alternate specifications and automatic calculations of some parameter.This overload fundamentally works identically the
super
method inSensor
, with several additions to the functionality that make some assumptions about the underlying system. Because of this, it still preferred to calladd_coupling()
on aCell
as well. Please refer to that methods documentation for further detailRabi frequency is a mandatory argument in
Sensor
but inCell
, there are 3 options for laser power specification:Explicit rabi-frequency definition identical to
Sensor
.Electric field strength, in V/m.
Specification of both beam power and beam waist, in W and m respectively.
Any one of these options can be used in place of the standard
rabi_frequency
argument ofadd_coupling()
. Note that in all cases, arabi_frequency
will be computed and passed toadd_single_coupling()
, and none of the above arguments will be added to the graph. Note that in any of these cases, if the computed dipole moment for the transition is zero, the coupling will be left off the graph.In all cases, the relative coupling strengths between sublevels of states (if present) is calculated and saved as the
coherent_cc
parameter on the graph. These coefficients are defined to be in units of \(1/2\langle J||d||J'\rangle\), as calculated bygetReducedMatrixElementJ()
. The corresponding Rabi frequency that Cell calculates therefore corresponds to \(\Omega_{red} = E \langle J||d||J'\rangle / 2\hbar\). The Rabi frequency for each transition added to the hamiltonian is then given by \(\text{coherent_cc}\cdot\Omega_{red}\cdot e^{i\text{phase}}\). In the case of NLJ states (ie no sublevels),coherent_cc=1
and the non-reduced Rabi frequency is used instead. See the physics documentation for further details.As in
Sensor
, ifdetuning
is specified, the coupling is assumed to act under the rotating-wave approximation (RWA), andtransition_frequency
can not be specified. However, unlike in aSensor
, ifdetuning
is not specified, in aCell
,transition_frequency
will be calculated automatically based on atomic properties rather than taken as an argument.- Parameters:
states (sequence) – Length-2 list-like object (list or tuple) of integers corresponding to the numbered states of the cell. Tuple order indicates which state to has higher energy: namely the second state is always assumed to have higher energy. This order must match the actual energy levels of the atom.
rabi_frequency (float, optional) – The rabi frequency, in Mrad/s, of the coupling field. If specified,
e_field
,beam_power
, andbeam_waist
cannot be specified.detuning (float, optional) – Field detuning, in Mrad/s, of a coupling in the RWA. If specified, RWA is assumed, otherwise RWA not assumed, and transition frequency will be calculated based on atomic properties.
transition_frequency (float, optional) – Kept such that method signature matches parent. Value must be
None
as the transition frequency is calculated from atomic properties.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.
kunit (sequence, optional) – A three-element iterable that defines the propagation direction of the field. It should be a normalized vector. This differs from
Sensor
’skvec
parameter, since appropriate scale factors are applied automatically inCell
. 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 that specifies a time-dependent field. The time dependence function is defined as a funtion that returns a unitless value as a function of time that is multiplied by the
rabi_frequency
parameter.label (str, optional) – The user-defined name of the coupling. This does not change any calculations, but can be used to track individual couplings, and will be reflected in the output of
axis_labels()
Default None results in using the states tuple as the label.e_field (float, optional) – Electric field strenth of the coupling in Volts/meter. If specified,
rabi_frequency
,beam_power
, andbeam_waist
cannot be specified.beam_power (float, optional) – Beam power in Watts. If specified,
beam_waist
must also be supplied, andrabi_frequency
ande_field
cannot be specified.beam_power
andbeam_waist
cannot be scanned simultaneously.beam_waist (float, optional) – 1/e^2 Beam waist (radius) in units of meters. Only necessary when specifying
beam_power
.q (int, optional) – Coupling polarization in spherical basis. Valid values are -1, 0, 1 for \(-\sigma\), linear, \(+\sigma\). Default is 0 for linear.
- Raises:
RydiquleError – If
states
is not a list-like of 2 integers.RydiquleError – If an invalid combination of
rabi_frequency
,e_field
,beam_power
, andbeam_waist
is provided.RydiquleError – If
tranistion_frequency
is passed as an argument (it is calculated from atomic properties).RydiquleError – If
beam_power
andbeam_waist
are both sequences.CouplingNotAllowedError – If the coupling is not dipole-allowed.
RydiquleError – If
kvec
is supplied instead ofkunit
.
Notes
Note
Note that while this function can be used directly just as in
Sensor
, it will often be called implicitly viaadd_coupling()
whichCell
inherits. While they are equivalent, the second of these options is often the more clear approach, and it automatically sets theprobe_tuple
attribute.Note
Specifying the beam power by beam parameters or electric field still computes the
rabi_frequency
and adds that quantity to theCell
to maintain consistency acrossrydiqule
’s other calculations. In other words,beam_power
,beam_waist
, ande_field
will never appear as quantities on the graph of aCell
.Examples
In the simplest case, physical properties are calculated automatically in a
Cell
All the familiar quantities are present, as well as many more. Note that while not strictly necessary, it is often convenient to alias states with shorthand variables to avoid very cumbersome state specification.>>> [g, e] = rq.D2_states("Rb87") >>> cell = rq.Cell("Rb87", [g, e]) >>> cell.add_single_coupling((g,e), detuning=1.0, rabi_frequency=2.0, label="probe") >>> print(cell) <class 'rydiqule.cell.Cell'> object with 2 states and 1 coherent couplings. States: [(n=5, l=0, j=0.5), (n=5, l=1, j=1.5)] Coherent Couplings: ((5, 0, 0.5),(5, 1, 1.5)): {rabi_frequency: 2.0, detuning: 1.0, phase: 0, kvec: (0, 0, 0), label: probe, coherent_cc: 1, dipole_moment: 2.44, q: 0} Decoherent Couplings: ((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11316} Energy Shifts: None
Since
add_couplings()
,add_coupling()
, andadd_coupling_group()
only iterate over calls of this function, they do not need to be overloaded.>>> [g, e] = rq.D2_states("Rb87") >>> cell = rq.Cell("Rb87", [g, e]) >>> probe = dict(states=(g,e), detuning=1.0, rabi_frequency=2.0, label="probe") >>> cell.add_couplings(probe) >>> print(cell) <class 'rydiqule.cell.Cell'> object with 2 states and 1 coherent couplings. States: [(n=5, l=0, j=0.5), (n=5, l=1, j=1.5)] Coherent Couplings: ((5, 0, 0.5),(5, 1, 1.5)): {rabi_frequency: 2.0, detuning: 1.0, phase: 0, kvec: (0, 0, 0), label: probe, coherent_cc: 1, dipole_moment: 2.44, q: 0} Decoherent Couplings: ((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11} Energy Shifts: None
e_field
can be specified instead ofrabi_frequency
, but arabi_frequency
will still be added to the system based on thee_field
and computed dipole moment rather thane_field
directly.>>> [g, e] = rq.D2_states("Rb87") >>> cell = rq.Cell("Rb87", [g, e]) >>> cell.add_coupling((g,e), detuning=1.0, e_field=6.0, label="probe") >>> print(cell) <class 'rydiqule.cell.Cell'> object with 2 states and 1 coherent couplings. States: [(n=5, l=0, j=0.5), (n=5, l=1, j=1.5)] Coherent Couplings: ((5, 0, 0.5),(5, 1, 1.5)): {rabi_frequency: 1.177, detuning: 1.0, phase: 0, kvec: (0, 0, 0), label: probe, coherent_cc: 1, dipole_moment: 2.44, q: 0} Decoherent Couplings: ((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11} Energy Shifts: None
As can
beam_power
andbeam_waist
, with similar behavior regarding how information is stored.>>> cell = rq.Cell("Rb85", rq.D2_states("Rb85"), cell_length = .0001) >>> cell.add_coupling((g,e), detuning=1.0, beam_power=1.0, beam_waist=1.0, label="probe") >>> print(cell) <class 'rydiqule.cell.Cell'> object with 2 states and 1 coherent couplings. States: [(n=5, l=0, j=0.5), (n=5, l=1, j=1.5)] Coherent Couplings: ((5, 0, 0.5),(5, 1, 1.5)): {rabi_frequency: 4.3, detuning: 1.0, phase: 0, kvec: (0, 0, 0), label: probe, coherent_cc: 1, dipole_moment: 2.44, q: 0} Decoherent Couplings: ((5, 1, 1.5),(5, 0, 0.5)): {gamma_transition: 38.11} Energy Shifts: None
- 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)¶
Add decoherent coupling to the graph between two states.
If
gamma
is list-like, the array generated bydecoherence_matrix()
will contain decoherence matrices for every combination of decoherence values provided. This functionality mirrors hamiltonian generation when parameters ofadd_coupling()
are list-like. Note that ifgamma
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 byadd_decoherence_group()
, but made transparent for scripting purposes. Defaults to 1.0label (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 decoherece 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, provida a differnt 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 decoherece 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)¶
Add an energy shift to a state.
First perfoms validation that the provided
state
is actually a node in the graph, then adds the shift specified byshift
to a self-loop edge keyed with"e_shift"
. This value will be added to the corresponding diagonal term when the hamiltonian is generated.
- add_transit_broadening(gamma_transit: float | List[float] | ndarray, repop: Dict[A_QState, float] | List[A_QState] | None = None, label: str = 'transit')[source]¶
Overload of the
add_transit_broadening()
method ofSensor
which automatically populates therepop
dictionary with a equal decay to all sublevels of the ground(n, l, j)
state.- Parameters:
gamma_transit (ScannableParameter) – Transit brodening of the system. Passed transparently to super function.
repop (dict, optional) – Dictionary of states for transit to repopulate in to. The keys represent tshe 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. If
None
, raises population decay due to transit broadening will be evenly divided amongst all states matching the(n, l, j)
quantum numbers of the lowest-energy state in the system. Defaults toNone
.label (str, optional) – Label to be passed to
add_decoherence()
. Defaults to “transit”
Examples
>>> atom = "Rb85" >>> g = rq.ground_state(atom, splitting="fs") >>> e = rq.D1_excited(atom, splitting="fs") >>> Rb_Cell = rq.Cell(atom, [g,e]) >>> Rb_Cell.add_transit_broadening(0.1) >>> for e in Rb_Cell.couplings.edges.data("gamma_transit"): ... print(e) ((n=5, l=0, j=0.5, m_j=-0.5), (n=5, l=0, j=0.5, m_j=-0.5), 0.05) ((n=5, l=0, j=0.5, m_j=-0.5), (n=5, l=0, j=0.5, m_j=0.5), 0.05) ((n=5, l=0, j=0.5, m_j=0.5), (n=5, l=0, j=0.5, m_j=-0.5), 0.05) ((n=5, l=0, j=0.5, m_j=0.5), (n=5, l=0, j=0.5, m_j=0.5), 0.05) ((n=5, l=1, j=0.5, m_j=-0.5), (n=5, l=0, j=0.5, m_j=-0.5), 0.05) ((n=5, l=1, j=0.5, m_j=-0.5), (n=5, l=0, j=0.5, m_j=0.5), 0.05) ((n=5, l=1, j=0.5, m_j=0.5), (n=5, l=0, j=0.5, m_j=-0.5), 0.05) ((n=5, l=1, j=0.5, m_j=0.5), (n=5, l=0, j=0.5, m_j=0.5), 0.05)
- axis_labels() List[str] ¶
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 parametes 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 howget_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:
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:
- couplings_with(*keys: str, method: Literal['all', 'any', 'not any'] = 'all') Dict[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], Dict] ¶
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 coosing “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:
Examples
Can be used, for example, to return couplings in the roating 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 ¶
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:
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 ¶
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:
Examples
>>> s = rq.Sensor(3) >>> print(s.dm_basis()) ['01_real' '02_real' '01_imag' '11_real' '12_real' '02_imag' '12_imag' '22_real']
- property eta¶
Get the eta value for the system.
The value is computed with the following formula Eq. 7 of Meyer et. al. PRA 104, 043103 (2021)
\[\eta = \sqrt{\frac{\omega \mu^2}{2 c \epsilon_0 \hbar A}}\]Where \(\omega\) is the probing frequency, \(\mu\) is the dipole moment, \(A\) is the beam area, \(c\) is the speed of light, \(\epsilon_0\) is the dielectric constant, and \(\hbar\) is the reduced Plank constant.
This value is only computed if there is not a
_eta
attribute in the system. If this attribute does exist, this function acts as an accessor for that attribute.- Returns:
The value eta for the system.
- Return type:
- get_couplings() Dict[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], Dict] ¶
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:
- get_doppler_shifts() ndarray ¶
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 of1e-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:
- get_hamiltonian() ndarray ¶
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 ofaxis_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 coulumns 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 ¶
Apply addition and subtraction logic corresponding to the direction of the couplings.
For a given state
n
, the path from ground will be traced ton
. 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 then
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 digonal of the hamiltonian of the system of shape
(*l,n)
, wherel
is the shape of the hamiltonian stack for the sensor.- Return type:
- get_parameter_mesh() List[ndarray] ¶
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 theindexing
argument always"ij"
for matrix indexing.- Returns:
list of mesh grids for every variable parameter
- Return type:
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 ¶
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:
- get_time_dependence() List[Callable[[float], complex]] ¶
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:
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 ¶
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:
- get_time_hamiltonian_components() Tuple[List[ndarray], List[ndarray]] ¶
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 ouput will be two lists of matricies 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), andn
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 atime_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. For0 <= i <= M
, the ith value along the first axis is the portion of the matrix which will be multiplied by the output of the ithtime_dependence
function.list of numpy.ndarray – The list of M
(*l,n,n)
matrices representing the imaginary-valued time-dependent portion of the hamiltonian. For0 <= i <= M
, the ith value along the first axis is the portion of the matrix which will be multiplied by the output of the ithtime_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 ¶
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:
- get_value_dictionary(key: str) dict ¶
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:
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]]] ¶
- int_states_map(invert: bool = False) Dict[int | str | Tuple[float, ...], int] | Dict[int, int | str | Tuple[float, ...]] ¶
Get a dictionary mapping between state labels and their corresponding integer ordering. Can be returned with
key:value
pairs defined either bylabel:int
orint:label
, controlled via optionalinvert
argument.
- property kappa¶
Property to calculate the kappa value of the system.
The value is computed with the following formula Eq. 5 of Meyer et. al. PRA 104, 043103 (2021)
\[\kappa = \frac{\omega n \mu^2}{2c \epsilon_0 \hbar}\]Where \(\omega\) is the probing frequency, \(\mu\) is the dipole moment, \(n\) is atomic cloud density, \(c\) is the speed of light, \(\epsilon_0\) is the dielectric constant, and \(\hbar\) is the reduced Plank constant.
This value is only computed if there is not a
_kappa
attribute in the system. If this attribute does exist, this function acts as an accessor for that attribute.- Returns:
The value kappa for the system.
- Return type:
- level_ordering() List[A_QState] [source]¶
Return a list of the states in the
Cell
in ascending energy order.All energies are calculated with respect to the ground state energy, which is defined as 0. Ground state is determined by the rydiqule’s calculation of ground energy, which uses
arc
to get the energy of the \(nP^{\frac{1}{2}}\) state, wheren
is 1 for Hydrogen, 2 for Lithium, etc.- Returns:
The Cell states in order of decending energy relative to the ground state \(nS^{\frac{1}{2}}\).
- Return type:
Examples
For the following example, states are in the list passed to the constructor in ascending energy order, so the ordering the basis is identical to the
level_ordering
. ComputedCell
attributes the Hamiltonian will, for clarity, always appear in the ording of levels in the list passed to the constructor.>>> from rydiqule import A_QState >>> atom = "Rb85" >>> [g, e] = rq.D2_states(atom) #uses the D2 line of Rb85 >>> state1 = A_QState(50, 2, 2.5) >>> state2 = A_QState(51, 2, 2.5) >>> my_cell = rq.Cell(atom, [g, e, state1, state2]) >>> print(my_cell.states) [(n=5, l=0, j=0.5), (n=5, l=1, j=1.5), (n=50, l=2, j=2.5), (n=51, l=2, j=2.5)] >>> # levels in order >>> for i, state in enumerate(my_cell.level_ordering()): ... print(f"{i}: {state}, E={my_cell.couplings.nodes[state]['energy']*2*np.pi*1e-6} Mrad/s") 0: (5, 0, 0.5), E=0.0 Mrad/s 1: (5, 1, 1.5), E=15168.8 Mrad/s 2: (50, 2, 2.5), E=39819.3 Mrad/s 3: (51, 2, 2.5), E=39821.5 Mrad/s
If we scramble the states in the constructor, the output of this function remains the same even though the order of basis states changes to match the list ordering in the constuctor.
>>> from rydiqule import A_QState >>> atom = "Rb85" >>> [g, e] = rq.D2_states(atom) #uses the D2 line of Rb85 >>> state1 = A_QState(50, 2, 2.5) >>> state2 = A_QState(51, 2, 2.5) >>> my_cell = rq.Cell(atom, [state2, e, g, state1]) >>> print(my_cell.states) [(n=51, l=2, j=2.5), (n=5, l=1, j=1.5), (n=5, l=0, j=0.5), (n=50, l=2, j=2.5)] >>> for i, state in enumerate(my_cell.level_ordering()): ... print(f"{i}: {state}, E={my_cell.couplings.nodes[state]['energy']*2*np.pi*1e-6} Mrad/s") 0: (5, 0, 0.5), E=0.0 Mrad/s 1: (5, 1, 1.5), E=15168.8 Mrad/s 2: (50, 2, 2.5), E=39819.3 Mrad/s 3: (51, 2, 2.5), E=39821.5 Mrad/s
- property probe_freq¶
Get the probe transition frequency, in rad/s.
Note that for
Cell
, probing transition frequency is calculated using only the(n,l,j)
states of the upper and lower manifolds of theprobe_tuple
attribute. For more precise calculations accounting for atomic splitting etc,probe_freq
must be set manually;rydiqule
does not support doing these calculations automatically.- Returns:
Probe transitiion frequency, in rad/s, between probing nlj states.
- Return type:
- 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 withadd_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)[source]¶
Sensor
specific method. Do not use withCell
.This function does not do anything as Cell automatically handles this functionality internally.
- Warns:
RydiquleWarning (Warns if function is used.)
- set_gamma_matrix(gamma_matrix: ndarray)¶
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 statei
to statej
.- Raises:
RydiquleError – If
gamma_matrix
is not a numpy array.ValueError – If
gamma_matrix
is not a square matrix of the appropriate sizeValueError – If the shape of
gamma_matrix
is not compatible withself.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 ¶
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 of1e-08
.- Returns:
Number of dimensions, between 0 and 3, where 0 means no doppler averaging kvectors have been specified or are too small to be calculates.
- Return type:
Examples
No spatial dimesions specified
>>> s = rq.Sensor(2) >>> s.add_coupling((0,1), detuning = 1, rabi_freqency=1) >>> print(s.spatial_dim()) 0
One spatial dimension specified
>>> s = rq.Sensor(2) >>> s.add_coupling((0,1), detuning = 1, rabi_freqency=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_freqency=1, kvec=(3,0,3)) >>> print(s.spatial_dim()) 2
>>> s = rq.Sensor(3) >>> s.add_coupling((0,1), detuning = 1, rabi_freqency=1, kvec=(3,0,3)) >>> s.add_coupling((1,2), detuning = 2, rabi_freqency=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:
- states_with_spec(statespec: A_QState) List[A_QState] [source]¶
Return a list of all states in the sensor matching the
state_spec
pattern.Matching is determined by same rules as
states_with_spec()
, with no additional logic to account for the different typing of the states. This means that there is expansion of any quantum numbers specified by the “all” keyword, and only states that are already nodes of theCell
graph will be included.- Parameters:
statespec (A_QState) – State specification againt which to perform matching,
- Returns:
List of all states in
Cell
instance which match the provided specification.- Return type:
List[A_QState]
Examples
>>> atom = "Rb85" >>> g = rq.ground_state(atom, splitting="fs") >>> e = rq.D1_excited(atom, splitting="fs") >>> Rb_Cell = rq.Cell(atom, [g,e]) >>> print(Rb_Cell.states_with_spec(A_QState(5, 0, 0.5))) [] >>> print(Rb_Cell.states_with_spec(A_QState(5, 0, 0.5, m_j="all"))) [(n=5, l=0, j=0.5, m_j=-0.5), (n=5, l=0, j=0.5, m_j=0.5)] >>> print(Rb_Cell.states_with_spec(A_QState(5, 0, 0.5, m_j=[-0.5, 0.5]))) [(n=5, l=0, j=0.5, m_j=-0.5), (n=5, l=0, j=0.5, m_j=0.5)]
- unzip_parameters(zip_label: str, verbose: bool | None = True)¶
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 withverbose=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. IfTrue
prints a message to std out ifzip_label
is not an element of the internalself._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 behaviour can be modified with
verbose=False
, potentially useful for scripting cases where the desired behavior is to silently countinue over non-existant 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.
- variable_parameter_sort(par: tuple) tuple ¶
Assistance function which determines the sorting order of elements parameters in sensor.
Called in
variable_parameters()
to ensure a consistent sort order. Provided as thekey
parameter in python’ssorted()
function before parameters are returned.Sorts first by
zip_label
, then bystates
, then byparameter
. 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.
- variable_parameters(apply_mesh: bool = False) List[Tuple[Tuple[int | str | Tuple[float, ...], int | str | Tuple[float, ...]], str, ndarray, str | None]] ¶
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)¶
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
andhamiltonian
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 quanties 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. IfNone
(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 orunzip_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)¶
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 ofzip_labels
with a “_” character, by default None.- Raises:
RydiquleError – If any of
zip_labels
do not exist in theSensor
.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']