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

"""
A class for the creation of quantum circuits containing closed timelike curves.
Classes and functions implementing quantum prescriptions of time travel.
"""

# https://peps.python.org/pep-0649/
# https://peps.python.org/pep-0749/
from __future__ import annotations

import copy

import numpy as np
import sympy as sp
from sympy.physics.quantum import TensorProduct
from sympy.physics.quantum.dagger import Dagger

from qhronology.utilities.classification import num, sym, mat, Forms, Kinds, matrix_form
from qhronology.utilities.helpers import (
    adjust_targets,
    count_systems,
    count_dims,
    extract_matrix,
    extract_conditions,
    flatten_list,
    assemble_composition,
    recursively_simplify,
)

from qhronology.quantum.states import QuantumState
from qhronology.quantum.gates import QuantumGate
from qhronology.quantum.circuits import QuantumCircuit

from qhronology.mechanics.operations import densify, columnify, partial_trace
from qhronology.mechanics.quantities import trace


class QuantumCTC(QuantumCircuit):
    """A class for creating quantum circuit models of quantum interactions near closed timelike curves and storing their metadata.

    This is built upon the :py:class:`~qhronology.quantum.circuits.QuantumCircuit` class, and so inherits all of its attributes, properties, and methods.

    Instances provide complete descriptions of quantum circuits involving antichronological time travel. The class however does not possess any ability to compute the output state (e.g., resolve temporal paradoxes) of the circuit; that functionality is associated with the specific prescriptions of quantum time travel, which are implemented as subclasses.

    Arguments
    ---------
    *args
        Variable length argument list, passed directly to the constructor :python:`__init__`  of the superclass :py:class:`~qhronology.quantum.circuits.QuantumCircuit`.
    circuit : QuantumCircuit
        An instance of the :py:class:`~qhronology.quantum.circuits.QuantumCircuit` class.
        The values of its attributes override any other values specified in :python:`*args` and :python:`**kwargs`.
        Defaults to :python:`None`.
    systems_respecting : int | list[int]
        The numerical indices of the chronology-respecting (CR) subsystems.
        Defaults to :python:`[]`.
    systems_violating : int | list[int]
        The numerical indices of the chronology-violating (CV) subsystems.
        Defaults to :python:`[]`.
    **kwargs
        Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.circuits.QuantumCircuit`.

    Note
    ----
    The lists of indices specified in :python:`systems_respecting` and :python:`systems_violating` must both be contiguous.
    Additionally, the circuit's inputs (:python:`inputs`) are treated as one contiguous total state, with the indices of its subsystems exactly matching those specified in :python:`systems_respecting`.

    Note
    ----
    It is best practice to specify only one of either :python:`systems_violating` or :python:`systems_violating`, never both.
    The properties associated with both of these constructor arguments automatically ensure that they are always complementary (with respect to the entire system space), and so only one needs to be specified.

    Note
    ----
    The :python:`circuit` argument can be used to merge the value of every attribute from a pre-existing :py:class:`~qhronology.quantum.circuits.QuantumCircuit` instance into the :py:class:`~qhronology.quantum.prescriptions.QuantumCTC` instance. Any such mergers override the values of the attributes associated with the other arguments specified in the constructor. It is best practice to specify either of:

    - only :python:`circuit` and one of either :python:`systems_respecting` or :python:`systems_violating`

    - :python:`*args` and :python:`**kwargs` (like a typical initialization of a :py:class:`~qhronology.quantum.circuits.QuantumCircuit` instance) without specifying :python:`circuit`

    Note
    ----
    The total interaction between the CR and CV systems is expected to be unitary, and so the sequence of gates in :python:`gates` cannot contain any non-unitary gates (e.g., measurement operations).

    Note
    ----
    Post-processing (e.g., traces and postselections) cannot be performed on any chronology-violating (CV) systems (i.e., those corresponding to indices specified in :python:`systems_violating`).
    """

    def __init__(
        self,
        *args,
        circuit: QuantumCircuit | None = None,
        systems_respecting: list[int] | None = None,
        systems_violating: list[int] | None = None,
        **kwargs,
    ):
        super().__init__(*args, **kwargs)
        if circuit is not None:
            self.__dict__ = copy.deepcopy(circuit.__dict__)

        if hasattr(self, "_systems_respecting") is False:
            systems_respecting = (
                [] if systems_respecting is None else systems_respecting
            )
            systems_violating = [] if systems_violating is None else systems_violating
            if len(systems_respecting) == 0 and len(systems_violating) == 0:
                raise ValueError(
                    """Either :python:`systems_respecting` or :python:`systems_violating` must be set."""
                )
            if len(systems_respecting) != 0 and len(systems_violating) != 0:
                if set(self.systems) != set(systems_respecting) | set(
                    systems_violating
                ):
                    raise ValueError(
                        """The union of :python:`systems_respecting` and :python:`systems_violating` is inequivalent to the entire system's structure."""
                    )

            self.systems_respecting = systems_respecting

    def __repr__(self) -> str:
        return repr(self.output_respecting())

    @property
    def matrix(self) -> mat:
        """The matrix representation of the total CTC-circuit output state (both CR and CV systems) prior to any post-processing."""
        return QuantumCircuit(
            inputs=self.inputs,
            gates=self.gates,
            traces=self.traces,
            postselections=self.postselections,
            symbols=self.symbols,
            conditions=self.conditions,
        ).matrix

    @property
    def systems_respecting(self) -> list[int]:
        """The numerical indices of the chronology-respecting (CR) subsystems."""
        return self._systems_respecting

    @systems_respecting.setter
    def systems_respecting(self, systems_respecting: list[int]):
        self._systems_respecting = systems_respecting
        self._systems_respecting = list(set(self._systems_respecting))

    @property
    def systems_violating(self) -> list[int]:
        """The numerical indices of the chronology-violating (CV) subsystems."""
        return list(set(self.systems) ^ set(self.systems_respecting))

    @systems_violating.setter
    def systems_violating(self, systems_violating: list[int]):
        systems_respecting = list(set(self.systems) ^ set(systems_violating))
        self.systems_respecting = systems_respecting

    @property
    def input_is_vector(self) -> bool:
        """Whether all states in :python:`inputs` are vector states."""
        is_vector = False
        if all(state.is_vector for state in self.inputs):
            is_vector = True
        return is_vector

    def input(
        self,
        merge: bool | None = None,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        norm: bool | num | expr | str | None = None,
        label: str | None = None,
        notation: str | None = None,
        debug: bool | None = None,
    ) -> QuantumState:
        """Construct the composite chronology-respecting (CR) input state of the closed timelike curve as a :py:class:`~qhronology.quantum.states.QuantumState` instance and return it.

        This is computed as the tensor product of the individual gates in the order in which they appear in the :python:`inputs` property.

        Is a vector state only when all of the component states are vectors.

        Arguments
        ---------
        merge : bool
            Whether to merge the labels of the individual quantum states into a single product, separated by :python:`"⊗"` operators, prior to any notational processing.
            Only relevant when all states are vectors.
            Defaults to :python:`True`.
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            If :python:`False`, does not substitute the conditions.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        norm : bool | num | expr | str
            The value to which the state is normalized.
            If :python:`True`, normalizes to a value of :math:`1`.
            If :python:`False`, does not normalize.
            Defaults to :python:`False`.
        label : str
            The unformatted string used to represent the state in mathematical expressions.
            Must have a non-zero length.
            Defaults to :python:`"⊗".join([state.label for state in self.inputs])`.
        notation : str
            The formatted string used to represent the state in mathematical expressions.
            When not :python:`None`, overrides the value passed to :python:`label`.
            Must have a non-zero length.
            Not intended to be set by the user in most cases.
            Defaults to :python:`"⊗".join([state.notation for state in self.inputs])` if :python:`label` is :python:`None` and either :python:`merge` is :python:`False` or the input states are all vectors, else :python:`None`.
        debug : bool
            Whether to print the internal state (held in :python:`matrix`) on change.
            Defaults to :python:`False`.

        Returns
        -------
        mat
            The total input state as a :py:class:`~qhronology.quantum.states.QuantumState` instance.

        Note
        ----
        Passing a value of :python:`False` to the :python:`merge` argument results in a state whose :python:`notation` is fixed and incompatible with any subsequent changes (including densification).
        This behaviour may be improved in the future.
        """
        merge = True if merge is None else merge
        conditions = self.conditions if conditions is None else conditions

        if label is None and (
            (merge is False and self.input_is_vector is True)
            or self.input_is_vector is False
        ):
            notation = (
                "⊗".join([state.notation for state in self.inputs])
                if notation is None
                else notation
            )
        label = (
            "⊗".join([state.label for state in self.inputs]) if label is None else label
        )

        form = Forms.MATRIX.value
        kind = Kinds.MIXED.value
        if self.input_is_vector is True:
            form = Forms.VECTOR.value
            kind = Kinds.PURE.value
        inputs = [state.output(conjugate=False) for state in self.inputs]
        if self.input_is_vector is False:
            inputs = [densify(state) for state in inputs]
        input_state = sp.Matrix(TensorProduct(*inputs))

        if count_systems(input_state, self.dim) != len(self.systems_respecting):
            raise ValueError(
                """The size of the given input state(s) does not match that specified by the property :python:`systems_respecting`."""
            )

        input_state = QuantumState(
            spec=input_state,
            form=form,
            kind=kind,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            norm=False,
            conjugate=False,
            label=None,
            notation=None,
            debug=False,
        )

        # Simplification
        simplify = False if simplify is None else simplify
        if simplify is True:
            input_state.simplify()

        input_state = QuantumState(
            form=form,
            kind=kind,
            spec=input_state.output(),
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            conjugate=conjugate,
            norm=norm,
            label=label,
            notation=notation,
            debug=debug,
        )

        return input_state

    # The four methods below merely output the reduced states, so the :python:`systems_respective` and :python:`systems_violating` of the base class acts just like extra traces.
    def output_violating(self) -> mat:
        return (
            QuantumCircuit(
                inputs=self.inputs,
                gates=self.gates,
                traces=self.traces,
                postselections=self.postselections,
                symbols=self.symbols,
                conditions=self.conditions,
            )
            .state(traces=self.systems_respecting)
            .output()
        )

    def output_respecting(self) -> mat:
        return (
            QuantumCircuit(
                inputs=self.inputs,
                gates=self.gates,
                traces=self.traces,
                postselections=self.postselections,
                symbols=self.symbols,
                conditions=self.conditions,
            )
            .state(traces=self.systems_violating)
            .output()
        )

    def state_violating(self) -> QuantumState:
        return QuantumCircuit(
            inputs=self.inputs,
            gates=self.gates,
            traces=self.traces,
            postselections=self.postselections,
            symbols=self.symbols,
            conditions=self.conditions,
        ).state(traces=self.systems_respecting)

    def state_respecting(self) -> QuantumState:
        return QuantumCircuit(
            inputs=self.inputs,
            gates=self.gates,
            traces=self.traces,
            postselections=self.postselections,
            symbols=self.symbols,
            conditions=self.conditions,
        ).state(traces=self.systems_violating)


