FockRepresentation ================== .. py:class:: hybridlane.ops.mixins.FockRepresentation Mixin for operators that define their representation in the Fock basis .. py:method:: compute_fock_matrix(wire_dims, *params, **hyperparams) :staticmethod: Computes the Fock matrix representation of the gate This method should be implemented by any gate to define its representation in the Fock basis. It should return a **dense** matrix of shape ``(d, d)``, where :math:`d = \prod_i \text{wire\_dims}_i` is the total dimension of the Hilbert space of the gate. **Details**: The ``wire_dims`` argument provides the dimension of each wire in the canonical wire order, the same order as ``self.wires``. An example using the ``CR`` gate, which has wire types ``[qubit, qumode]``, using dimension 2 for the qubit and a dimension of 3 for the qumode: >>> hl.CR.compute_fock_matrix((2, 3), 0.5) array([[1. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0.9689-0.2474j, 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0.8776-0.4794j, 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 1. -0.j , 0. -0.j , 0. -0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 0. -0.j , 0.9689+0.2474j, 0. -0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 0. -0.j , 0. -0.j , 0.8776+0.4794j]]) If ``parameters`` are tensors of a deep learning framework, the returned matrix should also be of the same framework so that it can be used in differentiable computations. If the gate is nonparametric, this should return a NumPy ``ndarray``. Using the same example as above with a JAX array as a parameter, the resulting matrix is a differentiable JAX ``Array``: >>> hl.CR.compute_fock_matrix((2, 3), jnp.array(0.5)) Array([[1. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0.9689-0.2474j, 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0.8776-0.4794j, 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 1. -0.j , 0. -0.j , 0. -0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 0. -0.j , 0.9689+0.2474j, 0. -0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 0. -0.j , 0. -0.j , 0.8776+0.4794j]], dtype=complex128) :param wire_dims: The dimension of each wire in the order of ``self.wires`` :param \*params: The parameters of the gate :param \*\*hyperparams: The hyperparameters of the gate :returns: The Fock dense matrix representation of the gate, matching the interface of the parameters. .. py:method:: fock_matrix(wire_dims, wire_order = None) Computes the dense Fock matrix representation of the gate The matrix returned has shape ``(d, d)`` where ``d`` is the total dimension of the Hilbert space, as determined from ``wire_dims``. **Details**: Because operators are symbolic don't know about device-level truncation, the user must provide the ``wire_dims`` argument to specify the dimension of each wire. Qubits should be explicitly set to dimension ``2``. An example using the :math:`R` gate with a Fock truncation of 3: >>> hl.R(0.123, wires=0).fock_matrix({0: 3}) array([[1. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0.9924-0.1227j, 0. +0.j ], [0. +0.j , 0. +0.j , 0.9699-0.2435j]]) If ``wire_order`` is not provided, it defaults to ``self.wires``. If the order of the wires in ``wire_order`` differs from ``self.wires``, the method automatically expands the matrix to the full Hilbert space and permutes the wires as necessary to match the requested order. For example, defining the :math:`R` gate to act on a composite qubit-qumode system with the qubit as wire 0 and the qumode as wire 1, the matrix will be expanded as :math:`I_2 \otimes R`: >>> hl.R(0.123, wires=1).fock_matrix({0: 2, 1: 3}, wire_order=(0, 1)) array([[1. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0.9924-0.1227j, 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0.9699-0.2435j, 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 1. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0.9924-0.1227j, 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0.9699-0.2435j]]) Passing a different ``wire_order`` returns a permuted matrix :math:`R \otimes I_2`: >>> hl.R(0.123, wires=1).fock_matrix({0: 2, 1: 3}, wire_order=(1, 0)) array([[1. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 1. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0.9924-0.1227j, 0. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 0.9924-0.1227j, 0. +0.j , 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0.9699-0.2435j, 0. +0.j ], [0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0. +0.j , 0.9699-0.2435j]]) If the gate is nonparametric, the returned matrix will be a NumPy ``ndarray``, but if the gate has parameters and they are tensors of a deep learning framework, the returned matrix will be of the same framework. This function is compatible with automatic differentiation and ``@jax.jit`` compilation provided the underlying ``compute_fock_matrix`` implementation is as well. An example using JAX: >>> hl.R(jnp.array(0.123), wires=0).fock_matrix({0: 3}) Array([[1. +0.j , 0. +0.j , 0. +0.j ], [0. +0.j , 0.9924-0.1227j, 0. +0.j ], [0. +0.j , 0. +0.j , 0.9699-0.2435j]], dtype=complex128, weak_type=True) Developers of new gates should prefer to implement ``compute_fock_matrix``.