Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- #545: Added an amplitude damping noise model. Introduces `amplitude_damping_channel` / `two_qubit_amplitude_damping_channel`, the `AmplitudeDampingNoise` / `TwoQubitAmplitudeDampingNoise` noise elements, and `AmplitudeDampingNoiseModel`.

- #568: Added the following methods to `Pattern`. By default, they modify the pattern in place, with an optional `copy` parameter. If `copy=True`, a modified copy is returned instead.
- `apply`, an in-place variant of `map`,
- `blochify`, an in-place variant of `to_bloch`,
- `substitute`, an in-place variant of `subs`,
- `replace_parameters`, an-in place variant of `xreplace`.

### Fixed

- #454, #481: Ensure `Pattern.minimize_space` only reduces max-space and does not increase it.
Expand Down Expand Up @@ -100,6 +106,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- #512: Method `Circuit.simulate_statevector` accepts a `backend: DenseStateBackend[_DenseStateT] | Literal["statevector", "densitymatrix"]` parameter.

- #568: Method `Pattern.infer_pauli_measurements` now operates in place by default, with an optional `copy` parameter. If `copy=True`, a modified copy is returned instead.

## [0.3.5] - 2026-03-26

### Added
Expand Down
2 changes: 1 addition & 1 deletion examples/deutsch_jozsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
# %%
# Now we preprocess all Pauli measurements, which requires that we move inputs to N commands

pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements()
print(
pattern.to_ascii(
Expand Down
2 changes: 1 addition & 1 deletion examples/mbqc_vqe.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ def build_mbqc_pattern(self, params: Iterable[ParameterizedAngle]) -> Pattern:
pattern = circuit.transpile().pattern
pattern.standardize()
pattern.shift_signals()
pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements()
pattern.minimize_space()
return pattern
Expand Down
2 changes: 1 addition & 1 deletion examples/qaoa.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
# %%
# perform Pauli measurements and plot the new (minimal) graph to perform the same quantum computation

pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements()
pattern.draw(flow_from_pattern=False)

Expand Down
2 changes: 1 addition & 1 deletion examples/qft_with_tn.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ def qft(circuit: Circuit, n: int) -> None:
# Using graph rewriting rules, we can classically preprocess Pauli measurements.
# We are currently improving the speed of this process by using rust-based graph manipulation backend.
pattern.remove_input_nodes()
pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements(standardize=True)


Expand Down
4 changes: 2 additions & 2 deletions examples/tn_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ def ansatz(
# %%
# Optimizing by removing Pauli measurements in the pattern.
pattern.remove_input_nodes()
pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements(standardize=True)

# %%
Expand Down Expand Up @@ -204,7 +204,7 @@ def cost(
pattern.standardize()
pattern.shift_signals()
pattern.remove_input_nodes()
pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements(standardize=True)
mbqc_tn = pattern.simulate_pattern(backend="tensornetwork", graph_prep="parallel")
exp_val: float = 0
Expand Down
2 changes: 1 addition & 1 deletion examples/visualization.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@

# %%
# next, show the gflow:
pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements()
pattern.draw(flow_from_pattern=False, measurement_labels=True)

Expand Down
2 changes: 1 addition & 1 deletion graphix/opengraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,7 +758,7 @@ def subs(self: OpenGraph[_M], variable: Parameter, substitute: ExpressionOrSuppo
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`.
The value or symbolic expression to substitute in place of ``variable``.

Returns
-------
Expand Down
186 changes: 166 additions & 20 deletions graphix/pattern.py
Original file line number Diff line number Diff line change
Expand Up @@ -1622,12 +1622,88 @@ def is_parameterized(self) -> bool:
)

def subs(self, variable: Parameter, substitute: ExpressionOrSupportsFloat) -> Pattern:
"""Return a copy of the pattern where all occurrences of the given variable in measurement angles are substituted by the given value."""
return self.map(lambda m: m.subs(variable, substitute))
"""Return a copy of the pattern where all occurrences of the given variable in measurement angles are substituted by the given value.

Equivalent to ``self.substitute(variable, substitute, copy=True)``

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
-------
Pattern
The pattern with the updated measurement parameters.
"""
return self.substitute(variable, substitute, copy=True)

def substitute(self, variable: Parameter, substitute: ExpressionOrSupportsFloat, *, copy: bool = False) -> Pattern:
"""Substitute all occurrences of the given variable in measurement angles by the given value.

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``.

copy : bool, optional
If ``True``, the current pattern remains unchanged and a
new pattern is returned. The default is ``False``, meaning
that changes are performed in place.

Returns
-------
Pattern
The pattern with the updated measurement parameters.
Equal to ``self`` if ``copy`` is ``False``.
"""
return self.apply(lambda m: m.subs(variable, substitute), copy=copy)

def xreplace(self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat]) -> Pattern:
"""Return a copy of the pattern where all occurrences of the given keys in measurement angles are substituted by the given values in parallel."""
return self.map(lambda m: m.xreplace(assignment))
"""Return a copy of the pattern where all occurrences of the given keys in measurement angles are substituted by the given values in parallel.

Equivalent to ``self.replace_parameters(assignment, copy=True)``.

Parameters
----------
assignment : Mapping[Parameter, ExpressionOrSupportsFloat]
A dictionary-like mapping where keys are the `Parameter` objects to be replaced and values are the new expressions or numerical values.

Returns
-------
Pattern
The pattern with the updated measurement parameters.
"""
return self.replace_parameters(assignment, copy=True)

def replace_parameters(
self, assignment: Mapping[Parameter, ExpressionOrSupportsFloat], *, copy: bool = False
) -> Pattern:
"""Substitute all occurrences of the given keys in measurement angles by the given values in parallel.

Parameters
----------
assignment : Mapping[Parameter, ExpressionOrSupportsFloat]
A dictionary-like mapping where keys are the `Parameter` objects to be replaced and values are the new expressions or numerical values.

copy : bool, optional
If ``True``, the current pattern remains unchanged and a
new pattern is returned. The default is ``False``, meaning
that changes are performed in place.

Returns
-------
Pattern
The pattern with the updated measurement parameters.
Equal to ``self`` if ``copy`` is ``False``.
"""
return self.apply(lambda m: m.xreplace(assignment), copy=copy)

def copy(self) -> Pattern:
"""Return a copy of the pattern."""
Expand Down Expand Up @@ -1700,9 +1776,48 @@ def check_measured(cmd: CommandType, node: int) -> None:
case CommandKind.C:
check_active(cmd, cmd.node)

def apply(self, f: Callable[[Measurement], Measurement], *, copy: bool = False) -> Pattern:
"""Apply the function ``f` to each measurement.

Parameters
----------
f: Callable[[Measurement], Measurement]
Function applied to each measurement.
copy : bool, optional
If ``True``, the current pattern remains unchanged and a
new pattern is returned. The default is ``False``, meaning
that changes are performed in place.

Returns
-------
Pattern
The resulting pattern.
If ``copy`` is ``False``, the result is ``self``.

Example
-------
>>> from graphix import Pattern, command
>>> from graphix.measurements import BlochMeasurement, Measurement
>>> pattern = Pattern(input_nodes=[0], cmds=[command.M(0, Measurement.XZ(0.25))])
>>> pattern.apply(lambda m: BlochMeasurement(m.angle + 1, m.plane))
Pattern(input_nodes=[0], cmds=[M(0, Measurement.XZ(1.25))])
>>> pattern
Pattern(input_nodes=[0], cmds=[M(0, Measurement.XZ(1.25))])
"""
new_seq = [cmd.map(f) if cmd.kind == CommandKind.M else cmd for cmd in self]

if copy:
new_pattern = Pattern(input_nodes=self.input_nodes, cmds=new_seq)
new_pattern.reorder_output_nodes(self.output_nodes)
return new_pattern
self.__seq = new_seq
return self

def map(self, f: Callable[[Measurement], Measurement]) -> Pattern:
"""Return a pattern where the function ``f`` has been applied to each measurement.

Equivalent to ``self.apply(f, copy=True)``.

Parameters
----------
f: Callable[[Measurement], Measurement]
Expand All @@ -1720,20 +1835,13 @@ def map(self, f: Callable[[Measurement], Measurement]) -> Pattern:
>>> pattern = Pattern(input_nodes=[0], cmds=[command.M(0, Measurement.XZ(0.25))])
>>> pattern.map(lambda m: BlochMeasurement(m.angle + 1, m.plane))
Pattern(input_nodes=[0], cmds=[M(0, Measurement.XZ(1.25))])
>>> pattern
Pattern(input_nodes=[0], cmds=[M(0, Measurement.XZ(0.25))])
"""
new_pattern = Pattern(input_nodes=self.input_nodes)
return self.apply(f, copy=True)

for cmd in self:
if cmd.kind == CommandKind.M:
new_pattern.add(cmd.map(f))
else:
new_pattern.add(cmd)

new_pattern.reorder_output_nodes(self.output_nodes)
return new_pattern

def infer_pauli_measurements(self, rel_tol: float = 1e-09, abs_tol: float = 0.0) -> Pattern:
"""Return an equivalent pattern in which Bloch measurements close to a Pauli measurement are replaced by Pauli measurements.
def infer_pauli_measurements(self, *, rel_tol: float = 1e-09, abs_tol: float = 0.0, copy: bool = False) -> Pattern:
"""Replace Bloch measurements close to a Pauli measurement by Pauli measurements.

Parameters
----------
Expand All @@ -1743,11 +1851,17 @@ def infer_pauli_measurements(self, rel_tol: float = 1e-09, abs_tol: float = 0.0)
abs_tol : float, optional
Absolute tolerance for comparing angles, passed to :func:`math.isclose`.
Default is ``0.0``.
copy : bool, optional
If ``True``, the current pattern remains unchanged and a
new pattern is returned. The default is ``False``, meaning
that changes are performed in place.

Returns
-------
Pattern
An equivalent pattern in which Bloch measurements close to a Pauli measurement are replaced by Pauli measurements.
A pattern in which Bloch measurements close to a Pauli
measurement are replaced by Pauli measurements. If
``copy`` is ``False``, the result is ``self``.

Example
-------
Expand All @@ -1757,27 +1871,59 @@ def infer_pauli_measurements(self, rel_tol: float = 1e-09, abs_tol: float = 0.0)
>>> pattern.infer_pauli_measurements()
Pattern(input_nodes=[0], cmds=[M(0, Measurement.Y)])
"""
return self.map(lambda m: m.to_pauli_or_bloch(rel_tol, abs_tol))
return self.apply(lambda m: m.to_pauli_or_bloch(rel_tol, abs_tol), copy=copy)

def blochify(self, *, copy: bool = False) -> Pattern:
"""Represent all measurements as Bloch measurements.

Parameters
----------
copy : bool, optional
If ``True``, the current pattern remains unchanged and a
new pattern is returned. The default is ``False``, meaning
that changes are performed in place.

Returns
-------
Pattern
A pattern in which all measurements are Bloch measurements.
If ``copy`` is ``False``, the result is ``self``.

Example
-------
>>> from graphix import Pattern, command
>>> from graphix.measurements import BlochMeasurement, Measurement
>>> pattern = Pattern(input_nodes=[0], cmds=[command.M(0, Measurement.Y)])
>>> pattern.blochify()
Pattern(input_nodes=[0], cmds=[M(0, Measurement.XY(0.5))])
>>> pattern
Pattern(input_nodes=[0], cmds=[M(0, Measurement.XY(0.5))])
"""
return self.apply(lambda m: m.to_bloch(), copy=copy)

def to_bloch(self) -> Pattern:
"""Return an equivalent pattern in which all measurements are represented as Bloch measurements.

Equivalent to ``self.blochify(copy=True)``.

Example
-------
>>> from graphix import Pattern, command
>>> from graphix.measurements import BlochMeasurement, Measurement
>>> pattern = Pattern(input_nodes=[0], cmds=[command.M(0, Measurement.Y)])
>>> pattern.to_bloch()
Pattern(input_nodes=[0], cmds=[M(0, Measurement.XY(0.5))])
>>> pattern
Pattern(input_nodes=[0], cmds=[M(0, Measurement.Y)])
"""
return self.map(lambda m: m.to_bloch())
return self.blochify(copy=True)

def perform_pauli_pushing(
self,
*,
leave_nodes: AbstractSet[Node] | None = None,
copy: bool = False,
standardize: bool = False,
*,
stacklevel: int = 1,
) -> Pattern:
"""Move Pauli measurements before the other measurements.
Expand Down
6 changes: 3 additions & 3 deletions tests/test_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_flow_after_pauli_preprocessing(fx_bg: PCG64, jumps: int) -> None:
pattern = circuit.transpile().pattern
pattern.standardize()
pattern.shift_signals()
pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements()
# We should convert to Bloch measurement the remaining Pauli
# measurements on input nodes.
Expand All @@ -83,7 +83,7 @@ def test_remove_useless_domains(fx_bg: PCG64, jumps: int) -> None:
pattern = circuit.transpile().pattern
pattern.standardize()
pattern.shift_signals()
pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements()
pattern2 = remove_useless_domains(pattern)
pattern2 = StandardizedPattern.from_pattern(pattern2).to_space_optimal_pattern()
Expand Down Expand Up @@ -143,7 +143,7 @@ def test_remove_local_clifford_commands(fx_bg: PCG64, jumps: int) -> None:
depth = 4
circuit = rand_circuit(nqubits, depth, rng)
pattern = circuit.transpile().pattern
pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements()
assert any(cmd.kind == CommandKind.C for cmd in pattern)
new_pattern = pattern.remove_local_clifford_commands(copy=True)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_parameter.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ def test_random_circuit_with_parameters(fx_bg: PCG64, jumps: int, use_xreplace:
pattern = circuit.transpile().pattern
pattern.standardize()
pattern.shift_signals()
pattern = pattern.infer_pauli_measurements()
pattern.infer_pauli_measurements()
pattern.remove_pauli_measurements()
pattern.minimize_space()
assignment: dict[Parameter, float] = {alpha: rng.uniform(high=2), beta: rng.uniform(high=2)}
Expand Down
Loading
Loading