def dctc_violating(
    input_respecting: mat | QuantumState,
    gate: mat | QuantumGate,
    systems_respecting: list[int],
    systems_violating: list[int],
    free_symbol: sym | str | None = None,
) -> mat:
    """Calculate the chronology-violating (CV) state(s) according to the D-CTC prescription by computing fixed points of the map

    .. math::

        \\MapDCTCsCV_{\\Unitary}[\\StateCR,\\StateCV]
            = \\trace_\\CR\\bigl[\\Unitary(\\StateCR \\otimes \\StateCV)\\Unitary^\\dagger\\bigr]

    given the chronology-respecting (CR) input state :python:`input_respecting` (:math:`\\StateCR`) and (unitary) interaction described by :python:`gate` (:math:`\\Unitary`).

    Arguments
    ---------
    input_respecting : mat | QuantumState
        The matrix representation of the chronology-respecting (CR) input state.
    gate : mat | QuantumGate
        The matrix representation of the gate describing the (unitary) interaction between the CR and CV systems.
    systems_respecting : list[int]
        The numerical indices of the chronology-respecting (CR) subsystems.
    systems_violating : list[int]
        The numerical indices of the chronology-violating (CV) subsystems.
    free_symbol : sym | str
        The representation of the algebraic symbol to be used as the free parameter in the case where the CV map has a multiplicity of fixed points.
        Defaults to :python:`"g"`.

    Returns
    -------
    mat
        The fixed-point solution(s) of the D-CTC CV map.

    Note
    ----
    Please note that this function in its current form is considered to be *highly* experimental.

    """
    free_symbol = "g" if free_symbol is None else free_symbol
    systems_respecting = list(set(systems_respecting))
    systems_violating = list(set(systems_violating))
    try:
        dim = input_respecting.dim
    except:
        dim = count_dims(
            matrix=densify(extract_matrix(input_respecting)), systems=systems_respecting
        )

    conditions_respecting = extract_conditions(input_respecting)
    trace_respecting = input_respecting.trace()
    input_respecting = densify(extract_matrix(input_respecting))

    # Use :python:`Symbol` for persistent (structurally bound) variables.
    # Use :python:`Dummy` for non-persistent (not structurally bound) variables.
    # The latter should be used so as to not interfere with any user-predefined variables.
    input_violating = sp.Matrix(
        [
            [sp.Dummy(f"τ_{i}{j}") for j in range(0, dim ** len(systems_violating))]
            for i in range(0, dim ** len(systems_violating))
        ]
    )

    input_total = assemble_composition(
        (input_respecting, systems_respecting), (input_violating, systems_violating)
    )

    gate = densify(extract_matrix(gate))
    output_total = gate * input_total * Dagger(gate)
    output_violating = partial_trace(
        matrix=output_total, targets=systems_respecting, dim=dim
    )
    output_violating = recursively_simplify(output_violating, conditions_respecting)

    equations = []
    unknowns_violating = [*input_violating]
    unknowns = list(unknowns_violating)

    for n in range(0, (dim ** len(systems_violating)) ** 2):
        equations.append(sp.Eq(output_violating[n], input_violating[n], evaluate=False))

    if trace_respecting == 1:
        equations += [sp.Eq(trace(input_respecting), 1, evaluate=False)]
        equations += [sp.Eq(trace(input_violating), 1, evaluate=False)]

    # Designed to loop twice: once without the trace equation, then once with it.
    counter = 0
    while True:
        solutions = sp.solve(equations, unknowns, set=True)
        if solutions[1] == set() or solutions[1] == {tuple(0 for _ in unknowns)}:
            solutions = sp.nonlinsolve(equations, unknowns)
            if isinstance(solutions, sp.sets.sets.EmptySet) is True or solutions == {
                tuple(0 for _ in unknowns)
            }:
                solutions = set()
            solutions = (unknowns, solutions)
        if len(solutions[1]) == 1:
            break
        elif len(solutions[1]) == 0:
            # Remove final equation (possibly trace condition) and try again.
            del equations[-1]
        else:
            raise NotImplementedError(
                """Support for multiple non-parametrized D-CTC CV solutions has not yet been implemented."""
            )
        counter += 1
        if counter == 2:
            raise NotImplementedError(
                """The D-CTC CV algorithm was unable to determine a solution (fixed point) to the CV map.
                If you are certain that your circuit does indeed have a solution, you are welcome to file a bug report."""
            )

    solutions = list(solutions[1])[0]
    solutions = {key: value for key, value in zip(unknowns, solutions)}

    # Check for all zeroes solution and, if found, replace it with the CR input state.
    # This assumes that a zero solution corresponds to the CR input.
    if any(value != 0 for value in solutions.values()) is False:
        pairs = {
            key: value
            for key, value in zip(list(input_violating), list(input_respecting))
        }
        for key, value in solutions.items():
            solutions[key] = pairs[key]
        solutions = list(solutions.values())
    else:
        # Invert solution about τ_00 in the case of a single free parameter so that the final result is consistent with the analytical approach.
        if len(solutions[input_violating[0]].free_symbols) == 1:
            inverting_symbol = list(solutions[input_violating[0]].free_symbols)[0]
            if inverting_symbol in solutions.keys():
                inverting_equation = [
                    sp.Eq(
                        input_violating[0],
                        solutions[input_violating[0]],
                        evaluate=False,
                    )
                ]
                inverting_solution = sp.nonlinsolve(
                    inverting_equation, [inverting_symbol]
                )
                inverting_solution = list(inverting_solution)[0]
                inverting_solution = {
                    key: value
                    for key, value in zip([inverting_symbol], inverting_solution)
                }
                for key, value in solutions.items():
                    solutions[key] = sp.simplify(
                        value.subs(
                            inverting_symbol, inverting_solution[inverting_symbol]
                        )
                    )

        unknowns = unknowns_violating
        solutions = [
            sp.simplify(solutions[unknown]) if unknown in solutions.keys() else unknown
            for unknown in unknowns
        ]
        free_symbols = list(
            set().union(*[element.free_symbols for element in solutions])
        )
        unknowns_free = [symbol for symbol in free_symbols if symbol in unknowns]
        free_variables = []
        if len(unknowns_free) == 1:
            free_variables = [sp.Symbol(f"{free_symbol}")]
        if len(unknowns_free) > 1:
            free_variables = [
                sp.Symbol(f"{free_symbol}_{i}") for i in range(0, len(unknowns_free))
            ]
        if len(free_variables) != 0:
            solutions = [
                solution.subs(dict(zip(unknowns_free, free_variables)))
                for solution in solutions
            ]

    output_violating = sp.Matrix(
        np.array(solutions).reshape(
            dim ** len(systems_violating), dim ** len(systems_violating)
        )
    )

    return output_violating


