Configuration¶
Configurability is a primary design goal of OpenQL: instead of hardcoding the way in which an algorithm is compiled for a particular platform, both the platform and the strategy for compiling to it are completely configurable. As such, OpenQL has quite a complex configuration system.
Most of the configuration is provided to OpenQL via JSON files. OpenQL uses a superset of the JSON file format for all input files, namely one that allows //
-based single-line comments; therefore, a configuration file written for OpenQL is not strictly valid JSON, but OpenQL can parse any valid JSON file (as long as it complies with the expected structure).
The two most important configuration file types are the platform and compiler configuration files.
The platform configuration file includes everything OpenQL needs to know about the target platform (i.e., what the quantum chip and microarchitecture looks like), and optionally includes information about how to compile for it. This file is passed to OpenQL when you construct a
ql.Platform
. OpenQL also has a number of default platform configuration files built into it; one for each architecture variant. Furthermore, architecture variants may include preprocessing logic for the platform configuration file, such that repetitive things for a particular platform can automatically be expanded; in this case, the description below documents the resulting structure, not necessarily what you would write (although the preprocessing logic should be minimal, such as only providing additional default values). See also the section on supported architectures.The compiler configuration file describes the steps that OpenQL should take to transform the incoming program to something that can run on the platform (or at least is no further away from being able to run on it). This is also referred to as the pass list or compilation strategy. Besides configuration via JSON, the strategy can also be configured using the Python/C++ API directly; this is particularly useful when you for example only want to insert a visualizer pass into the existing, default pass list, or when you’re doing design-space exploration to determine the optimal compilation strategy for a particular algorithm.
The structure of these files is documented below.
Platform configuration¶
The platform configuration JSON file (or JSON data, as it’s not necessarily always in file form) represents a complete description of what the target platform looks like, and optionally how to compile for it. At the top level, the structure is a JSON object, with the following keys recognized by OpenQL’s platform-agnostic logic, customarily written in the following order.
"eqasm_compiler"
: an optional description of how to compile for this platform."hardware_settings"
: contains basic descriptors for the hardware, such as qubit count and cycle time."topology"
: optionally provides a more in-depth description of how the qubits are organized."resources"
: optionally provides information about scheduling constraints, for example due to a number of qubits sharing a single waveform generator."instructions"
: lists the instruction set supported by the platform."gate_decomposition"
: optionally lists a set of decomposition rules that are immediately applied when a gate with a particular name is added.
Note
The plan is to move away from on-the-fly gate decomposition, and instead make a gate decomposition pass. The exact design for this has not been made yet, but it’s possible that the gate decomposition section will change in minor ways in the future, or will be deprecated in favor of an entirely new configuration file section.
Depending on the architecture being compiled for, as specified through the
"eqasm_compiler"
key or via the compiler configuration file override
during platform construction, additional sections or keys may be optional or
required, or entire sections may even be generated. Refer to the
architecture documentation for details to this end. The "none"
architecture by definition does not do any of this, and can thus always be
used as an override of sorts for this behavior; the only thing that the
architecture variant specification does is provide better defaults for the
platform and compiler configuration, so everything can always be specified
using the "none"
architecture if need be. The remainder of this page thus
describes the “universal” structure as used by "none"
, while the
architecture documentation may make achitecture-specific addenda.
In addition, passes are allowed to make use of additional structures in the configuration file. This implies that the common OpenQL code will not check for or warn you about unrecognized keys: it assumes that these will be read by something it is not aware of.
"eqasm_compiler"
section¶
The "eqasm_compiler"
key can take any of the following types of
values.
No value/unspecified: the defaults for the “none” architecture will implicitly be used.
A string matching one of the available architectures or architecture variants, for example
"cc"
or"cc_light.s7"
: the defaults for that architecture (variant) will be used. Legacy values, such as"eqasm_compiler_cc"
or"qx"
may also be used. Refer to the architecture documentation for a full and up-to-date list of recognized values.A filename: the specified file will be interpreted as a compiler configuration file, fully specifying what the compiler looks like.
A JSON object: the contents of the object will be interpreted as a compiler configuration file, again fully specifying what the compiler looks like, but without the extra file indirection.
This key can also be completely overridden by explicitly specifying a compiler configuration file during platform construction, thus allowing the platform and compiler configuration files to be completely disjoint, if this is preferred for the intended application.
"hardware_settings"
section¶
This must map to an object containing the basic parameters that describe the platform. OpenQL’s common code recognizes the following.
"qubit_number"
: must map to an integer specifying the total number of qubits in the platform. Qubit indices start at zero, so all indices must be in the range 0..N-1, where N is this value."creg_number"
: optionally specifies the number of 32-bit integer classical registers available in the platform. If not specified, the value will be inferred from the constructor ofql.Program
."breg_number"
: optionally specifies the number of single-bit classical registers available in the platform, used for receiving measurement results and predicates. If not specified, the value will be inferred from the constructor ofql.Program
."cycle_time"
: optionally specifies the cycle time used by the platform in nanoseconds. Currently this must be an integer value. If not specified, 1 will be used as a default, thus equating the nanosecond values to cycle values.
"topology"
section¶
The topology JSON object must have the following structure.
{
"form": <optional string, either "xy" or "irregular">,
"x_size": <optional integer for form="xy">,
"y_size": <optional integer for form="xy">,
"qubits": <mandatory array of objects for form="xy", unused for "irregular">,
"number_of_cores": <optional positive integer, default 1>,
"comm_qubits_per_core": <optional positive integer, num_qubits / number_of_cores>,
"connectivity": <optional string, either "specified" or "full">,
"edges": <mandatory array of objects for connectivity="specified", unused for "full">
...
}
The "form"
key specifies whether the qubits can be arranged in a 2D grid
of integer coordinates ("xy"
) or not ("irregular"
). If irregular, mapper
heuristics that rely on sorting possible paths by angle are unavailable.
If "xy"
, "x_size"
and "y_size"
specify the coordinate ranges (from
zero to the limit minus one), and "qubits"
specifies the coordinates.
"qubits"
must then be an array of objects of the following form:
{
"id": <qubit index, mandatory>,
"x": <X coordinate, mandatory>,
"y": <Y coordinate, mandatory>,
...
}
Each qubit must be specified exactly once. Any additional keys in the object are silently ignored, as other parts of OpenQL may use the structure as well.
If the "form"
key is missing, its value is derived from whether a
"qubits"
list is given. If "x_size"
or "y_size"
are missing, the
values are inferred from the largest coordinate found in "qubits"
.
The "number_of_cores"
key is used to specify multi-core architectures.
It must be a positive integer. Each core is assumed to have the same
number of qubits, so the total number of qubits must be divisible by this
number. The first N qubits belong to core 0, the next N belong to core 1,
etc, where N equals the total number of qubits divided by the number of
cores.
Cores can communicate only via communication qubits. The amount of these
qubits per cores may be set using the "comm_qubits_per_core"
key. Its
value must range between 1 and the number of qubits per core, and
defaults to the latter. The first N qubits for each core are considered
to be communication qubits, whereas the remainder are local qubits.
The "connectivity"
key specifies whether there are qubit connectivity
constraints ("specified"
) or all qubits (within a core) are connected
("full"
). In the former case, the "edges"
key must map to an array of
objects of the following form:
{
"id": <optional unique identifying integer>,
"src": <source qubit index, mandatory>,
"dst": <target qubit index, mandatory>,
...
}
Edges are directional; to allow qubits to interact “in both ways,” both directions must be specified. If any identifiers are specified, all edges should get one, and they should all be unique; otherwise, indices are generated using src*nq+dst. Any additional keys in the object are silently ignored, as other parts of OpenQL may use the structure as well (although they should preferably just extend this class).
When "connectivity"
is set to "full"
in a multi-core environment,
inter-core edges are only generated when both the source and destination
qubit is a communication qubit.
If the "connectivity"
key is missing, its value is derived from whether
an “edges” list is given.
Any additional keys in the topology root object are silently ignored, as other parts of OpenQL may use the structure as well.
"resources"
section¶
Two JSON structures are supported: one for compatibility with older platform configuration files, and one extended structure. The extended structure has the following syntax:
{
"architecture": <optional string, default "">,
"dnu": <optional list of strings, default []>,
"resources": {
"<name>": {
"type": "<type>",
"config": {
<optional configuration>
}
}
...
}
}
The optional "architecture"
key may be used to make shorthands for
architecture- specific resources, normally prefixed with
"arch.<architecture>."
. If it’s not specified or an empty string,
the architecture is derived from the compiler configuration
(either "eqasm_compiler"
or as overridden by the compiler configuration
file passed to the platform constructor).
The optional "dnu"
key may be used to specify a list of do-not-use
resource types (experimental, deprecated, or any other resource that’s
considered unfit for “production” use) that you explicitly want to use,
including the "dnu"
namespace they are defined in. Once specified, you’ll
be able to use the resource type without the "dnu"
namespace element. For
example, if you would include "dnu.whatever"
in the list, the resource
type "whatever"
may be used to add the resource.
The "resources"
key specifies the actual resource list. This consists of
a map from unique resource names matching [a-zA-Z0-9_\-]+
to a resource
configuration. The configuration object must have a “type” key, which
must identify a resource type that OpenQL knows about. The “config” key
is optional, and is used to pass type-specific configuration data to the
resource. If not specified, an empty JSON object will be passed to the
resource instead.
If the "resources"
key is not present, the old structure is used instead.
This has the following simplified form:
{
"<type>": {
<configuration>
},
...
}
"instructions"
section¶
This section specifies the instruction set of the architecture. It must be an object, where each key represents the name of a gate, and the value is again an object, containing any semantical information needed to describe the instruction.
Note
OpenQL currently derives much of the semantics from the gate name. For example, the cQASM writer determines whether it should emit a gate angle parameter based on whether the name of the gate equals a set of gate names that would logically have an angle. This is behavior is very much legacy, and is to be replaced with checking for keys in the instruction definition (though of course using appropriate defaults for backward compatibility).
OpenQL supports two classes of instructions: generalized gates and
specialized gates. For generalized gates, the gate name (i.e. the JSON
object key) must be a single identifier matching [a-zA-Z_][a-zA-Z0-9_]*
.
This means that the gate can be applied to any set of operands.
Specialized gates, on the other hand, have a fixed set of qubit operands.
Their JSON key must be of the form <name> <qubits>
, where <name>
is
as above, and <qubits>
is a comma-separated list (without spaces) of
qubit indices, each of the form q<index>
. For example, a correct name
for a specialized two-qubit gate would be cz q0,q1
. Specialized gates
allow different semantical parameters to be specified for each possible
set of qubit operands: for example, the duration for a particular gate on
a particular architecture may depend on the operands.
Note
If you’re using the mapper, and thus the input to your program is unmapped, OpenQL will also use the gate specializations when the gates still operate on virtual qubits. Therefore, you must specify a specialization for all possible qubit operand combinations of a particular gate, even if the gate does not exist for a particular combination due to for example connectivity constraints. Alternatively, you may specify a fallback using a generalized gate, as OpenQL will favor specialized gates when they exist.
Within the instruction definition object, OpenQL’s architecture/pass-agnostic logic currently only recognizes the following keys.
Note
Older versions of OpenQL recognized and required the existence of
many more keys, such as "matrix"
and "latency"
. All passes relying on
this information have since been cleaned out as they were no longer in
use, and all requirements on the existence of these keys have likewise
been lifted.
"cqasm_name"
key¶
Specifies an alternative name for the instruction when it is printed as cQASM or when read from cQASM. This must be a valid identifier. If not specified, it defaults to the normal instruction name.
"prototype"
key¶
Specifies the amount and type of operands that the instruction expects. If specified, it must be an array of strings. Each of these strings represents an operand, and must be set to the name of its expected type, optionally prefixed with the access mode, separated by a colon. By default, the available types are:
qubit
for qubits;bit
for classical bits;int
for 32-bit signed integers; andreal
for floating-point numbers.
The available access modes are:
B
for barriers (DDG write, liveness ignore);W
for write access or qubit state preparation (DDG write, liveness kill);U
for read+write/update access or regular non-commuting qubit usage (DDG write, liveness use);R
for read-only access (DDG read, liveness use);L
for operands that must be literals;X
for qubit access that behaves like an X rotation (DDG X, liveness use);Y
for qubit access that behaves like an Y rotation (DDG Y, liveness use);Z
for qubit access that behaves like an Z rotation (DDG Z, liveness use);M
for a measurement of the qubit for which the result is written to its implicitly associated bit register (DDG W for the qubit and its bit, liveness use for the qubit, and liveness kill for the bit); andI
for operands that should be ignored (DDG ignore, liveness ignore).
If the access mode is not specified for an operand, U
is assumed, as
it is the most pessimistic mode available. If no prototype is specified
at all, it will be inferred based on the instruction name for backward
compatibility, using the following rules (first regex-matching rule
applies):
move_init|prep(_?[xyz])?
->["W:qubit"]
h|i
->["U:qubit"]
rx
->["X:qubit", "L:real"]
(m|mr|r)?xm?[0-9]*
->["X:qubit"]
ry
->["Y:qubit", "L:real"]
(m|mr|r)?ym?[0-9]*
->["Y:qubit"]
rz
->["Z:qubit", "L:real"]
crz?
->["Z:qubit", "Z:qubit", "L:real"]
crk
->["Z:qubit", "Z:qubit", "L:int"]
[st](dag)?|(m|mr|r)?zm?[0-9]*
->["Z:qubit"]
meas(ure)?(_?[xyz])?(_keep)?
->["M:qubit"]
and["U:qubit", "W:bit"]
(teleport)?(move|swap)
->["U:qubit", "U:qubit"]
cnot|cx
->["Z:qubit", "X:qubit"]
cphase|cz
->["Z:qubit", "Z:qubit"]
cz_park
->["Z:qubit", "Z:qubit", "I:qubit"]
toffoli
->["Z:qubit", "Z:qubit", "X:qubit"]
no operands otherwise.
Furthermore, when gates are added via the API or old IR that don’t
match an existing instruction due to prototype mismatch, and the
prototype was inferred per the above rules, a clone is made of the
instruction type with the prototype inferred by means of the actual
operands, using the U
access mode for reference operands and R
for
anything else. When a default gate is encountered in the old IR and
needs to be converted to the new IR, the entire instruction type is
inferred from the default gate. From now on, however, it is strongly
recommended to explicitly specify prototypes and not rely on this
inference logic.
Note
It is possible to define multiple overloads for an instruction with the same name. Passes using the new IR will be able to distinguish between these overloads based on the types and writability of the operands, but be aware that any legacy pass will use one of the gate definitions at random (so differing duration or other attributes won’t work right). The JSON syntax for this is rather awkward, since object keys must be unique; the best thing to do is to just append spaces for the key, since these spaces are cleaned up when the instruction type is parsed.
"barrier"
key¶
An optional boolean that specifies that an instruction is to behave as a complete barrier, preventing it from being commuted with any other instruction during scheduling, and preventing optimizations on it. If not specified, the flag defaults to false.
"duration"
or "duration_cycles"
key¶
These keys specify the duration of the instruction in nanoseconds or cycles. It is illegal to specify both of them for a single instruction. If neither is specified, the duration defaults to a single cycle.
Note
OpenQL currently only supports durations that are an integer number of nanoseconds, so any fractions will be rounded up to the nearest nanosecond. Furthermore, in almost all contexts, the duration of an instruction will be rounded up to the nearest integer cycle count.
"decomposition"
key¶
May be used to specify one or more decomposition rules for the
instruction type. Unlike the rules in the "gate_decomposition"
section, these rules are normally only applied by an explicit
decomposition pass, of which the predicate matches the name and/or
additional JSON data for the rule. The value for the "decomposition"
key can take a number of shapes:
a single string: treated as a single, anonymous decomposition rule of which the decomposition is defined by the string parsed as a single-line cQASM 1.2 block;
an array of strings: as above, but each string represents a new line in the cQASM block;
a single object: treated as a single decomposition specification; or
an array of objects: treated as multiple decomposition specifications.
A decomposition specification object must have an "into"
key that
specifies the decomposition, which must be a single string or an array
of strings as above. In addition, it may be given a name via the
"name"
key, or any number of other keys for passes to use to
determine whether to apply a decomposition rule, or which decomposition
rule to apply if multiple options are defined.
The cQASM 1.2 block must satisfy the following rules:
the version header must not be specified (it is added automatically);
subcircuits and goto statements are not supported;
the operands of the to-be-decomposed gate can be accessed using the
op(int) -> ...
function, where the integer specifies the operand index (which must constant-propagate to an integer literal); andthe duration of the decomposed block may not be longer than the duration of the to-be-decomposed instruction (either make the instruction duration long enough, or define the decomposition as a single bundle and (re)schedule after applying the decomposition).
Other than that, the cQASM code is interpreted using the default
cQASM 1.2 rules. Note that this also means that the cQASM name of an
instruction must be used if said instructions has differing OpenQL and
cQASM names. Refer to the documentation of the cQASM 1.2 reader pass
(io.cqasm.Read
) for more information.
As an example, a CNOT gate with its usual decomposition might be specified as follows.
{
"prototype": ["Z:qubit", "X:qubit"],
"duration_cycles": 4,
"decomposition": {
"name": "to_cz",
"into": [
"ym90 op(1)",
"cz op(0), op(1)",
"skip 1",
"y90 op(1)"
]
}
}
Note that application of this decomposition rule would retain program validity with respect to schedule and data dependencies (if it was valid before application) for platforms where single-qubit rotations are single-cycle and the CZ gate is two-cycle, because the CNOT gate is defined to take four cycles, and the schedule of the decomposition is valid.
Note
The "decomposition"
key is only supported when the instruction
prototype is explicitly specified using the "prototype"
key.
Without a fixed prototype, type checking the op()
function would be
impossible.
"qubits"
key¶
This must map to a single qubit index or a list of qubit indices that
corresponds to the qubits in the specialization. For generalized
instructions, the list must either be empty or unspecified. The qubit
indices can be specified as either a string of the form "q<index>"
or
an integer with just the index.
Note
This field is obviously redundant. As such, it may be removed in the future, from which point onward it will be ignored.
"gate_decomposition"
section¶
This section specifies legacy decomposition rules for gates/instructions. They are applied in the following cases:
when a gate matching a decomposition rule is added to a kernel using the API;
immediately before a legacy pass (i.e. one that still operates on the old IR) is run; and
when a decomposition pass matching rules named “legacy” is run.
Rules in this section support only a subset of what the new decomposition system supports. For example, scheduling information cannot be represented, the to-be-decomposed instruction can only have qubit operands, and the to-be-decomposed instruction can’t exist in the old IR without being decomposed (preventing operations on it before decomposition). For these reasons, this decomposition system is deprecated, and only still exists for backward compatibility.
If the section is specified, it must be an object, where each key represents the name of the to-be-decomposed gate, along with capture groups for the qubit operands. The keys must map to arrays of strings, wherein each string represents a gate in the decomposition.
Examples of two decompositions are shown below. %0
and %1
refer to the
first argument and the second argument. This means according to the
decomposition on line 2, rx180 %0
will allow us to decompose rx180 q0
to x q0
. Similarly, the decomposition on line 3 will allow us to
decompose cnot q2, q0
to three instructions, namely: ry90 q0
,
cz q2, q0
, and ry90 q0
.
"gate_decomposition": {
"rx180 %0" : ["x %0"],
"cnot %0,%1" : ["ry90 %1","cz %0,%1","ry90 %1"]
}
These decompositions are simple macros (in-place substitutions) which allow programmer to manually specify a decomposition. These take place at the time of creation of a gate in a kernel. This means the scheduler will schedule decomposed instructions.
Note
Decomposition rules may only refer to custom gates that have already been defined in the instruction set.
Note
Recursive decomposition rules, i.e. decompositions that make use of other decomposed gate definitions, are not supported. Behavior for this is undefined; the nested rules may or may not end up being expanded, and if they’re not, internal compiler errors may result.
Note
These decomposition rules are intended to be replaced by a more powerful system in the future.
Compiler configuration¶
The compiler configuration JSON file (or JSON substructure) is expected to have the following structure:
{
"architecture": <optional string, default "">,
"dnu": <optional list of strings, default []>,
"pass-options": <optional object, default {}>,
"compatibility-mode": <optional boolean, default false>,
"passes": [
<pass description>
]
}
The optional "architecture"
key may be used to make shorthands for
architecture- specific passes, normally prefixed with
"arch.<architecture>."
. If it’s not specified or an empty string, no
shorthand aliases are made.
The optional "dnu"
key may be used to specify a list of do-not-use pass
types (experimental passes, deprecated passes, or any other pass that’s
considered unfit for “production” use) that you explicitly want to use,
including the “dnu” namespace they are defined in. Once specified, you’ll
be able to use the pass type without the "dnu"
namespace element. For
example, if you would include "dnu.whatever"
in the list, the pass type
"whatever"
may be used to add the pass.
The optional "pass-options"
key may be used to specify options common to
all passes. The values may be booleans, integers, strings, or null, but
nothing else. Null is used to reset an option to its hardcoded default
value. An option need not exist for each pass affected by it; if it
doesn’t, the default value is silently ignored for that pass. However, if
it does exist, it must be a valid value for the option with that name.
These option values propagate through the pass tree recursively, so
setting a default option in the root using this record will affect all
passes.
If "compatibility-mode"
is enabled, some of OpenQL’s global options add
implicit entries to the "pass-options"
structure when set, for backward
compatibility. However, entries in "pass-options"
always take precedence.
The logic for which options map to which is mostly documented in the
global option docs now, since those options don’t do anything else
anymore. Note that the global options by their original design have no
way to specify what pass they refer to, so each option is attempted for
each pass type! Which means we have to be a bit careful with picking
option names for the passes that are included in compatibility mode.
Pass descriptions can either be strings (in which case the string is interpreted as a pass type alias and everything else is inferred/default), or an object with the following structure.
{
"type": <optional string, default "">,
"name": <optional string, default "">,
"options": <optional object, default {}>
}
The "type"
key, if specified, must identify a pass type that OpenQL knows
about. You can call print_pass_types()
on a ql.Compiler
object to get
the list of available pass types (and their documentation) for your
particular configuration (just make an empty compiler object initially), or
you can read the documentation section on supported passes. If the "type"
key is not specified or empty, a group is made instead, and "group"
must
be specified for the group to do anything.
The "name"
key, if specified, is a user-defined name for the pass, that
must match [a-zA-Z0-9_\-]+
and be unique within the surrounding pass
list. If not specified, a name that complies with these requirements is
generated automatically, but the actual generated name should not be
relied upon to be consistent between OpenQL versions. The name may be
used to programmatically refer to passes after construction, and passes
may use it for logging or unique output filenames. However, passes should
not use the name for anything that affects the behavior of the pass.
The "options"
key, if specified, may be an object that maps option names
to option values. The values may be booleans, integers, strings, or null,
but nothing else. Null is used to enforce usage of the OpenQL-default
value for the option. The option names and values must be supported by
the particular pass type.