diff --git a/pyproject.toml b/pyproject.toml index 82ccb3a..e823371 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "qml-essentials" -version = "0.1.15" +version = "0.1.16" description = "" authors = ["Melvin Strobl ", "Maja Franz "] readme = "README.md" diff --git a/qml_essentials/entanglement.py b/qml_essentials/entanglement.py index 1bb68f6..ab5bdf1 100644 --- a/qml_essentials/entanglement.py +++ b/qml_essentials/entanglement.py @@ -12,7 +12,10 @@ class Entanglement: @staticmethod def meyer_wallach( - model: Model, n_samples: int, seed: Optional[int], **kwargs: Any # type: ignore + model: Model, + n_samples: Optional[int | None], + seed: Optional[int], + **kwargs: Any, ) -> float: """ Calculates the entangling capacity of a given quantum circuit @@ -21,6 +24,7 @@ def meyer_wallach( Args: model (Callable): Function that models the quantum circuit. n_samples (int): Number of samples per qubit. + If None or < 0, the current parameters of the model are used seed (Optional[int]): Seed for the random number generator. kwargs (Any): Additional keyword arguments for the model function. @@ -29,26 +33,31 @@ def meyer_wallach( to be between 0.0 and 1.0. """ rng = np.random.default_rng(seed) - if n_samples > 0: + if n_samples is not None and n_samples > 0: assert seed is not None, "Seed must be provided when samples > 0" # TODO: maybe switch to JAX rng model.initialize_params(rng=rng, repeat=n_samples) + params = model.params else: if seed is not None: log.warning("Seed is ignored when samples is 0") - n_samples = 1 - model.initialize_params(rng=rng, repeat=1) - samples = model.params.shape[-1] - mw_measure = np.zeros(samples, dtype=complex) + if len(model.params.shape) <= 2: + params = model.params.reshape(*model.params.shape, 1) + else: + log.info(f"Using sample size of model params: {model.params.shape[-1]}") + params = model.params + + n_samples = params.shape[-1] + mw_measure = np.zeros(n_samples, dtype=complex) qb = list(range(model.n_qubits)) # TODO: vectorize in future iterations - for i in range(samples): + for i in range(n_samples): # implicitly set input to none in case it's not needed kwargs.setdefault("inputs", None) # explicitly set execution type because everything else won't work - U = model(params=model.params[:, :, i], execution_type="density", **kwargs) + U = model(params=params[:, :, i], execution_type="density", **kwargs) entropy = 0 @@ -58,7 +67,7 @@ def meyer_wallach( mw_measure[i] = 1 - entropy / model.n_qubits - mw = 2 * np.sum(mw_measure.real) / samples + mw = 2 * np.sum(mw_measure.real) / n_samples # catch floating point errors entangling_capability = min(max(mw, 0.0), 1.0) diff --git a/qml_essentials/model.py b/qml_essentials/model.py index 2030dec..e935868 100644 --- a/qml_essentials/model.py +++ b/qml_essentials/model.py @@ -4,6 +4,7 @@ import hashlib import os import warnings +from autograd.numpy import numpy_boxes from qml_essentials.ansaetze import Ansaetze, Circuit @@ -441,8 +442,8 @@ def __str__(self) -> str: def __call__( self, - params: np.ndarray, - inputs: np.ndarray, + params: Optional[np.ndarray] = None, + inputs: Optional[np.ndarray] = None, noise_params: Optional[Dict[str, float]] = None, cache: Optional[bool] = False, execution_type: Optional[str] = None, @@ -452,9 +453,11 @@ def __call__( Perform a forward pass of the quantum circuit. Args: - params (np.ndarray): Weight vector of shape + params (Optional[np.ndarray]): Weight vector of shape [n_layers, n_qubits*n_params_per_layer]. - inputs (np.ndarray): Input vector of shape [1]. + If None, model internal parameters are used. + inputs (Optional[np.ndarray]): Input vector of shape [1]. + If None, zeros are used. noise_params (Optional[Dict[str, float]], optional): The noise parameters. Defaults to None which results in the last set noise parameters being used. @@ -488,8 +491,8 @@ def __call__( def _forward( self, - params: np.ndarray, - inputs: np.ndarray, + params: Optional[np.ndarray] = None, + inputs: Optional[np.ndarray] = None, noise_params: Optional[Dict[str, float]] = None, cache: Optional[bool] = False, execution_type: Optional[str] = None, @@ -499,9 +502,11 @@ def _forward( Perform a forward pass of the quantum circuit. Args: - params (np.ndarray): Weight vector of shape + params (Optional[np.ndarray]): Weight vector of shape [n_layers, n_qubits*n_params_per_layer]. - inputs (np.ndarray): Input vector of shape [1]. + If None, model internal parameters are used. + inputs (Optional[np.ndarray]): Input vector of shape [1]. + If None, zeros are used. noise_params (Optional[Dict[str, float]], optional): The noise parameters. Defaults to None which results in the last set noise parameters being used. @@ -533,6 +538,14 @@ def _forward( if execution_type is not None: self.execution_type = execution_type + if params is None: + params = self.params + else: + if numpy_boxes.ArrayBox == type(params): + self.params = params._value + else: + self.params = params + # the qasm representation contains the bound parameters, # thus it is ok to hash that hs = hashlib.md5( @@ -542,7 +555,7 @@ def _forward( "n_layers": self.n_layers, "pqc": self.pqc.__class__.__name__, "dru": self.data_reupload, - "params": params, + "params": self.params, # use safe-params "noise_params": self.noise_params, "execution_type": self.execution_type, "inputs": inputs, @@ -568,7 +581,7 @@ def _forward( # if density matrix requested or noise params used if self.execution_type == "density" or self.noise_params is not None: result = self.circuit_mixed( - params=params, + params=params, # use arraybox params inputs=inputs, ) else: @@ -578,7 +591,7 @@ def _forward( ) else: result = self.circuit( - params=params, + params=params, # use arraybox params inputs=inputs, ) diff --git a/tests/test_model.py b/tests/test_model.py index 0333f9e..c451d01 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -81,6 +81,14 @@ def test_parameters() -> None: }, ] + # Test the most minimal call + model = Model( + n_qubits=2, + n_layers=1, + circuit_type="Circuit_19", + ) + assert (model() == model(model.params)).all() + for test_case in test_cases: model = Model( n_qubits=2, @@ -645,3 +653,18 @@ def test_parity() -> None: assert not np.allclose( result_a, result_b ), f"Models should be different! Got {result_a} and {result_b}" + + +@pytest.mark.smoketest +def test_params_store() -> None: + model = Model( + n_qubits=2, + n_layers=1, + circuit_type="Circuit_1", + ) + opt = qml.AdamOptimizer(stepsize=0.01) + + def cost(params): + return model(params=params, inputs=np.array([0])).mean()._value + + params, cost = opt.step_and_cost(cost, model.params)