Accessing Parameters and Circuits

Both CircuitCollections and ParameterCollections use nested structures internally —they both use the dictionary-like class KeyMap to store their contents. This level of organization enables a rich interplay between different types of protocols performed on various subsets of the same quantum device. For example, the parameters estimated by the XRB protocol depend on whether the SRB protocol was also performed, and the make_crosstalk_diagnostics() macro generates SRB circuits in both simultaneous and isolated modes.

This guide suplements the API documentation for methods in the CircuitCollection and ParameterCollection classes. Skip ahead to Circuit Collection Internals or ParameterCollection Internals if you are anxious to get to useful examples. Throughout this page, we will use the following objects in our examples:

import trueq as tq

# make some varied circuits in a single CircuitCollection
circuits = tq.CircuitCollection()
circuits.append(tq.make_srb([4, 5], [4, 16]))
circuits.append(tq.make_srb([4], [4, 16]))
circuits.append(tq.make_xrb([4], [4, 12]))
circuits.append(tq.make_cb({(5, 4): tq.Gate.cnot}, [4, 12], pauli_decays=5))

# simulate them and put their results in a ParameterCollection
tq.Simulator().add_stochastic_pauli(px=0.05).run(circuits)
fit = circuits.fit([4, 5])

Before this, we briefly discuss Keys, KeySets, and KeyMaps.

Keys

True-Qᵀᴹ uses Keys to store attributes of some objects like circuits and parameters. This is done to enable efficient filtering and grouping of similar or dissimilar objects. Keys are lightweight hashable dictionaries; their keys and values must both be hashable. Under this constraint one can store anything one likes in a key:

key = tq.Key(fruit="banana", color="yellow", quantity=1)

One main feature of keys is their ability to match against patterns:

key.match(fruit="apple")
False
key.match(fruit="banana")
True
key.match(fruit="banana", color="brown")
False

Additionally, we can use sets to test for inclusion in some group of possibilities:

key.match(fruit={"apple", "pear"})
False
key.match(fruit={"apple", "banana"})
True

Various other conveniences are supported:

"shape" in key
False
"quantity" in key
True
key.quantity
1
key["quantity"]
1
key.names
("fruit", "color", "quantity")
key == tq.Key(color="yellow", fruit="banana", quantity=1)
True

KeySets

KeySets are special set-like containers for Keys.

keys = tq.KeySet(
    tq.Key(fruit="banana", color="yellow"),
    tq.Key(fruit="apple", color="red"),
    tq.Key(fruit="apple", color="yellow"),
    tq.Key(size=1),
)

They exist (instead of using the built-in set) mainly to propagate the match() functionality of Keys. When we call match on a KeySet, a new KeySet is returned that contains all Keys that match.

keys.match(color="yellow")
KeySet(Key(fruit='apple', color='yellow'), Key(fruit='banana', color='yellow'))

keys.match(size=1)
KeySet(tq.Key(size=1))

keys.match(shape="triangle")
KeySet()

They are also useful for getting all unique values of particular parameter:

keys.fruit
{"banana", "apple"}

KeyMaps

Since Keys are hashable, we can use them as keys in any dictionary. The KeyMap class exists (instead of using the built-in dict) to propagate the match() functionality of Keys into various key and value accessor methods. Instead of giving examples of these methods here, it is more instructive to just give examples in the sections below.

Circuit Collection Internals

CircuitCollections use a KeyMap instance as their internal storage. The keys of these dictionary-like storage containers are, of course, Key objects, and the values are lists of circuits. Note that Circuit objects have an attribute key which is a Key. The following cannot be emphasized enough:

Note

CircuitCollections group together circuits by their key attribute. Any two circuits added to a circuit collection with the same key attribute will be found in the same list. Any two circuits with different key attributes will be in different lists.

The protocols built-in to True-Qᵀᴹ typically assign keys such that the only difference between circuits in the same list are randomization choices. For example, in SRB all circuits of length 4 that act on the same set of qubits will be grouped together, whereas circuits with different lengths will be separated.

If you happen to know the key of the list of circuits you are interested in, you can acccess it directly.

key = tq.Key(n_random_cycles=4, protocol='SRB', twirl=(('C1', 4),))
len(circuits[key])
30 # there are 30 because n_circuits=30 when circuits was constructed