def dctc_respecting(
    input_respecting: mat | QuantumState,
    input_violating: mat | QuantumState,
    gate: mat | QuantumGate,
    systems_respecting: list[int],
    systems_violating: list[int],
) -> mat:
    """Calculate the chronology-respecting (CR) state(s) according to the D-CTC prescription's CR map

    .. math::

       \\MapDCTCsCR_{\\Unitary}[\\StateCR,\\StateCV]
           = \\trace_\\CV\\bigl[\\Unitary(\\StateCR \\otimes \\StateCV)\\Unitary^\\dagger\\bigr]

    given the chronology-respecting (CR) input state :python:`input_respecting` (:math:`\\StateCR`), chronology-violating (CV) solution state :python:`input_violating` (:math:`\\StateCV`), and (unitary) interaction described by :python:`gate` (:math:`\\Unitary`).

    Arguments
    ---------
    input_respecting : mat | QuantumState
        The matrix representation of the chronology-respecting (CR) input state.
    input_violating : mat | QuantumState
        The matrix representation of the chronology-violating (CR) solution state.
    gate : mat | QuantumGate
        The matrix representation of the gate describing the (unitary) interaction between the CR and CV systems.
    systems_respecting : list[int]
        The numerical indices of the chronology-respecting (CR) subsystems.
    systems_violating : list[int]
        The numerical indices of the chronology-violating (CV) subsystems.

    Returns
    -------
    mat
        The solution(s) of the D-CTC CR map.

    """
    systems_respecting = list(set(systems_respecting))
    systems_violating = list(set(systems_violating))
    try:
        dim = input_respecting.dim
    except:
        dim = count_dims(
            matrix=densify(extract_matrix(input_respecting)), systems=systems_respecting
        )

    input_respecting = densify(extract_matrix(input_respecting))
    input_violating = densify(extract_matrix(input_violating))
    input_total = assemble_composition(
        (input_respecting, systems_respecting), (input_violating, systems_violating)
    )

    gate = densify(extract_matrix(gate))
    output_total = gate * input_total * Dagger(gate)
    output_respecting = partial_trace(
        matrix=output_total, targets=systems_violating, dim=dim
    )

    return output_respecting


