# Project: Qhronology (https://github.com/lgbishop/qhronology)
# Author: lgbishop <lgbishop@protonmail.com>
# Copyright: Lachlan G. Bishop 2025
# License: AGPLv3 (non-commercial use), proprietary (commercial use)
# For more details, see the README in the project repository:
# https://github.com/lgbishop/qhronology,
# or visit the website:
# https://qhronology.org.
"""
Functions and a mixin for performing quantum operations.
"""
# https://peps.python.org/pep-0649/
# https://peps.python.org/pep-0749/
from __future__ import annotations
from typing import Any, Callable
import numpy as np
import sympy as sp
from qhronology.utilities.classification import Forms, arr, expr, mat, matrix_form, num
from qhronology.utilities.helpers import (
cast,
conjugate_transpose,
count_systems,
dtype,
extract_substitutions,
extract_representation,
extract_symbols,
flatten_list,
generate_identity,
generate_zeros,
matrix_multiplication,
recursively_simplify,
symbolize_substitutions,
symbolize_expression,
tensor_product,
to_column,
to_density,
to_matrix,
to_numerical,
)
[docs]
def densify(vector: mat | arr | QuantumObject) -> mat | arr:
"""Convert :python:`vector` to its corresponding matrix form via the outer product.
If :python:`vector` is a square matrix, it is unmodified.
Arguments
---------
vector : mat | arr
The input vector.
Returns
-------
mat | arr
The outer product of :python:`vector` with itself.
"""
vector = extract_representation(vector)
return to_density(vector)
[docs]
def columnify(vector: mat | arr | QuantumObject) -> mat | arr:
"""Convert :python:`vector` to its corresponding column vector form via transposition.
If :python:`vector` is a square matrix, it is unmodified.
Arguments
---------
vector : mat | arr
The input vector.
Returns
-------
mat | arr
The column form of :python:`vector`.
"""
vector = extract_representation(vector)
return to_column(vector)
[docs]
def dagger(matrix: mat | arr | QuantumObject) -> mat | arr:
"""Perform conjugate transposition on :python:`matrix`.
Arguments
---------
matrix : mat | arr
The input matrix.
Returns
-------
mat | arr
The conjugate transpose of :python:`matrix`.
"""
matrix = extract_representation(matrix)
return conjugate_transpose(matrix)
[docs]
def round(matrix: mat | arr | QuantumObject) -> mat | arr:
"""Round the elements of :python:`matrix` to the nearest (real) integer.
Arguments
---------
matrix : mat | arr
The input matrix.
Returns
-------
mat | arr
The rounded version of :python:`matrix`.
"""
matrix = extract_representation(matrix)
matrix_num = True if issubclass(dtype(matrix), num) is True else False
matrix_arr = True if isinstance(matrix, arr) is True else False
matrix = np.rint(np.real(np.array(matrix, dtype=complex))).astype(int)
if matrix_arr is False:
matrix = to_matrix(matrix)
return matrix
[docs]
def simplify(
matrix: mat | arr | QuantumObject, comprehensive: bool | None = None
) -> mat | arr:
"""Simplify :python:`matrix` using a powerful (albeit slow) algorithm.
Arguments
---------
matrix : mat | arr | QuantumObject
The matrix to be simplified.
comprehensive : bool
Whether the simplifying algorithm should use a relatively efficient subset of simplifying operations (:python:`False`), or alternatively use a larger, more powerful (but slower) set (:python:`True`).
Defaults to :python:`False`.
Returns
-------
mat | arr
The simplified version of :python:`matrix`.
Note
----
If :python:`comprehensive` is :python:`True`, the simplification algorithm will likely take *far* longer to execute than if :python:`comprehensive` were :python:`False`.
"""
substitutions = extract_substitutions(matrix)
symbols = extract_symbols(matrix)
matrix = extract_representation(matrix)
matrix = symbolize_expression(matrix, symbols)
substitutions = symbolize_substitutions(substitutions, symbols)
matrix = recursively_simplify(matrix, substitutions, comprehensive=comprehensive)
return matrix
[docs]
def rewrite(matrix: mat | arr | QuantumObject, function: Callable) -> mat | arr:
"""Rewrite the elements of :python:`matrix` using the given mathematical function (:python:`function`).
Useful when used with SymPy's mathematical functions, such as:
- :python:`exp()`
- :python:`log()`
- :python:`sin()`
- :python:`cos()`
- :python:`sqrt()`
Arguments
---------
matrix : mat | arr | QuantumObject
The matrix to be transformed.
function : Callable
A SymPy mathematical function.
Returns
-------
mat | arr
The transformed version of :python:`matrix`.
"""
symbols = extract_symbols(matrix)
matrix = extract_representation(matrix)
matrix = symbolize_expression(matrix, symbols)
if dtype(matrix) is object:
matrix_num = True if issubclass(dtype(matrix), num) is True else False
matrix_arr = True if isinstance(matrix, arr) is True else False
matrix = to_matrix(matrix)
try:
for index, entry in np.ndenumerate(matrix):
entry = entry.rewrite(function)
matrix[index] = entry
except:
raise ValueError(
f"""The specified function (`{function.__name__}()`) cannot be used to rewrite the matrix."""
)
matrix = cast(matrix, numerical=matrix_num, array=matrix_arr)
return matrix
[docs]
def apply(
matrix: mat | arr | QuantumObject,
function: Callable,
arguments: dict[str, Any] | None = None,
) -> mat | arr:
"""Apply a Python function (:python:`function`) element-wise to :python:`matrix`.
Useful when used with SymPy's symbolic-manipulation functions, such as:
- :python:`apart()`
- :python:`cancel()`
- :python:`collect()`
- :python:`expand()`
- :python:`factor()`
- :python:`simplify()`
- :python:`separatevars()`
- :python:`rewrite()` (though the :py:func:`~qhronology.mechanics.operations.rewrite` function should be used instead)
More can be found at:
- `SymPy documentation: Simplification <https://docs.sympy.org/latest/tutorials/intro-tutorial/simplification.html>`_
- `SymPy documentation: Simplify <https://docs.sympy.org/latest/modules/simplify/simplify.html>`_
Arguments
---------
matrix : mat | arr | QuantumObject
The matrix to be transformed.
function : Callable
A Python function.
Its first non-keyword argument must be able to take a mathematical expression or a matrix/array of such types.
arguments : dict[str, str]
A dictionary of keyword arguments (both keys and values as strings) to pass to the :python:`function` call.
Defaults to :python:`{}`.
Returns
-------
mat | arr
The transformed version of :python:`matrix`.
"""
arguments = {} if arguments is None else arguments
symbols = extract_symbols(matrix)
matrix = extract_representation(matrix)
matrix_num = True if issubclass(dtype(matrix), num) is True else False
matrix_arr = True if isinstance(matrix, arr) is True else False
matrix = symbolize_expression(matrix, symbols)
matrix = to_matrix(matrix)
try:
for index, entry in np.ndenumerate(matrix):
matrix[index] = function(entry, **arguments)
except:
try:
for index, entry in np.ndenumerate(matrix):
matrix[index] = function(
to_numerical(entry, numerical=True), **arguments
)
except:
raise ValueError(
f"""Unable to apply the specified function (`{function.__name__}()`) to the matrix."""
)
matrix = cast(matrix, numerical=matrix_num, array=matrix_arr)
return matrix
[docs]
def normalize(
matrix: mat | arr | QuantumObject, norm: num | expr | str | None = None
) -> mat | arr:
"""Normalize :python:`matrix` to the value specified (:python:`norm`).
Arguments
---------
matrix : mat | arr | QuantumObject
The matrix to be normalized.
norm : num | expr | str
The value to which the matrix is normalized.
Defaults to :python:`1`.
Returns
-------
mat | arr
The normalized version of :python:`matrix`.
"""
norm = 1 if norm is None else norm
is_vector = False
try:
is_vector = matrix.is_vector
except:
if matrix_form(matrix) == Forms.VECTOR.value:
is_vector = True
substitutions = extract_substitutions(matrix)
symbols = extract_symbols(matrix)
matrix = extract_representation(matrix)
matrix_num = True if issubclass(dtype(matrix), num) is True else False
matrix_arr = True if isinstance(matrix, arr) is True else False
matrix = symbolize_expression(matrix, symbols)
substitutions = symbolize_substitutions(substitutions, symbols)
trace = densify(matrix).trace()
norm = symbolize_expression(norm, symbols)
trace = symbolize_expression(trace, symbols)
norm = recursively_simplify(norm, substitutions)
trace = recursively_simplify(trace, substitutions)
factor = norm / trace
factor = recursively_simplify(factor, substitutions)
if is_vector is True:
factor = sp.sqrt(factor)
factor = recursively_simplify(factor, substitutions)
matrix = factor * matrix
matrix = cast(matrix, numerical=matrix_num, array=matrix_arr)
return matrix
[docs]
def coefficient(
matrix: mat | arr | QuantumObject, scalar: num | expr | str | None = None
) -> mat | arr:
"""Multiply :python:`matrix` by a scalar value (:python:`scalar`).
Arguments
---------
matrix : mat | arr | QuantumObject
The matrix to be scaled.
scalar : num | expr | str
The value by which the state is multiplied.
Defaults to :python:`1`.
Returns
-------
mat | arr
The scaled version of :python:`matrix`.
"""
scalar = 1 if scalar is None else scalar
substitutions = extract_substitutions(matrix)
symbols = extract_symbols(matrix)
matrix = extract_representation(matrix)
matrix_num = True if issubclass(dtype(matrix), num) is True else False
matrix_arr = True if isinstance(matrix, arr) is True else False
matrix = symbolize_expression(matrix, symbols)
substitutions = symbolize_substitutions(substitutions, symbols)
scalar = symbolize_expression(scalar, symbols)
matrix = scalar * matrix
matrix = cast(matrix, numerical=matrix_num, array=matrix_arr)
return matrix
[docs]
def partial_trace(
matrix: mat | arr | QuantumObject,
targets: list[int] | None = None,
discard: bool | None = None,
dim: int | None = None,
optimize: bool | None = None,
) -> num | expr | mat | arr:
"""Compute and return the partial trace of a matrix.
Arguments
---------
matrix : mat | arr
The matrix on which to perform the partial trace operation.
targets : list[int]
The numerical indices of the subsystem(s) to be partially traced over.
Defaults to :python:`[]`.
discard : bool
Whether the systems corresponding to the indices given in :python:`targets` are to be discarded (:python:`True`) or kept (:python:`False`).
Defaults to :python:`True`.
dim : int
The dimensionality of the matrix.
Must be a non-negative integer.
Defaults to :python:`2`.
optimize : bool
Whether to optimize the implementation's algorithm.
Can greatly increase the computational efficiency at the cost of a larger memory footprint during computation.
Defaults to :python:`True`.
Returns
-------
num | expr | mat | arr
The reduced matrix or scalar trace expression.
"""
targets = [] if targets is None else targets
discard = True if discard is None else discard
dim = 2 if dim is None else dim
optimize = True if optimize is None else optimize
matrix = extract_representation(matrix)
matrix_num = True if issubclass(dtype(matrix), num) is True else False
matrix_arr = True if isinstance(matrix, arr) is True else False
targets = flatten_list(list([targets]))
if len(targets) == 0 and discard is True:
return matrix
matrix = densify(matrix)
# Convert integer dim into the required list form
if isinstance(dim, int):
dim = [dim] * count_systems(matrix, dim)
dim = np.asarray(dim)
num_systems = dim.size
systems = [k for k in range(0, num_systems)]
matrix = np.asarray(matrix)
if discard is True:
keep = [k for k in systems if not k in targets]
else:
keep = [k for k in targets]
num_keep = np.prod(dim[keep]) - 1
i = [k for k in range(num_systems)]
j = [num_systems + k if k in keep else k for k in range(num_systems)]
operator_reduced = matrix.reshape(np.tile(dim, 2))
operator_reduced = np.einsum(operator_reduced, i + j, optimize=optimize)
if isinstance(operator_reduced, num):
return operator_reduced
else:
return cast(
operator_reduced.reshape(num_keep + 1, num_keep + 1),
numerical=matrix_num,
array=matrix_arr,
)
[docs]
def measure(
matrix: mat | arr | QuantumObject,
operators: list[mat | arr | QuantumObject],
targets: list[int],
observable: bool | None = None,
statistics: bool | None = None,
dim: int | None = None,
) -> mat | arr | list[num | expr]:
"""Perform a quantum measurement on one or more systems (indicated in :python:`targets`) of :python:`matrix`.
This function has two main modes of operation:
- When :python:`statistics` is :python:`True`, the (reduced) state (:math:`\\op{\\rho}`) (residing on the systems indicated in :python:`targets`) is measured, and the set of resulting statistics is returned.
This takes the form of an ordered list of values :math:`\\{p_i\\}_i` associated with each given operator, where:
- :math:`p_i = \\trace[\\Kraus_i^\\dagger \\Kraus_i \\op{\\rho}]` (measurement probabilities)
when :python:`observable` is :python:`False`
:inlinelatex:`\\newline` (:python:`operators` is a list of Kraus operators or projectors :math:`\\Kraus_i`)
- :math:`p_i = \\trace[\\Observable_i \\op{\\rho}]` (expectation values)
when :python:`observable` is :python:`True`
:inlinelatex:`\\newline` (:python:`operators` is a list of observables :math:`\\Observable_i`)
- When :python:`statistics` is :python:`False`, the (reduced) state (:math:`\\op{\\rho}`) (residing on the systems indicated in :python:`targets`) is measured and mutated according to its predicted post-measurement form (i.e., the sum of all possible measurement outcomes).
This yields the transformed states:
- When :python:`observable` is :python:`False`:
.. math:: \\op{\\rho}^\\prime = \\sum_i \\Kraus_i \\op{\\rho} \\Kraus_i^\\dagger
- When :python:`observable` is :python:`True`:
.. math:: \\op{\\rho}^\\prime = \\sum_i \\trace[\\Observable_i \\op{\\rho}] \\Observable_i
In the case where :python:`operators` contains only a single item (:math:`\\Kraus`) and the current state (:math:`\\ket{\\psi}`) is a vector form, the transformation of the state is in accordance with the rule
.. math::
\\ket{\\psi^\\prime} = \\frac{\\Kraus \\ket{\\psi}}
{\\sqrt{\\bra{\\psi} \\Kraus^\\dagger \\Kraus \\ket{\\psi}}}
when :python:`observable` is :python:`False`. In all other mutation cases, the post-measurement state is a matrix, even if the pre-measurement state was a vector.
The items in the list :python:`operators` can also be vectors (e.g., :math:`\\ket{\\xi_i}`), in which case each is converted into its corresponding operator matrix representation (e.g., :math:`\\ket{\\xi_i}\\bra{\\xi_i}`) prior to any measurements.
Arguments
---------
matrix : mat | arr | QuantumObject
The matrix to be measured.
operators: list[mat | arr | QuantumObject]
The operator(s) with which to perform the measurement.
These would typically be a (complete) set of Kraus operators forming a POVM,
a (complete) set of (orthogonal) projectors forming a PVM,
or a set of observables constituting a complete basis for the relevant state space.
targets : list[int]
The numerical indices of the subsystem(s) to be measured.
They must be contiguous, and their number must match the number of systems spanned by all given operators.
Indexing begins at :python:`0`.
All other systems are discarded (traced over) in the course of performing the measurement.
observable: bool
Whether to treat the items in :python:`operators` as observables instead of Kraus operators or projectors.
Defaults to :python:`False`.
statistics: bool
Whether to return a list of probabilities (:python:`True`) or transform :python:`matrix` into a post-measurement probabilistic sum of all outcomes (:python:`False`).
Defaults to :python:`False`.
dim : int
The dimensionality of :python:`matrix` and the item(s) of :python:`operators`.
Must be a non-negative integer.
Defaults to :python:`2`.
Returns
-------
mat | arr
The post-measurement :python:`matrix`.
Returned if :python:`statistics` is :python:`False`.
num | expr | list[num | expr]
A list of probabilities corresponding to each operator given in :python:`operators`.
Returned if :python:`statistics` is :python:`True`.
Note
----
This method does not verify the validity of supplied POVMs or the completeness of sets of observables, nor does it renormalize the post-measurement state.
"""
observable = False if observable is None else observable
statistics = False if statistics is None else statistics
dim = 2 if dim is None else dim
is_vector = False
try:
is_vector = matrix.is_vector
except:
if matrix_form(matrix) == Forms.VECTOR.value:
is_vector = True
substitutions = extract_substitutions(matrix)
symbols = extract_symbols(matrix)
matrix = extract_representation(matrix)
matrix_num = True if issubclass(dtype(matrix), num) is True else False
matrix_arr = True if isinstance(matrix, arr) is True else False
matrix = symbolize_expression(matrix, symbols)
substitutions = symbolize_substitutions(substitutions, symbols)
operators_initial = operators
operators = flatten_list([operators])
targets = flatten_list([targets])
matrix = partial_trace(matrix=matrix, targets=targets, discard=False, dim=dim)
operator_matrices = []
for operator in operators:
operator_matrices.append(
cast(
extract_representation(operator), numerical=matrix_num, array=matrix_arr
)
)
if statistics is False:
matrix_post_measurement = generate_zeros(
dim ** len(targets), numerical=matrix_num, array=matrix_arr
)
if observable is False:
if len(operator_matrices) == 1 and is_vector is True:
matrix_post_measurement = matrix_multiplication(
operator_matrices[0], matrix
)
normalization = 1 / sp.sqrt(densify(matrix_post_measurement).trace())
normalization = symbolize_expression(normalization, symbols)
normalization = recursively_simplify(normalization, substitutions)
matrix_post_measurement = normalization * matrix_post_measurement
else:
for operator in operator_matrices:
matrix_post_measurement = (
matrix_post_measurement
+ matrix_multiplication(
densify(operator),
densify(matrix),
conjugate_transpose(densify(operator)),
)
)
else:
for operator in operator_matrices:
probability = matrix_multiplication(
densify(operator), densify(matrix)
).trace()
probability = symbolize_expression(probability, symbols)
probability = recursively_simplify(probability, substitutions)
matrix_post_measurement = (
matrix_post_measurement + probability * densify(operator)
)
return cast(matrix_post_measurement, numerical=matrix_num, array=matrix_arr)
else:
if observable is False:
probabilities = [
matrix_multiplication(
densify(operator),
densify(matrix),
conjugate_transpose(densify(operator)),
).trace()
for operator in operator_matrices
]
else:
probabilities = [
matrix_multiplication(densify(operator), densify(matrix)).trace()
for operator in operator_matrices
]
for n, probability in enumerate(probabilities):
probability = symbolize_expression(probability, symbols)
probability = recursively_simplify(probability, substitutions)
probabilities[n] = probability
if isinstance(operators_initial, list) is False:
probabilities = probabilities[0]
return probabilities
[docs]
def postselect(
matrix: mat | arr | QuantumObject,
postselections: list[tuple[mat | arr | QuantumObject, list[int]]],
dim: int | None = None,
) -> mat | arr | list[num | expr]:
"""Perform postselection on :python:`matrix` against the operator(s) specified in :python:`postselections`.
The postselections can be given in either vector or matrix form.
For the former, the transformation of the vector :math:`\\ket{\\Psi}` (residing in some composite Hilbert space :math:`\\SpaceHilbert`) follows the standard rule
.. math:: \\ket{\\Psi^\\prime} = \\braket{\\phi}{\\Psi}
where :math:`\\ket{\\phi} \\in \\tilde{\\SpaceHilbert}` is the postselection vector (which resides in a proper subsystem :math:`\\tilde{\\SpaceHilbert}` of :math:`\\SpaceHilbert`).
In the case of a matrix form :math:`\\op{\\omega}` on :math:`\\tilde{\\SpaceHilbert}`, the notion of postselection of a density matrix :math:`\\op{\\rho}` on :math:`\\SpaceHilbert` naturally generalizes to
.. math:: \\op{\\rho}^\\prime = \\trace_{\\tilde{\\SpaceHilbert}}[\\op{\\omega} \\op{\\rho}].
If multiple postselections are supplied, :python:`matrix` will be successively postselected in the order in which they are given.
If a vector :python:`matrix` is postselected against a matrix form, it will automatically be transformed into its matrix form via the outer product as necessary.
Arguments
---------
matrix : mat | arr | QuantumObject
The matrix to be postselected.
postselections: list[tuple[mat | arr | QuantumObject, list[int]]]
A list of 2-tuples of vectors or matrix operators paired with the indices of their postselection target systems.
The indices of each postselection must be contiguous.
dim : int
The dimensionality of :python:`matrix` and the item(s) of :python:`postselections`.
Must be a non-negative integer.
Defaults to :python:`2`.
Returns
-------
mat | arr | list[num | expr]
The postselected form of :python:`matrix`.
"""
dim = 2 if dim is None else dim
matrix = extract_representation(matrix)
matrix_num = True if issubclass(dtype(matrix), num) is True else False
matrix_arr = True if isinstance(matrix, arr) is True else False
num_systems = count_systems(matrix, dim)
systems = [k for k in range(num_systems)]
is_vector = False
try:
is_vector = matrix.is_vector
except:
if matrix_form(matrix) == Forms.VECTOR.value:
is_vector = True
are_vector = [False for n in postselections]
for n, postselection in enumerate(postselections):
try:
are_vector[n] = postselection[0].is_vector
except:
if matrix_form(postselection[0]) == Forms.VECTOR.value:
are_vector[n] = True
postselection_is_vector = all(boolean is True for boolean in are_vector)
matrices = []
targets = []
for postselection in postselections:
operator = extract_representation(postselection[0])
if matrix_form(operator) == Forms.VECTOR.value:
operator = columnify(operator)
operator = cast(operator, numerical=matrix_num, array=matrix_arr)
matrices.append(operator)
num_systems = count_systems(operator, dim)
if len(postselection[1]) != num_systems:
raise ValueError(
"""Mismatch between the postselection operator's calculated size and the number of its specified targets."""
)
targets.append(sorted(postselection[1]))
operators = []
identity = generate_identity(dim, numerical=matrix_num, array=matrix_arr)
for system in systems:
if system not in flatten_list(targets):
operators.append(identity)
else:
min_targets = [min(group) for group in targets]
if system in min_targets:
operators.append(matrices[min_targets.index(system)])
if is_vector is True and postselection_is_vector is True:
operators_combined = tensor_product(*operators)
matrix = matrix_multiplication(conjugate_transpose(operators_combined), matrix)
else:
for i, operator in enumerate(operators):
operators[i] = densify(operator)
operators_combined = tensor_product(*operators)
matrix = matrix_multiplication(densify(operators_combined), densify(matrix))
matrix = partial_trace(matrix=matrix, targets=flatten_list(targets), dim=dim)
return cast(matrix, numerical=matrix_num, array=matrix_arr)
[docs]
class OperationsMixin:
"""A mixin for endowing classes with the ability to have their :python:`matrix` property mutated by various quantum operations.
Note
----
The :py:class:`~qhronology.mechanics.operations.OperationsMixin` mixin is used exclusively by the :py:class:`~qhronology.quantum.states.QuantumState` class---please see :numref:`sec:docs_states_operations` :ref:`sec:docs_states_operations` for documentation on its methods.
"""
def densify(self):
"""Convert the state to its equivalent (density) matrix representation.
States that are already in density matrix form are unmodified.
"""
self.current = densify(self)
def dagger(self):
"""Perform conjugate transposition on the state."""
self.current = dagger(self)
def round(self):
"""Round the state's elements to the nearest (real) integer."""
self.current = round(self)
def simplify(self, comprehensive: bool | None = None):
"""Apply a forced simplification to the state using the values of its :python:`symbols` and :python:`substitutions` properties.
Useful if intermediate simplification is required during a sequence of mutating operations in order to process the state into a more desirable form.
Arguments
---------
comprehensive : bool
Whether the simplifying algorithm should use a relatively efficient subset of simplifying operations (:python:`False`), or alternatively use a larger, more powerful (but slower) set (:python:`True`).
Defaults to :python:`False`.
Note
----
If :python:`comprehensive` is :python:`True`, the simplification algorithm will likely take *far* longer to execute than if :python:`comprehensive` were :python:`False`.
"""
self.current = simplify(self, comprehensive=comprehensive)
def rewrite(self, function: Callable):
"""Rewrite the elements of the state using the given mathematical function (:python:`function`).
Useful when used with SymPy's mathematical functions, such as:
- :python:`exp()`
- :python:`log()`
- :python:`sin()`
- :python:`cos()`
- :python:`sqrt()`
Arguments
---------
function : Callable
A SymPy mathematical function.
"""
self.current = rewrite(self, function=function)
def apply(self, function: Callable, arguments: dict[str, Any] | None = None):
"""Apply a Python function (:python:`function`) to the state.
Useful when used with SymPy's symbolic-manipulation functions, such as:
- :python:`simplify()`
- :python:`expand()`
- :python:`factor()`
- :python:`collect()`
- :python:`cancel()`
- :python:`apart()`
- :python:`separatevars()`
- :python:`rewrite()` (though the :py:meth:`~qhronology.quantum.states.QuantumState.rewrite` method should be used instead)
More can be found at:
- `SymPy documentation: Simplification <https://docs.sympy.org/latest/tutorials/intro-tutorial/simplification.html>`_
- `SymPy documentation: Simplify <https://docs.sympy.org/latest/modules/simplify/simplify.html>`_
Arguments
---------
function : Callable
A Python function.
Its first non-keyword argument must be able to take a mathematical expression or a matrix/array of such types.
arguments : dict[str, str]
A dictionary of keyword arguments (with the keywords as strings) to pass to the :python:`function` call.
Defaults to :python:`{}`.
"""
self.current = apply(self, function=function, arguments=arguments)
def normalize(self, norm: num | expr | str | None = None):
"""Perform a forced (re)normalization on the state to the value specified (:python:`norm`).
Useful when applied to a quantum state both before and after mutating operations, prior to any simplification (such as renormalization) performed on its processed output (obtained via the :python:`state()` method).
Arguments
---------
norm : num | expr | str
The value to which the state is normalized.
Defaults to :python:`1`.
"""
norm = 1 if norm is None else norm
self.current = normalize(self, norm=norm)
def coefficient(self, scalar: num | expr | str | None = None):
"""Multiply the state by a scalar value (:python:`scalar`).
Can be useful to manually (re)normalize states, or introduce a phase factor.
Arguments
---------
scalar : num | expr | str
The value by which the state is multiplied.
Defaults to :python:`1`.
"""
scalar = 1 if scalar is None else scalar
self.current = coefficient(self, scalar=scalar)
def partial_trace(
self,
targets: list[int] | None = None,
discard: bool | None = None,
optimize: bool | None = None,
):
"""Perform a partial trace operation on the state.
Arguments
---------
targets : list[int]
The numerical indices of the subsystem(s) to be partially traced over.
Indexing begins at :python:`0`.
Defaults to :python:`[]`.
discard : bool
Whether the systems corresponding to the indices given in :python:`targets` are to be discarded (:python:`True`) or kept (:python:`False`).
Defaults to :python:`True`.
optimize : bool
Whether to optimize the partial trace implementation's algorithm.
Can greatly increase the computational efficiency at the cost of a larger memory footprint during computation.
Defaults to :python:`True`.
"""
self.current = partial_trace(
matrix=self,
targets=targets,
discard=discard,
dim=self.dim,
optimize=optimize,
)
def measure(
self,
operators: list[mat | arr | QuantumObject],
targets: list[int] | None = None,
observable: bool | None = None,
statistics: bool | None = None,
) -> None | list[num | expr]:
"""Perform a quantum measurement on one or more systems (indicated in :python:`targets`) of the state.
This method has two main modes of operation:
- When :python:`statistics` is :python:`True`, the (reduced) state (:math:`\\op{\\rho}`) (residing on the systems indicated in :python:`targets`) is measured, and the set of resulting statistics is returned.
This takes the form of an ordered list of values :math:`\\{p_i\\}_i` associated with each given operator, where:
- :math:`p_i = \\trace[\\Kraus_i^\\dagger \\Kraus_i \\op{\\rho}]` (measurement probabilities)
when :python:`observable` is :python:`False`
:inlinelatex:`\\newline` (:python:`operators` is a list of Kraus operators or projectors :math:`\\Kraus_i`)
- :math:`p_i = \\trace[\\Observable_i \\op{\\rho}]` (expectation values)
when :python:`observable` is :python:`True`
:inlinelatex:`\\newline` (:python:`operators` is a list of observables :math:`\\Observable_i`)
- When :python:`statistics` is :python:`False`, the (reduced) state (:math:`\\op{\\rho}`) (residing on the systems indicated in :python:`targets`) is measured and mutated according to its predicted post-measurement form (i.e., the sum of all possible measurement outcomes).
This yields the transformed states:
- When :python:`observable` is :python:`False`:
.. math:: \\op{\\rho}^\\prime = \\sum_i \\Kraus_i \\op{\\rho} \\Kraus_i^\\dagger
- When :python:`observable` is :python:`True`:
.. math:: \\op{\\rho}^\\prime = \\sum_i \\trace[\\Observable_i \\op{\\rho}]\\Observable_i
In the case where :python:`operators` contains only a single item (:math:`\\Kraus`) and the current state (:math:`\\ket{\\psi}`) is a vector form, the transformation of the state is in accordance with the rule
.. math::
\\ket{\\psi^\\prime} = \\frac{\\Kraus \\ket{\\psi}}
{\\sqrt{\\bra{\\psi} \\Kraus^\\dagger \\Kraus \\ket{\\psi}}}
when :python:`observable` is :python:`False`. In all other mutation cases, the post-measurement state is a matrix, even if the pre-measurement state was a vector.
The items in the list :python:`operators` can also be vectors (e.g., :math:`\\ket{\\xi_i}`), in which case each is converted into its corresponding operator matrix representation (e.g., :math:`\\ket{\\xi_i}\\bra{\\xi_i}`) prior to any measurements.
Arguments
---------
operators: list[mat | arr | QuantumObject]
The operator(s) with which to perform the measurement.
These would typically be a (complete) set of Kraus operators forming a POVM,
a (complete) set of (orthogonal) projectors forming a PVM,
or a set of observables constituting a complete basis for the relevant state space.
targets : list[int]
The numerical indices of the subsystem(s) to be measured.
They must be contiguous, and their number must match the number of systems spanned by all given operators.
Indexing begins at :python:`0`.
All other systems are discarded (traced over) in the course of performing the measurement.
Defaults to the value of :python:`self.systems`.
observable: bool
Whether to treat the items in :python:`operators` as observables instead of Kraus operators or projectors.
Defaults to :python:`False`.
statistics: bool
Whether to return a list of probabilities (:python:`True`) or mutate the state into a post-measurement probabilistic sum of all outcomes (:python:`False`).
Defaults to :python:`False`.
Returns
-------
None
Returned if :python:`statistics` is :python:`False`.
num | expr | list[num | expr]
A list of probabilities corresponding to each operator given in :python:`operators`.
Returned if :python:`statistics` is :python:`True`.
Note
----
This method does not verify the validity of supplied POVMs or the completeness of sets of observables, nor does it renormalize the post-measurement state.
"""
targets = self.systems if targets is None else targets
observable = False if observable is None else observable
statistics = False if statistics is None else statistics
if statistics is False:
self.current = measure(
self,
operators=operators,
targets=targets,
observable=observable,
statistics=False,
dim=self.dim,
)
else:
return measure(
self,
operators=operators,
targets=targets,
observable=observable,
statistics=True,
dim=self.dim,
)
def postselect(self, postselections: list[tuple[mat | arr | QuantumObject, list[int]]]):
"""Perform postselection on the state against the operators(s) specified in :python:`postselections`.
The postselections can be given in either vector or matrix form.
For the former, the transformation of the vector :math:`\\ket{\\Psi}` (residing in some composite Hilbert space :math:`\\SpaceHilbert`) follows the standard rule
.. math:: \\ket{\\Psi^\\prime} = \\braket{\\phi}{\\Psi}
where :math:`\\ket{\\phi} \\in \\tilde{\\SpaceHilbert}` is the postselection vector (which resides in a proper subsystem :math:`\\tilde{\\SpaceHilbert}` of :math:`\\SpaceHilbert`).
In the case of a matrix form :math:`\\op{\\omega}` on :math:`\\tilde{\\SpaceHilbert}`, the notion of postselection of a density matrix :math:`\\op{\\rho}` on :math:`\\SpaceHilbert` naturally generalizes to
.. math:: \\op{\\rho}^\\prime = \\trace_{\\tilde{\\SpaceHilbert}}[\\op{\\omega} \\op{\\rho}].
If multiple postselections are supplied, the state will be successively postselected in the order in which they are specified.
If a vector state is postselected against a matrix form, it will automatically be transformed into its matrix form as necessary.
Arguments
---------
postselections: list[tuple[mat | arr | QuantumObject, list[int]]]
A list of 2-tuples of vectors or matrix operators paired with the indices of their postselection target systems.
The indices of each postselection must be contiguous.
Note
----
Any classes given in :python:`postselections` that are derived from the :py:class:`~qhronology.utilities.objects.QuantumObject` base class (such as :py:class:`~qhronology.quantum.states.QuantumState` and :py:class:`~qhronology.quantum.gates.QuantumGate`) will have their :python:`symbols` and :python:`substitutions` properties merged into the current :py:class:`~qhronology.quantum.states.QuantumState` instance.
"""
# Add the postselection(s) symbols and substitutions to the current instance.
for postselection in postselections:
self.substitutions += extract_substitutions(postselection[0])
symbols = extract_symbols(postselection[0])
for symbol in symbols.keys():
if symbol in self.symbols.keys():
self.symbols[symbol] |= symbols[symbol]
else:
self.symbols |= {symbol: symbols[symbol]}
self.current = postselect(self, postselections=postselections, dim=self.dim)