However, it is typically inconvenient to manually construct keys in this way. There are four convenience functions for accessing circuits:

  1. trueq.CircuitCollection.keys():

    Returns a KeySet of all keys in the collection that match a given filter.

    For example, let’s get all of the keys in the collection that came from the SRB protocol:

    circuits.keys(protocol="SRB")
    tq.KeySet(
        tq.Key(n_random_cycles=4, protocol="SRB", twirl=(("C1", 4),)),
        tq.Key(n_random_cycles=16, protocol="SRB", twirl=(("C1", 5), ("C1", 4))),
        tq.Key(n_random_cycles=16, protocol="SRB", twirl=(("C1", 4),)),
        tq.Key(n_random_cycles=4, protocol="SRB", twirl=(("C1", 5), ("C1", 4))),
    )
    

    Since the keys() method returns a new KeySet, we have access to all of its methods. For example, this is the easiest way to get all sequence lengths present in the SRB protocol:

    circuits.keys(protocol="SRB").n_random_cycles
    {4, 16}
    

    This enables convenient looping such as the following:

    for m in circuits.keys(protocol="SRB").n_random_cycles:
        for key in circuits.keys(protocol="SRB", n_random_cycles=m):
            pass
    

    Note that in this case circuits.similar_keys("n_random_cycles", protocol="SRB") (see below) would be even more convenient:

  2. trueq.CircuitCollection.subset():

    Returns an iterable over all circuits whose key attribute matches a given filter. This may seamlessly join together circuits in different circuit lists.

    For example, we can put all XRB circuits of this protocol into a new list or CircuitCollection like this:

    list(circuits.subset(protocol="XRB"))
    tq.CircuitCollection(circuits.subset(protocol="XRB"))
    

    We can match on multiple values, or containments:

    circuits.subset(protocol="XRB", n_random_cycles={4,5})
    
  3. trueq.CircuitCollection.similar_keys():

    Returns an iterable over KeySets. Each KeySet contains keys that match the filter, but are additionally grouped by some equal (or unequal) parameter value.

    It is often useful to both group and filter at the same time. Suppose we want all XRB circuits, and that we want to group them by their n_random_cycles value.

    for keys in circuits.similar_keys("n_random_cycles", protocol="XRB"):
        # keys is a KeySet where protocol=XRB, and where every n_random_cycles is equal
        for key in keys:
            pass
    

    As a convenience for more concise code, we can also group where everything except n_random_cycles is equal. This is done with the invert=True flag. This is particularly useful for seq_label in XRB.

    for keys in circuits.similar_keys("seq_label", invert=True, protocol="XRB"):
        # keys is a KeySet where protocol=XRB, and all other parameters except
        # seq_label are equal
        for key in keys:
            pass
    

ParameterCollection Internals

ParameterCollections also internally store their data as a KeyMap. The keys of these dictionary-like storage containers are, of course, Key objects, and the values are ParameterLists which are just lists of Parameters with fancy pattern matching functionality.

The most convenient way to view the output of a ParameterCollection is its summarize() method, or its fancy HTML representation in at IPython Notebook. If you want to access specific values then the relevant methods are listed below, followed by some illustrative examples.

  1. trueq.parameters.ParameterCollection.keys()

    Calling syntax: fit.keys(**filter)

  2. trueq.parameters.ParameterCollection.similar_keys()

    Calling syntax: fit.similar_keys(*names, invert=False, **filter)

  3. trueq.parameters.ParameterCollection.subset()

    Calling syntax: fit.subset(pattern="*", **filter)

  4. trueq.parameters.ParameterCollection.flat_items()

    Calling syntax: fit.flat_items(pattern="*", **filter)

  5. trueq.parameters.ParameterCollection.items()

    Calling syntax: fit.subset(**filter)

Suppose we are after the SRB infidelity of the qubit with label 4. Our first step might be to list all of the SRB keys just to see what sorts of parameters they have:

fit.keys()
KeySet(
    Key(labels=(4,), protocol="SRB", twirl=(("C1", 4), ("C1", 5))),
    Key(labels=(5,), protocol="SRB", twirl=(("C1", 4), ("C1", 5))),
    Key(labels=(4,), protocol="SRB", twirl=(("C1", 4),)),
    Key(labels=(4,), protocol="XRB", twirl=(("C1", 4),)),
    Key(
        cycle="Cycle((5, 4): Gate.cx, immutable=True)",
        labels=(4,),
        pauli_decay="Y",
        protocol="CB",
        twirl=(("P", 5), ("P", 4)),
    ),
    ...
)

Looking at these keys we can choose a filter to pass to keys() and then extract the corresponding parameters:

[fit[key].r for key in fit.keys(protocol="SRB", labels=(4,))]
[
    Parameter(r, 0.03081, 0.003481, 'Average gate infidelity of the error map'),
    Parameter(r, 0.03469, 0.003992, 'Average gate infidelity of the error map')
]

Note

In the example above, notice that fit[key] has type ParameterList, which allows us the notation fit[key].r; this grabs the parameter named r from the list.

You will notice that there are two entries returned in the list. This is because we added SRB to the circuit collection in two different ways; we are seeing the estimates of r when SRB is isolated on qubit 4, and when SRB is run simultaneously on qubits (4,5). If this behaviour were undesired, we would have to filter further. For example, we could filter by the twirling group of the Cliffords on qubit 4:

[param_list.r for param_list in fit.subset(protocol="SRB", twirl=(("C1", 4),))]
[Parameter(r, 0.03469, 0.003992, 'Average gate infidelity of the error map')]

Finally, since we aren’t doing anything with the key we are iterating over in the example above, we might as well use the subset() method instead:

list(fit.subset("r", protocol="SRB", twirl=(('C1', 4),)))[0]
Parameter(r, 0.03469, 0.003992, 'Average gate infidelity of the error map')

In general, flat_items() is probably the most versitile and useful of the functions enumerated above. Let’s say we wanted the estimate and standard-deviation of all parameters whose name starts with p and whose associated Key contains the parameter called pauli_decay. flat_items() will iterate over (Key, Parameter) pairs that match. Note that we are allowed one wild-card character in the parameter name, "p*".

{
    (key.labels, key.pauli_decay): param.estimate
    for key, param in fit.flat_items("p*")
    if "pauli_decay" in key
}
{
    ((4,), 'X'): 1.000936070952022,
    ((4,), 'Y'): 0.8071669611511227,
    ((4,), 'Z'): 0.7771025307013352
    ((5,), 'X'): 1.0029630993368421,
    ((5,), 'Y'): 0.7997100046417387,
    ((5,), 'Z'): 0.790959651528612,
}