class DCTC(QuantumCTC):
    """A subclass for creating closed timelike curves described by Deutsch's prescription (D-CTCs) of quantum time travel.

    This is built upon the :py:class:`~qhronology.quantum.prescriptions.QuantumCTC` class, and so inherits all of its attributes, properties, and methods.

    Arguments
    ---------
    *args
        Variable-length argument list, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
    free_symbol : sym | str
        The representation of the algebraic symbol to be used as the free parameter in the case where the CV map has a multiplicity of fixed points.
        Defaults to :python:`"g"`.
    **kwargs
        Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
    """

    def __init__(self, *args, free_symbol: sym | str | None = None, **kwargs):
        super().__init__(*args, **kwargs)
        free_symbol = "g" if free_symbol is None else free_symbol
        self.free_symbol = free_symbol

    @property
    def free_symbol(self) -> sym | str:
        """The representation of the algebraic symbol to be used as the free parameter in the case where the CV map has a multiplicity of fixed points."""
        return self._free_symbol

    @free_symbol.setter
    def free_symbol(self, free_symbol: sym | str):
        self._free_symbol = free_symbol

    @property
    def input_is_vector(self) -> bool:
        return False
        # D-CTC prescription requires density matrix inputs.

    @property
    def output_is_vector(self) -> bool:
        return False
        # The CR and CV maps of a D-CTC are non-linear, non-unitary (mixing) operations in general.

    @property
    def matrix(self) -> mat:
        """The matrix representation of the total D-CTC chronology-respecting (CR) output state prior to any post-processing."""
        output_respecting = dctc_respecting(
            input_respecting=self.input(conditions=[]),
            input_violating=self.state_violating(free_symbol=self.free_symbol),
            gate=self.gate(conditions=[]),
            systems_respecting=self.systems_respecting,
            systems_violating=self.systems_violating,
        )
        return output_respecting

    def output_violating(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        free_symbol: sym | str | None = None,
    ) -> mat:
        """Compute the matrix representation of the D-CTC chronology-violating (CV) state(s).

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        free_symbol : str
            The string representation of the algebraic symbol to be used as the free parameter in the case where the CV map has a multiplicity of fixed points.
            Defaults to the value of :python:`self.free_symbol`.

        Returns
        -------
        mat
            The matrix representation of the CV output state.
        """
        free_symbol = self.free_symbol if free_symbol is None else free_symbol

        output_violating = dctc_violating(
            input_respecting=self.input(conditions=[]),
            gate=self.gate(conditions=[]),
            systems_respecting=self.systems_respecting,
            systems_violating=self.systems_violating,
            free_symbol=free_symbol,
        )

        form = Forms.MATRIX.value
        kind = Kinds.MIXED.value

        output_violating = QuantumState(
            spec=output_violating,
            form=form,
            kind=kind,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            norm=False,
            conjugate=False,
            label=None,
            notation=None,
            debug=False,
        )

        # Simplification
        simplify = False if simplify is None else simplify
        if simplify is True:
            output_violating.simplify()

        # Conjugation
        conjugate = False if conjugate is None else conjugate
        if conjugate is True:
            output_violating.dagger()

        output_violating = QuantumState(
            spec=output_violating.output(),
            form=form,
            kind=kind,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            norm=False,
            conjugate=False,
            label=None,
            notation=None,
            debug=False,
        )

        return sp.Matrix(output_violating.output())

    def output_respecting(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        postprocess: bool | None = None,
        free_symbol: sym | str | None = None,
    ) -> mat:
        """Compute the matrix representation of the D-CTC chronology-respecting (CR) state(s) (including any post-processing).

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        postprocess : bool
            Whether to post-process the state (i.e., perform the circuit's traces and postselections).
            Defaults to :python:`True`.
        free_symbol : str
            The string representation of the algebraic symbol to be used as the free parameter in the case where the CV map has a multiplicity of fixed points.
            Defaults to the value of :python:`self.free_symbol`.

        Returns
        -------
        mat
            The matrix representation of the (post-processed) CR output state.
        """
        conditions = self.conditions if conditions is None else conditions
        free_symbol = self.free_symbol if free_symbol is None else free_symbol

        output_respecting = dctc_respecting(
            input_respecting=self.input(conditions=[]),
            input_violating=self.state_violating(free_symbol=free_symbol),
            gate=self.gate(conditions=[]),
            systems_respecting=self.systems_respecting,
            systems_violating=self.systems_violating,
        )

        form = Forms.MATRIX.value
        kind = Kinds.MIXED.value
        output_respecting = QuantumState(
            spec=output_respecting,
            form=form,
            kind=kind,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            norm=False,
            conjugate=False,
            label=None,
            notation=None,
            debug=False,
        )

        postprocess = True if postprocess is None else postprocess
        if postprocess is True:
            systems_removed = []

            # Partial traces
            traces = adjust_targets(self.systems_traces, systems_removed)
            output_respecting.partial_trace(targets=traces)
            systems_removed += traces

            # Postselections
            for postselection in self.postselections:
                length = count_systems(extract_matrix(postselection[0]), self.dim)
                listed = flatten_list([postselection[1]])
                systems = [(min(listed) + n) for n in range(0, length)]
                targets_postselection = adjust_targets(systems, systems_removed)
                output_respecting.postselect(
                    postselections=[(postselection[0], targets_postselection)]
                )
                systems_removed += systems

        # Simplification
        simplify = False if simplify is None else simplify
        if simplify is True:
            output_respecting.simplify()

        # Conjugation
        conjugate = False if conjugate is None else conjugate
        if conjugate is True:
            output_respecting.dagger()

        output_respecting = QuantumState(
            spec=output_respecting.output(),
            form=form,
            kind=kind,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            norm=False,
            conjugate=False,
            label=None,
            notation=None,
            debug=False,
        )

        return sp.Matrix(output_respecting.output())

    def output(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        postprocess: bool | None = None,
        free_symbol: sym | str | None = None,
    ) -> mat:
        """An alias for the :py:meth:`~qhronology.quantum.prescriptions.DCTC.output_respecting` method.

        Useful for polymorphism.

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        postprocess : bool
            Whether to post-process the state (i.e., perform the circuit's traces and postselections).
            Defaults to :python:`True`.
        free_symbol : str
            The string representation of the algebraic symbol to be used as the free parameter in the case where the CV map has a multiplicity of fixed points.
            Defaults to the value of :python:`self.free_symbol`.

        Returns
        -------
        mat
            The matrix representation of the (post-processed) CR output state.
        """
        return self.output_respecting(
            conditions=conditions,
            simplify=simplify,
            conjugate=conjugate,
            postprocess=postprocess,
            free_symbol=free_symbol,
        )

    def state_violating(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        norm: bool | num | expr | str | None = None,
        label: str | None = None,
        notation: str | None = None,
        traces: list[int] | None = None,
        debug: bool | None = None,
        free_symbol: sym | str | None = None,
    ) -> QuantumState:
        """Compute the D-CTC chronology-violating (CV) state(s) as a :py:class:`~qhronology.quantum.states.QuantumState` instance.

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state before committing it to the :python:`matrix` property.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        norm : bool | num | expr | str
            The value to which the state is normalized.
            If :python:`True`, normalizes to a value of :math:`1`.
            If :python:`False`, does not normalize.
            Defaults to :python:`False`.
        label : str
            The unformatted string used to represent the state in mathematical expressions.
            Must have a non-zero length.
            Defaults to :python:`"ρ"` (if :python:`form == "matrix"`) or :python:`"ψ"` (if :python:`form == "vector"`).
        notation : str
            The formatted string used to represent the state in mathematical expressions.
            When not :python:`None`, overrides the value passed to :python:`label`.
            Must have a non-zero length.
            Not intended to be set by the user in most cases.
            Defaults to :python:`None`.
        traces : list[int]
            A list of indices of the CV systems (relative to the entire circuit) on which to perform partial traces.
            Defaults to :python:`[]`.
        free_symbol : str
            The string representation of the algebraic symbol to be used as the free parameter in the case where the CV map has a multiplicity of fixed points.
            Defaults to the value of :python:`self.free_symbol`.
        debug : bool
            Whether to print the internal state (held in :python:`matrix`) on change.
            Defaults to :python:`False`.

        Returns
        -------
        QuantumState
            The CV output state as a :py:class:`~qhronology.quantum.states.QuantumState` instance.
        """
        conditions = self.conditions if conditions is None else conditions
        traces = [] if traces is None else traces

        form = Forms.MATRIX.value
        kind = Kinds.MIXED.value
        state = QuantumState(
            form=form,
            kind=kind,
            spec=sp.Matrix(
                self.output_violating(
                    conditions=conditions,
                    simplify=simplify,
                    conjugate=False,
                    free_symbol=free_symbol,
                )
            ),
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            conjugate=conjugate,
            norm=norm,
            label=label,
            notation=notation,
            debug=debug,
        )
        traces = adjust_targets(traces, self.systems_removed + self.systems_respecting)
        state.partial_trace(targets=traces)
        return state

    def state_respecting(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        norm: bool | num | expr | str | None = None,
        label: str | None = None,
        notation: str | None = None,
        traces: list[int] | None = None,
        postprocess: bool | None = None,
        debug: bool | None = None,
        free_symbol: sym | str | None = None,
    ) -> QuantumState:
        """Compute the D-CTC chronology-respecting (CR) state(s) as a :py:class:`~qhronology.quantum.states.QuantumState` instance.

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state before committing it to the :python:`matrix` property.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        norm : bool | num | expr | str
            The value to which the state is normalized.
            If :python:`True`, normalizes to a value of :math:`1`.
            If :python:`False`, does not normalize.
            Defaults to :python:`False`.
        label : str
            The unformatted string used to represent the state in mathematical expressions.
            Must have a non-zero length.
            Defaults to :python:`"ρ"` (if :python:`form == "matrix"`) or :python:`"ψ"` (if :python:`form == "vector"`).
        notation : str
            The formatted string used to represent the state in mathematical expressions.
            When not :python:`None`, overrides the value passed to :python:`label`.
            Must have a non-zero length.
            Not intended to be set by the user in most cases.
            Defaults to :python:`None`.
        traces : list[int]
            A list of indices of the CR systems (relative to the entire circuit) on which to perform partial traces.
            Performed regardless of the value of :python:`postprocess`.
            Defaults to :python:`[]`.
        postprocess : bool
            Whether to post-process the state (i.e., perform the circuit's traces and postselections).
            Defaults to :python:`True`.
        free_symbol : str
            The string representation of the algebraic symbol to be used as the free parameter in the case where the CV map has a multiplicity of fixed points.
            Defaults to the value of :python:`self.free_symbol`.
        debug : bool
            Whether to print the internal state (held in :python:`matrix`) on change.
            Defaults to :python:`False`.

        Returns
        -------
        QuantumState
            The (post-processed) CR output state as a :py:class:`~qhronology.quantum.states.QuantumState` instance.
        """
        conditions = self.conditions if conditions is None else conditions
        traces = [] if traces is None else traces
        postprocess = True if postprocess is None else postprocess

        form = Forms.MATRIX.value
        kind = Kinds.MIXED.value
        if postprocess is True:
            traces = adjust_targets(
                traces, self.systems_removed + self.systems_violating
            )
        else:
            traces = adjust_targets(traces, self.systems_violating)

        matrix = sp.Matrix(
            self.output_respecting(
                conditions=conditions,
                simplify=simplify,
                conjugate=False,
                postprocess=postprocess,
                free_symbol=free_symbol,
            )
        )

        state = QuantumState(
            form=form,
            kind=kind,
            spec=matrix,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            conjugate=conjugate,
            norm=norm,
            label=label,
            notation=notation,
            debug=debug,
        )
        state.partial_trace(targets=traces)
        return state


