Gate maps
Representing gates with matrices is a great way to ensure plugin compatibility;
unlike using for example names for gates, the matrix unambiguously represents
the mathematical operation that is to be performed. However, there are cases
where you may want to distinguish whether a gate is for instance one of the
Pauli gates or something else. You could do this with dqcs_mat_approx_eq()
,
but if you have a lot of gates your code will explode, be error-prone
(especially if you want to do the reverse operation as well), and may not be
very efficient. More generally, a plugin may want to use its own higher-level
internal gate representation, and convert between that and DQCsim's
matrix-based representation.
Gate maps intend to solve this problem. You can define any data structure to represent your gates as long as it can map to/from the following:
- any kind of key (a
void*
) defining the type of gate. - a number of qubit arguments.
- optionally, an
ArbData
representing parameters if your definition of a gate type is parameterized.
You can then use a gate map to convert between that representation and DQCsim's representation, typically in both directions. Going from DQCsim's matrix representation to your plugin's representation is called detection, while the opposite direction is called construction. Once you have a gate map, you can use the following functions to do this.
dqcs_gm_detect()
Uses a gate map object to convert an incoming DQCsim gate to the plugin's
representation.
Uses a gate map object to convert an incoming DQCsim gate to the plugin's representation.
dqcs_bool_return_t dqcs_gm_detect(
dqcs_handle_t gm,
dqcs_handle_t gate,
const void **key_data,
dqcs_handle_t *qubits,
dqcs_handle_t *param_data
)
gm
must be a handle to a gate map object (dqcs_mm_new()
).
gate
must be a handle to a gate. The handle is borrowed; it is not
mutated or deleted.
key_data
serves as an optional return value; if non-NULL and a match is
found, the key_data
specified when the respective detector was added is
returned here as a const void *
. If no match is found, *key_data
is
not assigned.
qubits
serves as an optional return value; if non-NULL and a match
is found, it is set to a handle to a new QubitSet
object representing the
gate's qubits. Ownership of this handle is passed to the user, so it
is up to the user to eventually delete it. If no match is found,
*qubits
is set to 0.
param_data
serves as an optional return value; if non-NULL and a match
is found, it is set to a handle to a new ArbData
object representing the
gate's parameters. Ownership of this handle is passed to the user, so it
is up to the user to eventually delete it. If no match is found,
*param_data
is set to 0.
This function returns DQCS_TRUE
if a match was found, DQCS_FALSE
if no
match was found, or DQCS_BOOL_FAILURE
if an error occurs.
dqcs_gm_construct_one()
Uses a gate map object to construct a one-qubit DQCsim gate from the
plugin's representation.
Uses a gate map object to construct a one-qubit DQCsim gate from the plugin's representation.
dqcs_handle_t dqcs_gm_construct_one(
dqcs_handle_t gm,
const void *key_data,
dqcs_qubit_t qa,
dqcs_handle_t param_data
)
This function is simply a shorthand for dqcs_gm_construct()
with
one qubit in the qubits
set, to make constructing one-qubit gates more
ergonomic. Refer to its documentation for more information.
dqcs_gm_construct_two()
Uses a gate map object to construct a two-qubit DQCsim gate from the
plugin's representation.
Uses a gate map object to construct a two-qubit DQCsim gate from the plugin's representation.
dqcs_handle_t dqcs_gm_construct_two(
dqcs_handle_t gm,
const void *key_data,
dqcs_qubit_t qa,
dqcs_qubit_t qb,
dqcs_handle_t param_data
)
This function is simply a shorthand for dqcs_gm_construct()
with
two qubits in the qubits
set, to make constructing two-qubit gates more
ergonomic. Refer to its documentation for more information.
dqcs_gm_construct_three()
Uses a gate map object to construct a three-qubit DQCsim gate from the
plugin's representation.
Uses a gate map object to construct a three-qubit DQCsim gate from the plugin's representation.
dqcs_handle_t dqcs_gm_construct_three(
dqcs_handle_t gm,
const void *key_data,
dqcs_qubit_t qa,
dqcs_qubit_t qb,
dqcs_qubit_t qc,
dqcs_handle_t param_data
)
This function is simply a shorthand for dqcs_gm_construct()
with
three qubits in the qubits
set, to make constructing three-qubit gates
more ergonomic. Refer to its documentation for more information.
dqcs_gm_construct()
Uses a gate map object to construct a multi-qubit DQCsim gate from the
plugin's representation.
Uses a gate map object to construct a multi-qubit DQCsim gate from the plugin's representation.
dqcs_handle_t dqcs_gm_construct(
dqcs_handle_t gm,
const void *key_data,
dqcs_handle_t qubits,
dqcs_handle_t param_data
)
gm
must be a handle to a gate map object (dqcs_mm_new()
).
gate
must be a handle to a gate. The handle is borrowed; it is not
mutated or deleted.
key_data
specifies the gate mapping key for the constructor to use. Note
that the pointer must match exactly to what was specified when the
mapping(s) was/were added.
qubits
specifies the qubits arguments for the constructed gate. It is
up to the constructor function to determine how to interpret these. The
parameter is optional; passing 0 is equivalent to passing an empty qubit
set. The handle is deleted if the function succeeds.
param_data
specifies the ArbData
object used to parameterize the gate.
It is optional; if 0, an empty ArbData
is automatically constructed by
DQCsim. The handle is deleted if the function succeeds.
This function returns the handle to the gate, or 0 to indicate failure. The qubit set and parameterization data (if specified) are consumed/deleted by this function if and only if it succeeds.
Converters
Conceptually, a gate map consists of a number of converter objects, each
typically consisting of a detector function and a constructor function.
In the most generic case, the detector takes a DQCsim gate as its input, and
converts it to a qubit set and an ArbData
if it recognizes the gate, while
the constructor performs the inverse operation. Detection is usually fuzzy to
account for floating-point inaccuracies, while construction is as exact as
possible.
These converter objects are stored in the gate map as the values of an ordered
map, for which the key is the user-defined void*
key defining the type of
gate. Thus, each converter represents a single gate type. When a gate is to be
detected, DQCsim will call each converter's detector function in insertion
order until one of the detectors returns a match. When a gate is to be
constructed, it simply maps the gate type key to the appropriate converter and
calls only its constructor.
The most generic converter described above can be added to a map with the following function, but it is also the most complicated to implement.
dqcs_gm_add_custom()
Adds a fully customizable gate mapping to the given gate map.
Adds a fully customizable gate mapping to the given gate map.
dqcs_return_t dqcs_gm_add_custom(
dqcs_handle_t gm,
void (*key_free)(void *key_data),
void *key_data,
dqcs_bool_return_t (*detector)(
const void *user_data,
dqcs_handle_t gate,
dqcs_handle_t *qubits,
dqcs_handle_t *param_data
),
void (*detector_user_free)(void *user_data),
void *detector_user_data,
dqcs_handle_t (*constructor)(
const void *user_data,
dqcs_handle_t qubits,
dqcs_handle_t param_data
),
void (*constructor_user_free)(void *user_data),
void *constructor_user_data
)
Note that this is the only type of mapping that can handle custom/named gates.
detector
is the detector function pointer. It is optional; if null, this
mapping only supports construction.
detector_user_free
is an optional callback function used to free
detector_user_data
when the gate map is destroyed, when this function
fails, or when detector
was null.
detector_user_data
is a user-specified value that is passed to the
detector
callback function. It is not used by DQCsim.
constructor
is the constructor function pointer. It is optional; if
null, this mapping only supports detection.
constructor_user_free
is an optional callback function used to free
constructor_user_data
when the gate map is destroyed, when this function
fails, or when constructor
was null.
constructor_user_data
is a user-specified value that is passed to the
constructor
callback function. It is not used by DQCsim.
If both constructor
and detector
are null for some reason, the
function is no-op (besides possibly calling the *_free()
callbacks.
The detector callback receives the complete gate passed to the gate map
for it to match as it pleases. If the gate matches, the detector function
must return DQCS_TRUE
. It may assign qubits
to a qbset
object
representing the qubit arguments (substituted with an empty set if it
doesn't), and may assign param_data
to an arb
handle with the
parameterization data (if it doesn't, the data from the gate is used; if
this was modified by the callback, the modified data is used). If the gate
doesn't match, it must return DQCS_FALSE
. If an error occurs, it must
call dqcs_error_set()
with the error message and return
DQCS_BOOL_FAILURE
.
The constructor callback performs the reverse operation. It receives an
ArbData
handle containing the parameterization data and a qubit set, and
must construct a gate based on this information. If construction succeeds,
the constructor function must return the gate handle. If an error occurs,
it must call dqcs_error_set()
with the error message and return 0.
It is up to the user how to do the matching and constructing, but the
converter functions must always return the same value for the same input.
In other words, they must be pure functions. Otherwise, the caching
behavior of the GateMap
will make the results inconsistent.
There is also a specialized version that detects unitary gate matrices instead of complete gates. This version deals with distinguishing between unitary, measurement, and custom gates for you. It also converts between DQCsim's seperate target/control qubit set and the single gate-type-sensitive qubit set in the plugin representation for you.
dqcs_gm_add_custom_unitary()
Adds a custom unitary gate mapping to the given gate map.
Adds a custom unitary gate mapping to the given gate map.
dqcs_return_t dqcs_gm_add_custom_unitary(
dqcs_handle_t gm,
void (*key_free)(void *key_data),
void *key_data,
dqcs_bool_return_t (*detector)(
const void *user_data,
dqcs_handle_t matrix,
size_t num_controls,
dqcs_handle_t *param_data
),
void (*detector_user_free)(void *user_data),
void *detector_user_data,
dqcs_handle_t (*constructor)(
const void *user_data,
dqcs_handle_t *param_data,
intptr_t *num_controls
),
void (*constructor_user_free)(void *user_data),
void *constructor_user_data
)
gm
must be a handle to a gate map object (dqcs_gm_new()
).
key_free
is an optional callback function used to free key_data
when
the gate map is destroyed, or when this function fails.
key_data
is the user-specified value used to identify this mapping.
detector
is the detector function pointer. It is optional; if null, this
mapping only supports construction.
detector_user_free
is an optional callback function used to free
detector_user_data
when the gate map is destroyed, when this function
fails, or when detector
was null.
detector_user_data
is a user-specified value that is passed to the
detector
callback function. It is not used by DQCsim.
constructor
is the constructor function pointer. It is optional; if
null, this mapping only supports detection.
constructor_user_free
is an optional callback function used to free
constructor_user_data
when the gate map is destroyed, when this function
fails, or when constructor
was null.
constructor_user_data
is a user-specified value that is passed to the
constructor
callback function. It is not used by DQCsim.
If both constructor
and detector
are null for some reason, the
function is no-op (besides possibly calling the *_free()
callbacks.
The detector callback receives a matrix and control qubit information for
the user to match. The matrix is passed through the matrix
handle.
num_controls
is passed the number of explicit control qubits that exist
besides the matrix (that is, if nonzero, the matrix is actually only the
non-controlled submatrix of the controlled gate). param_data
is given an
ArbData
handle initialized with the ArbData
attached to the gate. If
the gate matches, the detector function must return DQCS_TRUE
. In this
case, it can mutate the param_data
to add the detected gate parameters.
If it doesn't match, it must return DQCS_FALSE
. If an error occurs, it
must call dqcs_error_set()
with the error message and return
DQCS_BOOL_FAILURE
.
The constructor callback performs the reverse operation. It receives an
ArbData
handle containing the parameterization data, and must construct
the matrix, return the bound on the number of control qubits, and must
return the ArbData
associated with the gate by mutating the param_data
handle. num_controls
will point to a variable initialized to -1
representing a constraint on the number of control qubits. This works as
follows: if negative, any number of qubits is allowed; if zero or
positive, only that number is allowed. If construction succeeds, the
constructor function must return a handle to the constructed matrix. If
it fails, it must call dqcs_error_set()
with an error message and return
0.
It is up to the user how to do the matching and constructing, but the
converter functions must always return the same value for the same input.
In other words, they must be pure functions. Otherwise, the caching
behavior of the GateMap
will make the results inconsistent.
More likely, though, you just want to detect the usual gates, like X, H, swap,
and so on. To help you do this, DQCsim includes built-in converters for every
dqcs_predefined_gate_t
, which you can add to the map with the following,
much simpler function.
dqcs_gm_add_predef_unitary()
Adds a unitary gate mapping for the given DQCsim-defined gate to the
given gate map.
Adds a unitary gate mapping for the given DQCsim-defined gate to the given gate map.
dqcs_return_t dqcs_gm_add_predef_unitary(
dqcs_handle_t gm,
void (*key_free)(void *user_data),
void *key_data,
dqcs_predefined_gate_t gate,
intptr_t num_controls,
double epsilon,
bool ignore_gphase
)
gm
must be a handle to a gate map object (dqcs_gm_new()
).
key_free
is an optional callback function used to free key_data
when
the gate map is destroyed, or when this function fails.
key_data
is the user-specified value used to identify this mapping.
gate
defines which predefined gate to use. Some of the predefined gates
are parameterized.
num_controls
specifies the number of control qubits associated with this
gate type. If negative, the gate can have any number of control qubits.
If zero or positive, the number of control qubits must be as specified.
epsilon
specifies the maximum element-wise root-mean-square error
between the incoming matrix and the to be detected matrix that results in a
positive match.
ignore_phase
specifies whether the aforementioned check should ignore
global phase or not when there are no explicit control qubits.
For most gate types, the parameterization ArbData
object returned by
detection and consumed by construction is mapped one-to-one to the user
data of the gate in the DQCsim-protocol. Some of the detectors however
detect parameterized gate matrices. These detectors prefix a fixed number
of binary string arguments to the ArbData
upon detection, and pop these
when constructing. The specs for this can be found in the docs for
dqcs_predefined_gate_t
.
You can also easily detect gates with a special, fixed matrix.
dqcs_gm_add_fixed_unitary()
Adds a unitary gate mapping for the given gate matrix to the given gate
map.
Adds a unitary gate mapping for the given gate matrix to the given gate map.
dqcs_return_t dqcs_gm_add_fixed_unitary(
dqcs_handle_t gm,
void (*key_free)(void *key_data),
void *key_data,
dqcs_handle_t matrix,
intptr_t num_controls,
double epsilon,
bool ignore_gphase
)
gm
must be a handle to a gate map object (dqcs_gm_new()
).
key_free
is an optional callback function used to free key_data
when
the gate map is destroyed, or when this function fails.
key_data
is the user-specified value used to identify this mapping.
matrix
must be passed a handle to the matrix to detect. It is consumed
by this function.
num_controls
specifies the number of control qubits associated with this
gate type. If negative, the gate can have any number of control qubits.
If zero or positive, the number of control qubits must be as specified.
epsilon
specifies the maximum element-wise root-mean-square error
between the incoming matrix and the to be detected matrix that results in a
positive match.
ignore_phase
specifies whether the aforementioned check should ignore
global phase or not when there are no explicit control qubits.
The parameterization ArbData
object returned by detection and consumed
by construction is mapped one-to-one to the user data of the gate in the
DQCsim-protocol.
Finally, you can detect measurement and prep gates with the following built-in detectors.
dqcs_gm_add_measure()
Adds a measurement gate mapping to the given gate map.
Adds a measurement gate mapping to the given gate map.
dqcs_return_t dqcs_gm_add_measure(
dqcs_handle_t gm,
void (*key_free)(void *user_data),
void *key_data,
intptr_t num_measures,
dqcs_handle_t basis,
double epsilon
)
gm
must be a handle to a gate map object (dqcs_gm_new()
).
key_free
is an optional callback function used to free key_data
when
the gate map is destroyed, or when this function fails.
key_data
is the user-specified value used to identify this mapping.
num_measures
specifies the number of measured qubits for this gate type.
If negative, the gate can have any number of measured qubits. If zero or
positive, the number of measured qubits must be as specified.
basis
optionally specifies a handle to a 2x2 matrix specifying the
measurement basis to be detected. If not specified, the Z basis is used.
The matrix is deleted by the call iff the function succeeds.
epsilon
specifies the maximum RMS deviation between the specified basis
(if any) and the incoming basis.
The parameterization ArbData
object returned by detection and consumed
by construction is mapped one-to-one to the user data of the gate in the
DQCsim-protocol.
dqcs_gm_add_prep()
Adds a prep gate mapping to the given gate map.
Adds a prep gate mapping to the given gate map.
dqcs_return_t dqcs_gm_add_prep(
dqcs_handle_t gm,
void (*key_free)(void *user_data),
void *key_data,
intptr_t num_targets,
dqcs_handle_t basis,
double epsilon
)
gm
must be a handle to a gate map object (dqcs_gm_new()
).
key_free
is an optional callback function used to free key_data
when
the gate map is destroyed, or when this function fails.
key_data
is the user-specified value used to identify this mapping.
num_targets
specifies the number of target qubits for this gate type.
If negative, the gate can have any number of targets. If zero or
positive, the number of target qubits must be as specified.
basis
optionally specifies a handle to a 2x2 matrix specifying the
prep basis. If not specified, the Z basis is used. The matrix is deleted
by the call iff the function succeeds.
epsilon
specifies the maximum RMS deviation between the specified basis
(if any) and the incoming basis.
The parameterization ArbData
object returned by detection and consumed
by construction is mapped one-to-one to the user data of the gate in the
DQCsim-protocol.
Caching
The detection logic in a gate map includes a cache to improve performance when the same gate is received a number of times. This is quite typical, as algorithms typically use only a small subset of gates frequently. There are four different caching strategies:
-
The cache key is an exact copy of the incoming gate, so the cache only hits when the exact gate is received twice. In this case, the cache maps directly to the result of the previous detection, so no detector function needs to be run. This is the safest option.
-
The cache ignores differing
ArbData
attachments to the incoming gates. This is valid only if whether a detector function matches or not is not sensitive to thisArbData
. However, the cache only maps to the matching detection function, and calls it again for every cache hit. Thus, theArbData
returned by the detector can still depend on the gate'sArbData
. -
The cache key is a copy of the incoming gate, but without the qubit references. That is, for example an X gate applied to q1 is considered equal to an X gate applied to q2. This is valid only if whether a detector function matches or not is not sensitive to the qubit references. It can, however, be sensitive to the amount of qubits, and as with the
ArbData
insensitivity described above, the detector is still called for each cache hit and can thus still return a different qubit set. -
A combination of the latter two, i.e., the cache is not sensitive to either the gate's
ArbData
or to the qubit references.
Preprocessing
To be as compatible with other plugins as possible, you may want to preprocess
the incoming gates with either dqcs_gate_reduce_control()
or
dqcs_gate_expand_control()
. We already went over these in the previous
section, but their description is repeated here for convenience.
When you apply dqcs_gate_reduce_control()
to each incoming gate before
passing it to dqcs_gm_detect()
, you ensure that if the upstream plugin is
sending for instance a CNOT using a complete two-qubit gate matrix and two
target qubits, it will still be detected as a controlled X gate with one
control qubit, instead of some different gate.
You can also choose to do the opposite, converting from for instance DQCsim's
controlled X representation to a full CNOT matrix using
dqcs_gate_expand_control
. However, in this case you'll have to detect
controlled matrices with dqcs_gm_add_fixed_unitary()
or a fully custom
implementation, as DQCsim only provided predefined matrices for the
non-controlled (sub)matrices.
Constructing a gate map
Having read all of the above, you should be ready to construct a new gate map. The construction function takes the caching strategy as its parameters, as well as two optional callback functions used to compare and hash your plugin's key type, needed for the internal hashmap mapping from key to converter object.
dqcs_gm_new()
Constructs a new gate map.
Constructs a new gate map.
dqcs_handle_t dqcs_gm_new(
bool strip_qubit_refs,
bool strip_data,
bool (*key_cmp)(
const void,
const void
),
uint64_t (*key_hash)(const void)
)
Returns a handle to a gate map with no mappings attached to it yet. Use
dqcs_gm_add_*()
to do that. The mappings are queried in the order in
which they are added, so be sure to add more specific gates first. Once
added, use dqcs_gm_detect()
to detect incoming DQCsim gates, and
dqcs_gm_construct*()
to (re)construct gates for transmission.
Gate maps objects retain a cache to speed up detection of similar DQCsim
gates: if a gate is received for the second time, the cache will hit,
avoiding recomputation of the detector functions. What constitutes
"similar gates" is defined by the two booleans passed to this function. If
strip_qubit_refs
is set, all qubit references associated with the gate
will be invalidated (i.e., set to 0), such that for instance an X gate
applied to qubit 1 will be considered equal to an X gate applied to qubit
2. If strip_data
is set, the ArbData
associated with the incoming
gate is removed.
Gates are identified through user-defined void*
keys. To do the above,
however, DQCsim needs to know the following things:
- how to delete an owned copy of a key if your semantics are that DQCsim owns it,
- how to compare two keys (equality);
- how to hash a key.
The deletion function is passed when the key is passed. If the keys are objects of different classes, this allows different constructors to be passed here. There can only be a single comparison and hash function for each gate map, though. They are passed here.
key_cmp
represents this comparison function. It takes two void*
to
keys and must returns whether they are equal or not. If not specified,
the default is to compare the pointers themselves, instead of the values
they refer to. key_cmp
must be a pure function, i.e., depend only on its
input values.
key_hash
represents the hashing function. It takes a void*
key and
returns a 64-bit hash representative of the key. For any pair of keys
for which key_cmp
returns true, the hashes must be equal. The default
behavior depends on whether key_cmp
is defined: if it is, all keys will
have the same hash; if it isn't, the pointer is itself hashed. key_hash
must be a pure function, i.e., depend only on its input values.
It is recommended to first preprocess incoming gates with
dqcs_gate_reduce_control()
. In this case, controlled unitary gate
matrices will be reduced to their non-controlled submatrix, such that the
unitary gate detectors will operate on said submatrix. The predefined
unitary gate detectors are more-or-less based on this assumption (as there
are no predefined controlled matrices).
Alternatively, you can preprocess with dqcs_gate_expand_control()
. In
this case, you can use dqcs_gm_add_fixed_unitary()
to detect the full
matrix in all cases, by specifying the CNOT matrix instead of an X matrix
with one control qubit.
If you don't preprocess, the upstream plugin determines the representation. That is, it may send a CNOT as a two-qubit gate with a CNOT matrix or as a controlled X gate with a single target and single control qubit. The gate map will then detect these as two different kinds of gates.