################################################################################
# © Copyright 2021-2022 Zapata Computing Inc.
################################################################################
"""Public API for the orquestra circuits.

Allows:

- defining quantum circuits with gates applied to qubits
- set of built-in gates (see imports in this `__init__`)
- circuit (de)serialization to/from JSON-compatible dicts

Examples:

    Defining a circuit with a NOT gate on qubit 0 & Hadamard gate on qubit 1:

    .. code:: python

        circuit = Circuit()
        circuit += X(0)
        circuit += H(1)

    Adding 2-qubit gates:

    .. code:: python

        circuit += CNOT(0, 4)

    Adding parametrized gates:

    .. code:: python

        circuit += RX(np.pi / 2)(0)
        circuit += RX(sympy.sympify("theta / 2"))(1)

    Adding 2-qubit parametrized gates:

    .. code:: python

        circuit += CPHASE(0.314)(3, 2)

    Adding a built-in gate by its name:

    .. code:: python

        circuit += builtin_gate_by_name("X")(0)
        circuit += builtin_gate_by_name("RX")(np.pi * 1.5)(1)

    Binding parameters:

    .. code:: python

        circuit = circuit.bind({sympy.Symbol("theta"): -np.pi / 5})

    Iterating over circuit contents:

    .. code:: python

        for gate_op in circuit.operations:
            name = gate_op.gate.name
            params = gate_op.gate.params
            qubits = gate_op.qubit_indices
            print(f"{name} with params {params} applied to {qubits}")

    Making a different circuit (e.g. shifting gates by 1 qubit):

    .. code:: python

        new_circuit = Circuit(
            operations=[
                gate_op.gate(*[qubit + 1 for qubit in gate_op.qubits])
                for gate_op in circuit.operations
            ],
            n_qubits=circuit.n_qubits
        )

    (De)serialization:

    .. code:: python

        to_dict(circuit)
        circuit5 = circuit_from_dict(dict5)

    Inverting a circuit::

    .. code:: python

        circuit_inverted = circuit.invert()


Defining new gates
------------------

To use a gate that isn't already covered by built-in ones you can define a custom gate
or extend the set of the built-in ones and file a PR to orquestra-quantum.

Using custom gates:

.. code:: python

    custom_a = circuits.CustomGateDefinition(
        gate_name="custom_a",  # names need to be unique
        matrix=sympy.Matrix(
            [
                [-1, 0],
                [0, 1],
            ]
        ),
        params_ordering=(),
    )

    custom_b = circuits.CustomGateDefinition(
        gate_name="custom_b",
        matrix=sympy.Matrix(
            [
                [0, sympy.Symbol("theta") * 2],
                [sympy.Symbol("gamma") + 3, 1],
            ]
        ),
        params_ordering=(sympy.Symbol("gamma"), sympy.Symbol("theta")),
    )

    circuit = Circuit()
    circuit += custom_a()(0)
    circuit += custom_b(np.pi, np.pi / 2)(0)


Extending built-in gates requires:

- Adding its definition to `orquestra.quantum.circuits._builtin_gates`. Refer to other
    1- or multi-qubit, parametric/nonparametric gates there to see how it's been done
    for other gates.

- Adding its matrix to `orquestra.quantum.circuits._matrices`.

- Adding tests for conversion to other frameworks in:
    - `qeqiskit.conversions.circuit_conversions_test`
    - `qecirq.conversions.circuit_conversions_test`
    - `qeforest.conversions.circuit_conversions_test`

- Implement conversions. Some might work out of the box, e.g. if there's a gate with the
    same name defined in PyQuil our converters will use it by default without need for
    explicit mappings.
"""

from ._builtin_gates import (
    CNOT,
    CPHASE,
    CZ,
    ISWAP,
    MS,
    PHASE,
    RH,
    RX,
    RY,
    RZ,
    SWAP,
    U3,
    XX,
    XY,
    YY,
    ZZ,
    Delay,
    GatePrototype,
    GPi,
    GPi2,
    H,
    I,
    S,
    T,
    X,
    Y,
    Z,
    builtin_gate_by_name,
)
from ._circuit import Circuit, split_circuit
from ._gates import (
    ControlledGate,
    CustomGateDefinition,
    Dagger,
    Exponential,
    Gate,
    GateOperation,
    MatrixFactoryGate,
)
from ._generators import (
    add_ancilla_register,
    apply_gate_to_qubits,
    create_layer_of_gates,
)
from ._itertools import (
    combine_bitstrings,
    combine_measurement_counts,
    expand_sample_sizes,
    split_into_batches,
)
from ._operations import Operation
from ._serde import (
    circuit_from_dict,
    circuitset_from_dict,
    load_circuit,
    load_circuitset,
    save_circuit,
    save_circuitset,
    to_dict,
)
from ._testing import create_random_circuit
from ._wavefunction_operations import MultiPhaseOperation
from .symbolic import natural_key, natural_key_revlex