def pctc_violating(
    input_respecting: mat | QuantumState,
    gate: mat | QuantumGate,
    systems_respecting: list[int],
    systems_violating: list[int],
) -> mat:
    """Calculate the chronology-violating (CV) state according to the P-CTC weak-measurement tomography expression for the prescription's CV map

    .. math::

       \\MapPCTCsCV_{\\Unitary}[\\StateCR]
           = \\trace_\\CR\\bigl[\\Unitary(\\StateCR \\otimes \\tfrac{1}{\\Dimension}\\Identity)
           \\Unitary^\\dagger\\bigr],

    given the chronology-respecting (CR) input state :python:`input_respecting` (:math:`\\StateCR`) and interaction described by :python:`gate` (:math:`\\Unitary`).
    Here, :math:`\\Dimension` is the dimensionality of the CV Hilbert space (assumed to be equivalent to that of its CR counterpart), while :math:`\\Identity` is the :math:`\\Dimension \\times \\Dimension` identity matrix.

    Arguments
    ---------
    input_respecting : mat | QuantumState
        The matrix representation of the chronology-respecting (CR) input state.
    gate : mat | QuantumGate
        The matrix representation of the gate describing the (unitary) interaction between the CR and CV systems.
    systems_respecting : list[int]
        The numerical indices of the chronology-respecting (CR) subsystems.
    systems_violating : list[int]
        The numerical indices of the chronology-violating (CV) subsystems.

    Returns
    -------
    mat
        The weak-measurement tomography expression for the P-CTC's CV state.

    Note
    ----
    The validity of the expression used in this function to compute the P-CTC CV state for *non-qubit* systems has not been proven.
    """
    systems_respecting = list(set(systems_respecting))
    systems_violating = list(set(systems_violating))
    try:
        dim = input_respecting.dim
    except:
        dim = count_dims(
            matrix=densify(extract_matrix(input_respecting)), systems=systems_respecting
        )

    input_respecting = densify(extract_matrix(input_respecting))
    identity = (sp.Rational(1, dim) ** len(systems_violating)) * sp.eye(
        dim ** len(systems_violating)
    )

    input_total = assemble_composition(
        (input_respecting, systems_respecting), (identity, systems_violating)
    )

    gate = densify(extract_matrix(gate))
    output_total = gate * input_total * Dagger(gate)
    output_violating = partial_trace(
        matrix=output_total, targets=systems_respecting, dim=dim
    )

    return output_violating


