Source code for qiskit_nature.second_q.circuit.library.initial_states.vscf

# This code is part of a Qiskit project.
#
# (C) Copyright IBM 2020, 2023.
#
# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
#
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Initial state for vibrational modes."""

from __future__ import annotations

import logging

import numpy as np

from qiskit import QuantumRegister
from qiskit.circuit.library import BlueprintCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_nature.second_q.mappers import DirectMapper
from qiskit_nature.second_q.mappers import QubitMapper, TaperedQubitMapper
from qiskit_nature.second_q.operators import VibrationalOp

logger = logging.getLogger(__name__)


[docs]class VSCF(BlueprintCircuit): r"""Initial state for vibrational modes. Creates an occupation number vector as defined in [1]. As example, for 2 modes with 4 modals per mode it creates: :math:`|1000 1000\rangle`. References: [1] Ollitrault Pauline J., Chemical science 11 (2020): 6842-6855. """ def __init__( self, num_modals: list[int] | None = None, qubit_mapper: QubitMapper | None = None, ) -> None: # pylint: disable=unused-argument """ Args: num_modals: Is a list defining the number of modals per mode. E.g. for a 3 modes system with 4 modals per mode num_modals = [4,4,4] qubit_mapper: a QubitMapper. This argument is currently being ignored because only a single use-case is supported at the time of release: that of the :class:`.DirectMapper`. However, for future-compatibility of this functions signature, the argument has already been inserted. """ super().__init__() self._num_modals = num_modals self._qubit_mapper = qubit_mapper self._bitstr: list[bool] | None = None self.qubit_mapper = DirectMapper() if qubit_mapper is None else qubit_mapper @property def qubit_mapper(self) -> QubitMapper | None: """The qubit mapper.""" return self._qubit_mapper @qubit_mapper.setter def qubit_mapper(self, mapper: QubitMapper | None) -> None: """Sets the qubit mapper.""" self._invalidate() if isinstance(mapper, TaperedQubitMapper): # we also include the TaperedQubitMapper here, purely for the check done below mapper = mapper.mapper if not isinstance(mapper, DirectMapper): logger.warning( "The only supported `QubitMapper` for this application are those based on the " "`DirectMapper`. However you specified %s as an input, which will be ignored until " "more variants will be supported.", type(mapper), ) mapper = DirectMapper() self._qubit_mapper = mapper self._reset_register() @property def num_modals(self) -> list[int]: """The number of modals per mode.""" return self._num_modals @num_modals.setter def num_modals(self, num_modals: list[int]) -> None: """Sets the number of modals.""" self._invalidate() self._num_modals = num_modals self._reset_register() def _check_configuration(self, raise_on_failure: bool = True) -> bool: """Check if the configuration of the VSCF class is valid. Args: raise_on_failure: Whether to raise on failure. Returns: True, if the configuration is valid and the circuit can be constructed. Otherwise an ValueError or TypeError is raised. Raises: ValueError: If the number of modals per mode is not specified. ValueError: If any of the number of modals is less than zero. ValueError: If the qubit mapper is not specified. """ if self.num_modals is None: if raise_on_failure: raise ValueError("The number of modals cannot be 'None'.") return False if any(n < 0 for n in self.num_modals): if raise_on_failure: raise ValueError( f"The number of modals cannot be smaller than 0 was {self.num_modals}." ) return False if self.qubit_mapper is None: if raise_on_failure: raise ValueError("The qubit mapper cannot be `None`.") return False return True def _reset_register(self): """Reset the register and recompute the mapped VSCF bitstring.""" self.qregs = [] self._bitstr = None if self._check_configuration(raise_on_failure=False): self._bitstr = vscf_bitstring_mapped(self.num_modals, self.qubit_mapper) self.qregs = [QuantumRegister(len(self._bitstr), name="q")] def _build(self) -> None: """ Construct the VSCF initial state given its parameters. Returns: QuantumCircuit: a quantum circuit preparing the VSCF initial state given a number of modals per mode and a qubit mapper. """ if self._is_built: return super()._build() # Construct the circuit for bitstring. Since this is defined as an initial state # circuit its assumed that this is applied first to the qubits that are initialized to # the zero state. Hence we just need to account for all True entries and set those. if self._bitstr is not None: for i, bit in enumerate(self._bitstr): if bit: self.x(i)
def vscf_bitstring_mapped( num_modals: list[int], qubit_mapper: QubitMapper, ) -> list[bool]: # pylint: disable=unused-argument """Compute the bitstring representing the mapped VSCF initial state based on the given the number of modals per mode and qubit mapper. Args: num_modals: A list defining the number of modals per mode. E.g. for a 3 modes system with 4 modals per mode num_modals = [4,4,4]. qubit_mapper: A QubitMapper. Returns: The bitstring representing the mapped state of the VSCF initial state as array of bools. """ # get the bitstring encoding initial state bitstr = vscf_bitstring(num_modals) # encode the bitstring in a `VibrationalOp` bitstr_op = VibrationalOp( { " ".join( f"+_{VibrationalOp.build_dual_index(num_modals, idx)}" for idx, bit in enumerate(bitstr) if bit ): 1.0 }, num_modals=num_modals, ) # map the `VibrationalOp` to a qubit operator qubit_op: SparsePauliOp if isinstance(qubit_mapper, TaperedQubitMapper): # To avoid checking commutativity, we call the two methods separately. qubit_op = qubit_mapper.map_clifford(bitstr_op) qubit_op = qubit_mapper.taper_clifford(qubit_op, check_commutes=False) else: qubit_op = qubit_mapper.map(bitstr_op) # We check the mapped operator `x` part of the paulis because we want to have particles # i.e. True, where the initial state introduced a creation (`+`) operator. bits = [] for bit in qubit_op.paulis.x[0]: bits.append(bit) return bits def vscf_bitstring(num_modals: list[int]) -> list[bool]: """Compute the bitstring representing the VSCF initial state based on the modals per mode. Args: num_modals: Is a list defining the number of modals per mode. E.g. for a 3 modes system with 4 modals per mode num_modals = [4,4,4]. Returns: The bitstring representing the state of the VSCF state as array of bools. """ num_qubits = sum(num_modals) bitstr = np.zeros(num_qubits, bool) count = 0 for modal in num_modals: bitstr[count] = True count += modal return bitstr.tolist()