graphix.opengraph module¶
Class for open graph states.
- class OpenGraph(graph: nx.Graph[int], input_nodes: Sequence[int], output_nodes: Sequence[int], measurements: Mapping[int, _AM_co], output_cliffords: Mapping[int, Clifford] = <factory>)[source]¶
Bases:
Parameterizable,Simulable[_AM_co],Generic[_AM_co]An unmutable dataclass providing a representation of open graph states.
- graph¶
The underlying resource-state graph. Nodes represent qubits and edges represent the application of \(CZ\) gate on the linked nodes.
- Type:
- input_nodes¶
An ordered sequence of node labels corresponding to the open graph inputs.
- Type:
Sequence[int]
- output_nodes¶
An ordered sequence of node labels corresponding to the open graph outputs.
- Type:
Sequence[int]
- measurements¶
A mapping between the non-output nodes of the open graph (
key) and their corresponding measurement label (value). Measurement labels can be specified asMeasurement,PlaneorAxisinstances.- Type:
Mapping[int, _AM_co]
- output_cliffords¶
A mapping between output nodes of the open graph (
key) and Clifford operators. This attribute allows to preserve the semantics of (deterministic) patterns with local-clifford commands when extracting the open graph. SeePattern.to_opengraph().
Notes
The inputs and outputs of
OpenGraphinstances in Graphix are defined as ordered sequences of node labels. This contrasts the usual definition of open graphs in the literature, where inputs and outputs are unordered sets of nodes labels. This restriction facilitates the interplay withPatternobjects, where the order of input and output nodes represents a choice of Hilbert space basis.Example
>>> import networkx as nx >>> from graphix.opengraph import OpenGraph >>> from graphix.measurements import Measurement >>> >>> graph = nx.Graph([(0, 1), (1, 2)]) >>> measurements = {i: Measurement.XY(0.5 * i) for i in range(2)} >>> input_nodes = [0] >>> output_nodes = [2] >>> og = OpenGraph(graph, input_nodes, output_nodes, measurements)
- to_pattern(*, stacklevel: int = 1) Pattern[source]¶
Extract a deterministic pattern from an
OpenGraph[Measurement]if it exists.- Parameters:
stacklevel (int, optional) – Stack level to use for warnings. Defaults to 1, meaning that warnings are reported at this function’s call site.
- Returns:
A deterministic pattern on the open graph.
- Return type:
- Raises:
OpenGraphError – If the open graph does not have flow.
Notes
The open graph instance must be of parametric type
Measurementto allow for a pattern extraction, otherwise it does not contain information about the measurement angles.This method proceeds by searching a flow on the open graph and converting it into a pattern as prescripted in Ref. [1_to_pattern]. It first attempts to find a causal flow because the corresponding flow-finding algorithm has lower complexity. If it fails, it attemps to find a Pauli flow because this property is more general than a generalised flow, and the corresponding flow-finding algorithms have the same complexity in the current implementation.
References
[1_to_pattern]Browne et al., NJP 9, 250 (2007)
- draw(**options: Unpack[DrawKwargs]) None[source]¶
Visualize the opengraph.
- Parameters:
options (Unpack[DrawKwargs]) – Options controlling graph visualization. See
VisualizationOptions.
- map(f: Callable[[_A], _B]) OpenGraph[_B][source]¶
Apply a function to every measurement of the open graph and return the resulting open graph.
- Parameters:
f (Callable[[_A], _B]) – Function applied to each measurement.
- Returns:
The resulting open graph.
- Return type:
OpenGraph[_B]
Notes
The types of measurements can be changed by
f.Example
>>> import networkx as nx >>> from graphix.opengraph import OpenGraph >>> from graphix.measurements import Measurement >>> graph = nx.Graph([(0, 1), (1, 2)]) >>> measurements = {0: Measurement.XY(0.5), 1: Measurement.YZ(1)} >>> og = OpenGraph(graph, [0], [2], measurements) >>> og.map(lambda m: m.plane).measurements {0: Plane.XY, 1: Plane.YZ}
- to_bloch() OpenGraph[BlochMeasurement][source]¶
Return the open graph where all measurements are converted to Bloch.
Example
>>> import networkx as nx >>> from graphix.opengraph import OpenGraph >>> from graphix.measurements import Measurement >>> graph = nx.Graph([(0, 1), (1, 2)]) >>> measurements = {0: Measurement.X, 1: Measurement.Y} >>> og = OpenGraph(graph, [0], [2], measurements) >>> og.to_bloch().measurements {0: Measurement.XY(0), 1: Measurement.XY(0.5)}
- downcast_bloch() OpenGraph[BlochMeasurement][source]¶
Return the open graph if all measurements are described as Bloch measurements; raise
TypeErrorotherwise.If this method succeeds, the returned graph is identical to the original one, but this function narrows the static type of the measurements. The returned graph can be used with functions that require all measurements are described as Bloch measurements, such as
OpenGraph.to_causalflow().Example
>>> import networkx as nx >>> from graphix.opengraph import OpenGraph >>> from graphix.measurements import Measurement >>> graph = nx.Graph([(0, 1), (1, 2)]) >>> measurements = {0: Measurement.XY(0.5), 1: Measurement.YZ(1)} >>> og = OpenGraph(graph, [0], [2], measurements) >>> og.downcast_bloch().measurements {0: Measurement.XY(0.5), 1: Measurement.YZ(1)} >>> measurements = {0: Measurement.X, 1: Measurement.Y} >>> og = OpenGraph(graph, [0], [2], measurements) >>> og.downcast_bloch() Traceback (most recent call last): ... TypeError: Bloch measurement expected, but Pauli measurement was found.
- infer_pauli_measurements(rel_tol: float = 1e-09, abs_tol: float = 0.0) OpenGraph[Measurement][source]¶
Return an equivalent open graph in which Bloch measurements close to a Pauli measurement are replaced by Pauli measurements.
Pauli measurements are measurements with a Pauli angle, i.e. an integer multiple of \(\pi/2\).
This method can be used before calling methods such as
OpenGraph.to_pauliflow_or_none, that deal with Pauli measurements in a special way.- Parameters:
pattern (pattern) – Source pattern.
rel_tol (float, optional) – Relative tolerance for comparing angles, passed to
math.isclose(). Default is1e-9.abs_tol (float, optional) – Absolute tolerance for comparing angles, passed to
math.isclose(). Default is0.0.
- Returns:
An equivalent open graph in which Bloch measurements close to a Pauli measurement are replaced by Pauli measurements.
- Return type:
Example
>>> import networkx as nx >>> from graphix.opengraph import OpenGraph >>> from graphix.measurements import Measurement >>> graph = nx.Graph([(0, 1), (1, 2)]) >>> measurements = {0: Measurement.XY(0.5), 1: Measurement.YZ(1)} >>> og = OpenGraph(graph, [0], [2], measurements) >>> og.infer_pauli_measurements().measurements {0: Measurement.Y, 1: -Measurement.Z}
- isclose(other: OpenGraph[_AM_co], rel_tol: float = 1e-09, abs_tol: float = 0.0) bool[source]¶
Check if two open graphs are equal within a given tolerance.
- Parameters:
- Returns:
Trueif the two open graphs are approximately equal.- Return type:
Notes
- This method verifies the open graphs have:
Truly equal underlying graphs (not up to an isomorphism).
Equal input and output nodes.
Same measurement planes or axes and approximately equal measurement angles if the open graph is of parametric type
Measurement.Equal
output_cliffordsmappings.
The static typer does not allow an
isclosecomparison of two open graphs with different parametric type. For a structural comparison, seeOpenGraph.is_equal_structurally().
- is_equal_structurally(other: OpenGraph[AbstractMeasurement]) bool[source]¶
Compare the underlying structure of two open graphs.
- Parameters:
other (OpenGraph[AbstractMeasurement])
- Returns:
Trueifselfandoghave the same structure.- Return type:
Notes
This method verifies the open graphs have:
Truly equal underlying graphs (not up to an isomorphism).
Equal input and output nodes. This assumes equal types as well, i.e., if
self.input_nodesis alistandother.input_nodesis atuple, this method will returnFalse.Equal
output_cliffordsmappings.
It assumes the open graphs are well formed.
The static typer allows comparing the structure of two open graphs with different parametric type.
- neighbors(nodes: Collection[int]) set[int][source]¶
Return the set containing the neighborhood of a set of nodes in the open graph.
- odd_neighbors(nodes: Collection[int]) set[int][source]¶
Return the set containing the odd neighborhood of a set of nodes in the open graph.
- to_causalflow() CausalFlow[_PM_co][source]¶
Try to extract a causal flow on the open graph.
This method is a wrapper over
OpenGraph.to_causalflow_or_none()with a single return type.This method requires all measurements to be planar: to extract the causal flow from a graph that contains Pauli measurements, you can apply
OpenGraph.to_bloch()first.- Returns:
A causal flow object if the open graph has causal flow.
- Return type:
CausalFlow[_PM_co]
- Raises:
OpenGraphError – If the open graph does not have a causal flow.
See also
- to_gflow() GFlow[_PM_co][source]¶
Try to extract a maximally delayed generalised flow (gflow) on the open graph.
This method is a wrapper over
OpenGraph.to_gflow_or_none()with a single return type.This method requires all measurements to be planar: to extract a gflow from a graph that contains Pauli measurements, you can apply
OpenGraph.to_bloch()first.- Returns:
A gflow object if the open graph has gflow.
- Return type:
GFlow[_PM_co]
- Raises:
OpenGraphError – If the open graph does not have a gflow.
See also
- to_pauliflow(*, stacklevel: int = 1) PauliFlow[_AM_co][source]¶
Try to extract a maximally delayed Pauli on the open graph.
This method is a wrapper over
OpenGraph.to_pauliflow_or_none()with a single return type.- Parameters:
stacklevel (int, optional) – Stack level to use for warnings. Defaults to 1, meaning that warnings are reported at this function’s call site.
- Returns:
A Pauli flow object if the open graph has Pauli flow.
- Return type:
PauliFlow[_AM_co]
- Raises:
OpenGraphError – If the open graph does not have a Pauli flow.
- to_causalflow_or_none() CausalFlow[_PM_co] | None[source]¶
Return a causal flow on the open graph if it exists.
This method requires all measurements to be planar: to find the causal flow from a graph that contains Pauli measurements, you can apply
OpenGraph.to_bloch()first.- Returns:
A causal flow object if the open graph has causal flow or
Noneotherwise.- Return type:
CausalFlow[_PM_co] | None
See also
Notes
The open graph instance must be of parametric type
MeasurementorPlanesince the causal flow is only defined on open graphs with \(XY\) measurements.This function implements the algorithm presented in Ref. [1] with polynomial complexity on the number of nodes, \(O(N^2)\).
References
[1] Mhalla and Perdrix, (2008), Finding Optimal Flows Efficiently, doi.org/10.1007/978-3-540-70575-8_70
- to_gflow_or_none() GFlow[_PM_co] | None[source]¶
Return a maximally delayed Pauli on the open graph if it exists.
This method requires all measurements to be planar: to find a gflow in a graph that contains Pauli measurements, you can apply
OpenGraph.to_bloch()first.- Returns:
A gflow object if the open graph has gflow or
Noneotherwise.- Return type:
GFlow[_PM_co] | None
See also
Notes
The open graph instance must be of parametric type
MeasurementorPlanesince the gflow is only defined on open graphs with planar measurements. Measurement instances with a Pauli angle (integer multiple of \(\pi/2\)) are interpreted asPlaneinstances, in contrast withOpenGraph.to_pauliflow_or_none().This function implements the algorithm presented in Ref. [1] with polynomial complexity on the number of nodes, \(O(N^3)\).
References
[1] Mitosek and Backens, 2024 (arXiv:2410.23439).
- to_pauliflow_or_none(*, stacklevel: int = 1) PauliFlow[_AM_co] | None[source]¶
Return a maximally delayed Pauli on the open graph if it exists.
- Parameters:
stacklevel (int, optional) – Stack level to use for warnings. Defaults to 1, meaning that warnings are reported at this function’s call site.
- Returns:
A Pauli flow object if the open graph has Pauli flow or
Noneotherwise.- Return type:
PauliFlow[_AM_co] | None
Notes
Measurements are considered to be Pauli only if they are represented as
graphix.measurements.PauliMeasurement. The methodOpenGraph.infer_pauli_measurements()can be called first to convert all measurements with a Pauli angle (integer multiple of \(\pi/2\)) into Pauli measurements.This function implements the algorithm presented in Ref. [1] with polynomial complexity on the number of nodes, \(O(N^3)\).
References
[1] Mitosek and Backens, 2024 (arXiv:2410.23439).
Example
>>> import warnings >>> import networkx as nx >>> from graphix.opengraph import OpenGraph >>> from graphix.measurements import Measurement >>> graph = nx.Graph([(0, 1), (1, 2)]) >>> measurements = {0: Measurement.XZ(0.5), 1: Measurement.XZ(0.5)} >>> og = OpenGraph(graph, [0], [2], measurements) >>> with warnings.catch_warnings(): ... warnings.filterwarnings("ignore", message="Open graph with non-inferred Pauli measurements.") ... og.to_pauliflow() Traceback (most recent call last): ... graphix.opengraph.OpenGraphError: The open graph does not have a Pauli flow. >>> og.infer_pauli_measurements().measurements {0: Measurement.X, 1: Measurement.X} >>> str(og.infer_pauli_measurements().to_pauliflow()) 'p(0) = {1}, p(1) = {2}; {0, 1} < {2}'
- to_circuit(pexp_cp: Callable[[PauliExponentialDAG, Circuit], None] | None = None, cm_cp: Callable[[CliffordMap, Circuit], None] | None = None, *, stacklevel: int = 1) Circuit[source]¶
Extract a unitary in the form of a circuit from an open graph resource state.
This method acts as a wrapper around the circuit extraction routine, simplifying its usage. It first attempts to extract the Pauli flow of the open graph, then applies the circuit extraction procedure described in Ref. [1_to_circuit], and finally compiles the resulting circuit using the provided passes. To obtain the open graph’s unitary in the form of a Pauli exponential DAG along with a Clifford transformation, as presented in Ref. [1_to_circuit], one should instead operate directly on the flow object using
PauliFlow.extract_circuit().- Parameters:
pexp_cp (Callable[[PauliExponentialDAG, Circuit], None] | None) – Compilation pass to synthesize a Pauli exponential DAG. If
None(default),graphix.circ_ext.compilation.pexp_ladder_pass()is employed.cm_cp (Callable[[CliffordMap, Circuit], None] | None) – Compilation pass to synthesize a Clifford map. If
None(default),graphix.circ_ext.compilation.cm_berg_pass()is employed. This pass only handles unitaries so far (Clifford maps with the same number of input and ouptut nodes).stacklevel (int, optional) – Stack level to use for warnings. Defaults to 1, meaning that warnings are reported at this function’s call site.
- Returns:
Quantum circuit represented as a set of instructions.
- Return type:
Notes
The open graph instance must be of parametric type
Measurementto allow for a circuit extraction, otherwise it does not contain information about the measurement angles.This wrapper extracts a Pauli flow rather than a gflow, as the former is more general while the underlying extraction algorithms have the same computational complexity in both cases. The resulting unitary is identical whether it is obtained from a
GFlowor from aPauliFlowwith inferred Pauli measurements. However, compilation passes that simultaneously diagonalize Pauli exponentials within the same layer of the Pauli exponential DAG may benefit from flows of lower depth, which is often the case for Pauli flow. The passgraphix.circ_ext.compilation.pexp_ladder_pass()does not take into account the flow’s depth.
References
Examples
>>> import warnings >>> import networkx as nx >>> from graphix.opengraph import OpenGraph >>> from graphix.measurements import Measurement >>> og = OpenGraph( ... graph=nx.Graph([(0, 1), (1, 2), (3, 4), (4, 5), (6, 7), (7, 8), (1, 3), (4, 6)]), ... input_nodes=(0, 3, 6), ... output_nodes=(2, 5, 8), ... measurements=dict.fromkeys((0, 1, 3, 4, 6, 7), Measurement.XY(angle=0)), ... ) >>> with warnings.catch_warnings(): ... warnings.filterwarnings("ignore", message="Open graph with non-inferred Pauli measurements.") ... og.to_circuit() Circuit(width=3, instr=[H(2), H(1), CNOT(2, 1), H(1), H(1), H(0), CNOT(2, 0), CNOT(1, 0), H(2), H(1), H(0)]) >>> # The default compilation passes do not exploit the lower depth of the Pauli flow >>> # compared the gflow. >>> og.infer_pauli_measurements().to_circuit() Circuit(width=3, instr=[H(2), H(1), CNOT(2, 1), H(1), H(1), H(0), CNOT(2, 0), CNOT(1, 0), H(2), H(1), H(0)])
- compose(other: OpenGraph[_AM_co], mapping: Mapping[int, int]) tuple[OpenGraph[_AM_co], dict[int, int]][source]¶
Compose two open graphs by merging subsets of nodes from
selfandother, and relabeling the nodes ofotherthat were not merged.- Parameters:
- Returns:
og (OpenGraph[_AM_co]) – Composed open graph.
mapping_complete (dict[int, int]) – Complete relabelling of the nodes in
other, withkeysandvaluesdenoting the old and new node label, respectively.
Notes
Let’s denote \(\{G(V_1, E_1), I_1, O_1\}\) the open graph
self, \(\{G(V_2, E_2), I_2, O_2\}\) the open graphother, \(\{G(V, E), I, O\}\) the resulting open graphogand{v:u}an element ofmapping.We define \(V, U\) the set of nodes in
mapping.keys()andmapping.values(), and \(M = U \cap V_1\) the set of merged nodes.The open graph composition requires that
\(V \subseteq V_2\).
If both
vanduare measured, the corresponding measurements must have the same plane and angle.
The returned open graph follows this convention:
\(I = (I_1 \cup I_2) \setminus M \cup (I_1 \cap I_2 \cap M)\),
\(O = (O_1 \cup O_2) \setminus M \cup (O_1 \cap O_2 \cap M)\),
If only one node of the pair
{v:u}is measured, this measure is assigned to \(u \in V\) in the resulting open graph.Input (and, respectively, output) nodes in the returned open graph have the order of the open graph
selffollowed by those of the open graphother. Merged nodes are removed, except when they are input (or output) nodes in both open graphs, in which case, they appear in the order they originally had in the graphself.Clifford operations on output nodes:
if two output nodes \(o_1\) and \(o_2\) are merged, then \(C(o_1) = C_2(o_2) \circ C_1(o_1)\);
if an output node \(o_1\) is merged with a measured node \(m_2\), then \(M(o_1) = M_2(m_2) \circ C_1(o_1)\);
if a measured node \(m_1\) is merged with an output node \(o_2\), then \(M(m_1) = M_1(m_1) \circ C_2(o_2)\).
- with_parameter(variable: Parameter, substitute: ExpressionOrSupportsFloat) OpenGraph[_M][source]¶
Substitute a parameter with a value or expression in all measurement angles.
Creates a new open graph where every measurement angle containing the specified variable is updated using the provided substitution. The original open graph instance remains unmodified.
- Parameters:
variable (Parameter) – The symbolic expression to be replaced within the measurement angles.
substitute (ExpressionOrSupportsFloat) – The value or symbolic expression to substitute in place of
variable.
- Returns:
A new instance of OpenGraph with the updated measurement parameters.
- Return type:
Notes
Substitution relies on object identity. You must provide the exact parameter object instance currently stored in the measurements. Passing a new object with the same name will not trigger a substitution if it is not the same instance in memory.
Examples
>>> import networkx as nx >>> from graphix.fundamentals import Plane >>> from graphix.measurements import Measurement >>> from graphix.opengraph import OpenGraph >>> from graphix.parameter import Placeholder >>> # Initialize placeholders and open graph >>> parametric_angles = [Placeholder(f"alpha{i}") for i in range(2)] >>> measurements = {node: Measurement.XY(angle) for node, angle in enumerate(parametric_angles)} >>> og = OpenGraph( ... graph=nx.Graph([(0, 1), (1, 2)]), ... input_nodes=[0], ... output_nodes=[2], ... measurements=measurements, ... ) >>> # To perform substitution, use the actual object in memory >>> new_og = og.with_parameter(parametric_angles[0], 0.3) >>> # Note: og.with_parameter(Placeholder("alpha0"), 0.3) would not trigger any substitution.
- with_parameters(assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) OpenGraph[_M][source]¶
Perform parallel substitution of multiple parameters in measurement angles.
Creates a new open graph where occurrences of parameters defined in the assignment mapping are replaced by their corresponding values. The original open graph instance remains unmodified.
- Parameters:
assignment (Mapping[Parameter, ExpressionOrSupportsFloat]) – A dictionary-like mapping where keys are the
Parameterobjects to be replaced and values are the new expressions or numerical values.- Returns:
A new instance of OpenGraph with the updated measurement angles.
- Return type:
Notes
The notes provided in
self.subs()apply here as well.Examples
>>> import networkx as nx >>> from graphix.fundamentals import Plane >>> from graphix.measurements import Measurement >>> from graphix.opengraph import OpenGraph >>> from graphix.parameter import Placeholder >>> # Initialize placeholders >>> alpha = Placeholder("alpha") >>> beta = Placeholder("beta") >>> measurements = {0: Measurement.XY(alpha), 1: Measurement.XY(beta)} >>> og = OpenGraph(nx.Graph([(0, 1)]), [0], [], measurements) >>> # Substitute multiple parameters at once >>> subs_map = {alpha: 0.5, beta: 1.2} >>> new_og = og.with_parameters(subs_map)