def pctc_respecting(
    input_respecting: mat | QuantumState,
    gate: mat | QuantumGate,
    systems_respecting: list[int],
    systems_violating: list[int],
) -> mat:
    """Calculate the (non-renormalized) chronology-respecting (CR) state according to the P-CTC prescription's non-renormalizing CR map

    .. math::

       \\MapPCTCsCR_{\\Unitary}[\\StateCR]
           \\propto \\OperatorPCTC \\StateCR \\OperatorPCTC^\\dagger

    given the chronology-respecting (CR) input state :python:`input_respecting` (:math:`\\StateCR`) and (unitary) interaction described by :python:`gate` (:math:`\\Unitary`).
    Here,

    .. math:: \\OperatorPCTC \\equiv \\trace_\\CV[\\Unitary]

    is the P-CTC operator.

    Note
    ----
    This function does not renormalize the output state as per the renormalized P-CTC map

    .. math::

       \\MapPCTCsCR_{\\Unitary}[\\StateCR]
           = \\frac{\\OperatorPCTC \\StateCR \\OperatorPCTC^\\dagger}
           {\\trace\\bigl[ \\OperatorPCTC \\StateCR \\OperatorPCTC^\\dagger\\bigr]}.

    Arguments
    ---------
    input_respecting : mat | QuantumState
        The matrix representation of the chronology-respecting (CR) input state.
    gate : mat | QuantumGate
        The matrix representation of the gate describing the (unitary) interaction between the CR and CV systems.
    systems_respecting : list[int]
        The numerical indices of the chronology-respecting (CR) subsystems.
    systems_violating : list[int]
        The numerical indices of the chronology-violating (CV) subsystems.

    Returns
    -------
    mat
        The solution of the P-CTC CR map.
    """
    systems_respecting = list(set(systems_respecting))
    systems_violating = list(set(systems_violating))
    try:
        dim = input_respecting.dim
    except:
        dim = count_dims(
            matrix=densify(extract_matrix(input_respecting)), systems=systems_respecting
        )

    input_respecting = extract_matrix(input_respecting)
    gate = densify(extract_matrix(gate))

    gate_reduced = partial_trace(matrix=gate, targets=systems_violating, dim=dim)
    if matrix_form(input_respecting) == Forms.VECTOR.value:
        input_respecting = columnify(input_respecting)
        output_respecting = gate_reduced * input_respecting
        # renormalization = recursively_simplify(sp.sqrt(1/trace(output_respecting)))
        # output_respecting = renormalization*output_respecting
    else:
        output_respecting = gate_reduced * input_respecting * Dagger(gate_reduced)
        # renormalization = recursively_simplify(1/trace(output_respecting))
        # output_respecting = renormalization*output_respecting

    return output_respecting


class PCTC(QuantumCTC):
    """A subclass for creating closed timelike curves described by the postselected teleportation prescription (P-CTCs) of quantum time travel.

    This is built upon the :py:class:`~qhronology.quantum.prescriptions.QuantumCTC` class, and so inherits all of its attributes, properties, and methods.

    Arguments
    ---------
    *args
        Variable-length argument list, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
    **kwargs
        Arbitrary keyword arguments, passed directly to the constructor :python:`__init__` of the superclass :py:class:`~qhronology.quantum.gates.QuantumGate`.
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    @property
    def matrix(self) -> mat:
        """The matrix representation of the total P-CTC chronology-respecting (CR) output state prior to any post-processing."""
        output_respecting = pctc_respecting(
            input_respecting=self.input(conditions=[]),
            gate=self.gate(conditions=[]),
            systems_respecting=self.systems_respecting,
            systems_violating=self.systems_violating,
        )
        return output_respecting

    def output_violating(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
    ) -> mat:
        """Compute the matrix representation of the P-CTC chronology-violating (CV) state.

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.

        Returns
        -------
        mat
            The matrix representation of the CV output state.

        Note
        ----
        The validity of the expression used in this method to compute the P-CTC CV state for *non-qubit* systems has not been proven.
        """
        output_violating = pctc_violating(
            input_respecting=self.input(conditions=[]),
            gate=self.gate(conditions=[]),
            systems_respecting=self.systems_respecting,
            systems_violating=self.systems_violating,
        )

        form = Forms.MATRIX.value
        kind = Kinds.MIXED.value

        output_violating = QuantumState(
            spec=output_violating,
            form=form,
            kind=kind,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            norm=False,
            conjugate=False,
            label=None,
            notation=None,
            debug=False,
        )

        # Simplification
        simplify = False if simplify is None else simplify
        if simplify is True:
            output_violating.simplify()

        # Conjugation
        conjugate = False if conjugate is None else conjugate
        if conjugate is True:
            output_violating.dagger()

        output_violating = QuantumState(
            spec=output_violating.output(),
            form=form,
            kind=kind,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            norm=False,
            conjugate=False,
            label=None,
            notation=None,
            debug=False,
        )

        return sp.Matrix(output_violating.output())

    def output_respecting(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        postprocess: bool | None = None,
    ) -> mat:
        """Compute the matrix representation of the P-CTC chronology-respecting (CR) state (including any post-processing).

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        postprocess : bool
            Whether to post-process the state (i.e., perform the circuit's traces and postselections).
            Defaults to :python:`True`.

        Returns
        -------
        mat
            The matrix representation of the (post-processed) CR output state.

        Note
        ----
        The output state is not renormalized.
        """
        conditions = self.conditions if conditions is None else conditions

        output_respecting = pctc_respecting(
            input_respecting=self.input(conditions=[]),
            gate=self.gate(conditions=[]),
            systems_respecting=self.systems_respecting,
            systems_violating=self.systems_violating,
        )

        form = Forms.MATRIX.value
        kind = Kinds.MIXED.value
        if self.input_is_vector is True:
            form = Forms.VECTOR.value
            kind = Kinds.PURE.value
        if self.gate_is_linear is False:
            form = Forms.MATRIX.value
            kind = Kinds.MIXED.value
        output_respecting = QuantumState(
            spec=output_respecting,
            form=form,
            kind=kind,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            norm=False,
            conjugate=False,
            label=None,
            notation=None,
            debug=False,
        )

        postprocess = True if postprocess is None else postprocess
        if postprocess is True:
            systems_removed = []

            # Partial traces
            traces = adjust_targets(self.systems_traces, systems_removed)
            output_respecting.partial_trace(targets=traces)
            systems_removed += traces

            # Postselections
            for postselection in self.postselections:
                length = count_systems(extract_matrix(postselection[0]), self.dim)
                listed = flatten_list([postselection[1]])
                systems = [(min(listed) + n) for n in range(0, length)]
                targets_postselection = adjust_targets(systems, systems_removed)
                output_respecting.postselect(
                    postselections=[(postselection[0], targets_postselection)]
                )
                systems_removed += systems

        # Simplification
        simplify = False if simplify is None else simplify
        if simplify is True:
            output_respecting.simplify()

        # Conjugation
        conjugate = False if conjugate is None else conjugate
        if conjugate is True:
            output_respecting.dagger()

        output_respecting = QuantumState(
            spec=output_respecting.output(),
            form=form,
            kind=kind,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            norm=False,
            conjugate=False,
            label=None,
            notation=None,
            debug=False,
        )

        return sp.Matrix(output_respecting.output())

    def output(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        postprocess: bool | None = None,
    ) -> mat:
        """An alias for the :py:meth:`~qhronology.quantum.prescriptions.PCTC.output_respecting` method.

        Useful for polymorphism.

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        postprocess : bool
            Whether to post-process the state (i.e., perform the circuit's traces and postselections).
            Defaults to :python:`True`.

        Returns
        -------
        mat
            The matrix representation of the (post-processed) CR output state.

        Note
        ----
        The output state is not renormalized.
        """
        return self.output_respecting(
            conditions=conditions,
            simplify=simplify,
            conjugate=conjugate,
            postprocess=postprocess,
        )

    def state_violating(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        norm: bool | num | expr | str | None = None,
        label: str | None = None,
        notation: str | None = None,
        traces: list[int] | None = None,
        debug: bool | None = None,
    ) -> QuantumState:
        """Compute the P-CTC chronology-violating (CV) state as a :py:class:`~qhronology.quantum.states.QuantumState` instance.

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state before committing it in the :python:`matrix` property.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        norm : bool | num | expr | str
            The value to which the state is normalized.
            If :python:`True`, normalizes to a value of :math:`1`.
            If :python:`False`, does not normalize.
            Defaults to :python:`False`.
        label : str
            The unformatted string used to represent the state in mathematical expressions.
            Must have a non-zero length.
            Defaults to :python:`"ρ"` (if :python:`form == "matrix"`) or :python:`"ψ"` (if :python:`form == "vector"`).
        notation : str
            The formatted string used to represent the state in mathematical expressions.
            When not :python:`None`, overrides the value passed to :python:`label`.
            Must have a non-zero length.
            Not intended to be set by the user in most cases.
            Defaults to :python:`None`.
        traces : list[int]
            A list of indices of the CV systems (relative to the entire circuit) on which to perform partial traces.
            Defaults to :python:`[]`.
        debug : bool
            Whether to print the internal state (held in :python:`matrix`) on change.
            Defaults to :python:`False`.

        Returns
        -------
        QuantumState
            The CV output state as a :py:class:`~qhronology.quantum.states.QuantumState` instance.

        Note
        ----
        The validity of the expression used in this method to compute the P-CTC CV state for *non-qubit* systems has not been proven.
        """
        traces = [] if traces is None else traces

        form = Forms.MATRIX.value
        kind = Kinds.MIXED.value
        state = QuantumState(
            form=form,
            kind=kind,
            spec=sp.Matrix(
                self.output_violating(
                    conditions=conditions, simplify=simplify, conjugate=False
                )
            ),
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            conjugate=conjugate,
            norm=norm,
            label=label,
            notation=notation,
            debug=debug,
        )
        traces = adjust_targets(traces, self.systems_removed + self.systems_respecting)
        state.partial_trace(targets=traces)
        return state

    def state_respecting(
        self,
        conditions: list[tuple[num | expr | str, num | expr | str]] | None = None,
        simplify: bool | None = None,
        conjugate: bool | None = None,
        norm: bool | num | expr | str | None = None,
        label: str | None = None,
        notation: str | None = None,
        traces: list[int] | None = None,
        postprocess: bool | None = None,
        debug: bool | None = None,
    ) -> QuantumState:
        """Compute the P-CTC chronology-respecting (CR) state as a :py:class:`~qhronology.quantum.states.QuantumState` instance.

        Arguments
        ---------
        conditions : list[tuple[num | expr | str, num | expr | str]]
            Algebraic conditions to be applied to the state.
            Defaults to the value of :python:`self.conditions`.
        simplify : bool
            Whether to perform algebraic simplification on the state before committing it to the :python:`matrix` property.
            Defaults to :python:`False`.
        conjugate : bool
            Whether to perform Hermitian conjugation on the state.
            Defaults to :python:`False`.
        norm : bool | num | expr | str
            The value to which the state is normalized.
            If :python:`True`, normalizes to a value of :math:`1`.
            If :python:`False`, does not normalize.
            Defaults to :python:`False`.
        label : str
            The unformatted string used to represent the state in mathematical expressions.
            Must have a non-zero length.
            Defaults to :python:`"ρ"` (if :python:`form == "matrix"`) or :python:`"ψ"` (if :python:`form == "vector"`).
        notation : str
            The formatted string used to represent the state in mathematical expressions.
            When not :python:`None`, overrides the value passed to :python:`label`.
            Must have a non-zero length.
            Not intended to be set by the user in most cases.
            Defaults to :python:`None`.
        traces : list[int]
            A list of indices of the CR systems (relative to the entire circuit) on which to perform partial traces.
            Performed regardless of the value of :python:`postprocess`.
            Defaults to :python:`[]`.
        postprocess : bool
            Whether to post-process the state (i.e., perform the circuit's traces and postselections).
            Defaults to :python:`True`.
        debug : bool
            Whether to print the internal state (held in :python:`matrix`) on change.
            Defaults to :python:`False`.

        Returns
        -------
        QuantumState
            The (post-processed) CR output state as a :py:class:`~qhronology.quantum.states.QuantumState` instance.

        Note
        ----
        The output state is not renormalized if :python:`norm` is :python:`False`.
        """
        conditions = self.conditions if conditions is None else conditions
        traces = [] if traces is None else traces
        postprocess = True if postprocess is None else postprocess

        form = Forms.MATRIX.value
        kind = Kinds.MIXED.value

        if postprocess is True:
            if self.output_is_vector is True and len(traces) == 0:
                form = Forms.VECTOR.value
                kind = Kinds.PURE.value
            traces = adjust_targets(
                traces, self.systems_removed + self.systems_violating
            )
        else:
            if (
                self.input_is_vector is True
                and self.gate_is_linear is True
                and len(traces) == 0
            ):
                form = Forms.VECTOR.value
                kind = Kinds.PURE.value
            traces = adjust_targets(traces, self.systems_violating)

        matrix = sp.Matrix(
            self.output_respecting(
                conditions=conditions,
                simplify=simplify,
                conjugate=False,
                postprocess=postprocess,
            )
        )

        state = QuantumState(
            form=form,
            kind=kind,
            spec=matrix,
            symbols=self.symbols,
            dim=self.dim,
            conditions=conditions,
            conjugate=conjugate,
            norm=norm,
            label=label,
            notation=notation,
            debug=debug,
        )
        state.partial_trace(targets=traces)
        return state
