Repository: PennyLaneAI/pennylane
Pull request #6863 (2025-01-20 18:59:19)

`StatePrep` accepts sparse matrices as input
**Context:**

To make PennyLane use sparse matrics, at the atomic level we need to provide at least two features:
 - states in PL can start from sparse
 - necessary operators (e.g. `QubitUnitary`) have their sprase version along with the default sparse-sparse, sparse-dense multiplication
 
This Pull Request focus on fulfilling the first.

Note that up to v0.40, PL implements state initialization using class `StatePrep` from `ops/qubit/state_preparation.py`. Conceptually, we treated the state preparation as a generalized operation here, although unlike many other operators which are typically differentiable, `StatePrep` shall be expected as non-differentiable in most of the practical context.

Recall the interface of `StatePrep`, 
```python
class StatePrep(state, wires, pad_with=None, normalize=False, validate_norm=True)
```
where `state` is the input values for the initalization, `wires` is the total number of qubits of the final state, `pad_with` refers to a specific feature with which the user can use a shorter `state` of length less than `2**wires` and PL will automatically fill the remaining component with the given `pad_with`. `normalize` and `validate_norm` are standard controls for the norm preprocessing of a state-vector.

Revisiting this, we would like to support specific input type of `state` as sparse matrices. Specifically, for simplicity of the first feature, we just use `csr_matrix` from `SciPy.sparse` (Compressed Sparse Row~Column~ format; check https://docs.scipy.org/doc/scipy/reference/sparse.html for futher details). Meanwhile, note that a nonzero `pad_with` is clearly against the idea of sparsity since it makes the state dense, so we will disable this option if the previous sparsity of `state` is given by a user. 

**Description of the Change:**
 - Introduced extra type hint and conditional branch inside the initialization of `StatePrep`, implementing it overloading
 - Implemented the private method `_sparse_statevec_permute` to align with the original default `StatePrep.state_vector` where the input states can be embedded into the given larger wire sets with permuted wire order.
 - [x] ~check the parent classes for other supported operations, e.g. equal~ updated: out-of-scope and not necesary. Leave it to future improvement.
 - [x] improvement of the permute algorithm logic 

**Benefits:**
 - Extend the support range of `StatePrep` by overloading it with new features of sparse matrices.

**Possible Drawbacks:**
 - Do not support batched states for simplicity of our MVP implementation. Open for discussion whether or not this should be included as one of the necessary requirements.
 - Documentations remain missing for now, because `StatePrep` is actually facing public users, and it does not make sense to add notes in docs before the successful implementation of the whole workflow. They will be added in the future PR that complets the end-to-end sparse-matrix feature of PennyLane (i.e., at least a measurement can be done, which requires modification on the qubit simulator workflow)

**Related GitHub Issues:**
[sc-82067]

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6863?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: Patch coverage is `98.18182%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 99.54%. Comparing base [(`afec979`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/afec97918b5ca018145d40921bd4bd5d44b6c6ae?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`c859837`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/c85983761dee4c0a63e620bb83933bbad4ea72d4?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 4 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6863?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/ops/qubit/state\_preparation.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6863?src=pr&el=tree&filepath=pennylane%2Fops%2Fqubit%2Fstate_preparation.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9zdGF0ZV9wcmVwYXJhdGlvbi5weQ==) | 98.18% | [1 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6863?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #6863      +/-   ##
==========================================
- Coverage   99.54%   99.54%   -0.01%     
==========================================
  Files         477      478       +1     
  Lines       45246    45369     +123     
==========================================
+ Hits        45042    45164     +122     
- Misses        204      205       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/6863?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

JerryChen97: I'll udpate changelog if we are OK with the current implementation, or until we converge at the final implementation
isaacdevlugt: > @isaacdevlugt Hi Isaac I wonder what's your opinion on the public documentation of this PR to merge into master branch? I left arguments in the description/drawbacks

Thanks @JerryChen97 😃! Indeed, this is a bit of a grey area. I would still update the `Parameters` section in the docstring to have:

```
state (array[complex] or csr_matrix (or whatever the formal type name is for that)) – the state vector to prepare
```

While we're here, let's move everything below `Differentiating with respect to the state` into a `.. usage-details::` section! 

![Screenshot 2025-01-28 at 3 42 21 PM](https://github.com/user-attachments/assets/a06ef292-7f10-4816-afaf-cc1f78ce1f04)

It can't hurt to have a quick example showing sparse-state usage in the Usage Details section, either. E.g., 

```pycon
>>> import scipy as sp
>>> init_state = sp.sparse.csr_matrix([0, 0, 1, 0])
>>> qsv_op = qml.StatePrep(init_state, wires=[1, 2])
>>> ket = qsv_op.state_vector(wire_order=wire_order)
>>> ket
<Compressed Sparse Row sparse matrix of dtype 'float64'
	with 1 stored elements and shape (1, 4)>
```
isaacdevlugt: Also @JerryChen97 don't forget the changelog! 📄 

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 90123cc5f..adb442ed2 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -6,12 +6,28 @@
 
 <h3>Improvements 🛠</h3>
 
+* `qml.StatePrep` now accepts sparse state vectors. Users can create `StatePrep` using `scipy.sparse.csr_matrix`. Note that non-zero `pad_with` is forbidden.
+  [(#6863)](https://github.com/PennyLaneAI/pennylane/pull/6863)
+
+  ```pycon
+  >>> import scipy as sp
+  >>> init_state = sp.sparse.csr_matrix([0, 0, 1, 0])
+  >>> qsv_op = qml.StatePrep(init_state, wires=[1, 2])
+  >>> wire_order = [0, 1, 2]
+  >>> ket = qsv_op.state_vector(wire_order=wire_order)
+  >>> print(ket)
+  <Compressed Sparse Row sparse matrix of dtype 'float64'
+         with 1 stored elements and shape (1, 8)>
+    Coords        Values
+    (0, 2)        1.0
+  ```
+
 * A `RuntimeWarning` is now raised by `qml.QNode` and `qml.execute` if executing JAX workflows and the installed version of JAX
   is greater than `0.4.28`.
   [(#6864)](https://github.com/PennyLaneAI/pennylane/pull/6864)
 
 * Python control flow (`if/else`, `for`, `while`) is now supported when program capture is enabled by setting 
-  autograph=True` at the QNode level. 
+  `autograph=True` at the QNode level. 
   [(#6837)](https://github.com/PennyLaneAI/pennylane/pull/6837)
 
   ```python
@@ -28,6 +44,7 @@
               qml.RX(1,i)
       return qml.state()
   ```
+
   ```pycon
   >>> print(qml.draw(circuit)(num_loops=3))
   0: ──H────────┤  State
@@ -50,6 +67,7 @@
       qml.RX(x, 0)
       return qml.expval(qml.Z(0))
   ```
+
   ```pycon
   >>> config = qml.workflow.construct_execution_config(circuit)(1)
   >>> pprint.pprint(config)
@@ -91,7 +109,7 @@
 
   its settings can be modified with `update`, which returns a new `QNode` object. Here is an example
   of updating a QNode's `diff_method`:
-  
+
   ```pycon
   >>> print(circuit.diff_method)
   best
@@ -110,7 +128,7 @@
 * The coefficients of observables now have improved differentiability.
   [(#6598)](https://github.com/PennyLaneAI/pennylane/pull/6598)
 
-* An empty basis set in `qml.compile` is now recognized as valid, resulting in decomposition of all operators that can be decomposed. 
+* An empty basis set in `qml.compile` is now recognized as valid, resulting in decomposition of all operators that can be decomposed.
    [(#6821)](https://github.com/PennyLaneAI/pennylane/pull/6821)
 
 * An informative error is raised when a `QNode` with `diff_method=None` is differentiated.
@@ -145,21 +163,21 @@
 * `qml.execute` now has a collection of keyword-only arguments.
   [(#6598)](https://github.com/PennyLaneAI/pennylane/pull/6598)
 
-* The ``decomp_depth`` argument in :func:`~pennylane.transforms.set_decomposition` has been removed. 
+* The ``decomp_depth`` argument in :func:`~pennylane.transforms.set_decomposition` has been removed.
   [(#6824)](https://github.com/PennyLaneAI/pennylane/pull/6824)
 
-* The ``max_expansion`` argument in :func:`~pennylane.devices.preprocess.decompose` has been removed. 
+* The ``max_expansion`` argument in :func:`~pennylane.devices.preprocess.decompose` has been removed.
   [(#6824)](https://github.com/PennyLaneAI/pennylane/pull/6824)
 
-* The ``tape`` and ``qtape`` properties of ``QNode`` have been removed. 
+* The ``tape`` and ``qtape`` properties of ``QNode`` have been removed.
   Instead, use the ``qml.workflow.construct_tape`` function.
   [(#6825)](https://github.com/PennyLaneAI/pennylane/pull/6825)
 
 * The ``gradient_fn`` keyword argument to ``qml.execute`` has been removed. Instead, it has been replaced with ``diff_method``.
   [(#6830)](https://github.com/PennyLaneAI/pennylane/pull/6830)
   
-* The ``QNode.get_best_method`` and ``QNode.best_method_str`` methods have been removed. 
-  Instead, use the ``qml.workflow.get_best_diff_method`` function. 
+* The ``QNode.get_best_method`` and ``QNode.best_method_str`` methods have been removed.
+  Instead, use the ``qml.workflow.get_best_diff_method`` function.
   [(#6823)](https://github.com/PennyLaneAI/pennylane/pull/6823)
 
 * The `output_dim` property of `qml.tape.QuantumScript` has been removed. Instead, use method `shape` of `QuantumScript` or `MeasurementProcess` to get the same information.
@@ -177,7 +195,7 @@
   Instead, use the `wires` argument as the second positional argument.
   [(#6839)](https://github.com/PennyLaneAI/pennylane/pull/6839)
 
-* The `mcm_method` keyword in `qml.execute` has been deprecated. 
+* The `mcm_method` keyword in `qml.execute` has been deprecated.
   Instead, use the ``mcm_method`` and ``postselect_mode`` arguments.
   [(#6807)](https://github.com/PennyLaneAI/pennylane/pull/6807)
 
@@ -223,8 +241,8 @@
 
 <h3>Documentation 📝</h3>
 
-* The docstrings for `qml.unary_mapping`, `qml.binary_mapping`, `qml.christiansen_mapping`, 
-  `qml.qchem.localize_normal_modes`, and `qml.qchem.VibrationalPES` have been updated to include better 
+* The docstrings for `qml.unary_mapping`, `qml.binary_mapping`, `qml.christiansen_mapping`,
+  `qml.qchem.localize_normal_modes`, and `qml.qchem.VibrationalPES` have been updated to include better
   code examples.
   [(#6717)](https://github.com/PennyLaneAI/pennylane/pull/6717)
 
diff --git a/pennylane/ops/qubit/state_preparation.py b/pennylane/ops/qubit/state_preparation.py
index a7c79dded..342267d79 100644
--- a/pennylane/ops/qubit/state_preparation.py
+++ b/pennylane/ops/qubit/state_preparation.py
@@ -16,9 +16,12 @@ This submodule contains the discrete-variable quantum operations concerned
 with preparing a certain state on the device.
 """
 # pylint:disable=too-many-branches,abstract-method,arguments-differ,protected-access,no-member
-from typing import Optional
+from typing import Optional, Union
+from warnings import warn
 
 import numpy as np
+import scipy as sp
+from scipy.sparse import csr_matrix
 
 import pennylane as qml
 from pennylane import math
@@ -177,13 +180,7 @@ class BasisState(StatePrepBase):
 
 class StatePrep(StatePrepBase):
     r"""StatePrep(state, wires, pad_with = None, normalize = False, validate_norm = True)
-    Prepare subsystems using the given ket vector in the computational basis.
-
-    By setting ``pad_with`` to a real or complex number, ``state`` is automatically padded to dimension
-    :math:`2^n` where :math:`n` is the number of qubits used in the template.
-
-    To represent a valid quantum state vector, the L2-norm of ``state`` must be one.
-    The argument ``normalize`` can be set to ``True`` to automatically normalize the state.
+    Prepare subsystems using a state vector in the computational basis.
 
     **Details:**
 
@@ -204,10 +201,12 @@ class StatePrep(StatePrepBase):
         as :math:`U|0\rangle = |\psi\rangle`
 
     Args:
-        state (array[complex]): the state vector to prepare
+        state (array[complex] or csr_matrix): the state vector to prepare
         wires (Sequence[int] or int): the wire(s) the operation acts on
-        pad_with (float or complex):  if not None, the input is padded with this constant to size :math:`2^n`
-        normalize (bool): whether to normalize the state vector
+        pad_with (float or complex): if not ``None``, ``state`` is padded with this constant to be of size :math:`2^n`, where
+            :math:`n` is the number of wires.
+        normalize (bool): whether to normalize the state vector. To represent a valid quantum state vector, the L2-norm
+            of ``state`` must be one. The argument ``normalize`` can be set to ``True`` to normalize the state automatically.
         id (str): custom label given to an operator instance,
             can be useful for some applications where the instance has to be identified
         validate_norm (bool): whether to validate the norm of the input state
@@ -236,10 +235,13 @@ class StatePrep(StatePrepBase):
         >>> state
         tensor([0.5+0.j, 0.5+0.j, 0.5+0.j, 0.5+0.j], requires_grad=True)
 
+    .. details::
+        :title: Usage Details
+
         **Differentiating with respect to the state**
 
         Due to non-trivial classical processing to construct the state preparation circuit,
-        the state argument is in general **not differentiable**.
+        the state argument is, in general, **not differentiable**.
 
         **Normalization**
 
@@ -277,6 +279,32 @@ class StatePrep(StatePrepBase):
         >>> state
         tensor([0.70710678+0.j, 0.70710678+0.j, 0.        +0.j, 0.        +0.j], requires_grad=True)
 
+        **Sparse state input**
+        `state` can also be provided as a sparse matrix.  The state will be implicitly
+        zero-padded to the full Hilbert space dimension.
+
+        .. code-block:: pycon
+
+            >>> import scipy as sp
+            >>> init_state = sp.sparse.csr_matrix([0, 0, 1, 0])
+            >>> qsv_op = qml.StatePrep(init_state, wires=[1, 2])
+            >>> wire_order = [0, 1, 2]
+            >>> ket = qsv_op.state_vector(wire_order=wire_order)
+            >>> print(ket)  # Sparse representation
+            <Compressed Sparse Row sparse matrix of dtype 'float64'
+                    with 1 stored elements and shape (1, 8)>
+              Coords        Values
+              (0, 2)        1.0
+            >>> print(ket.toarray().flatten())  # Dense representation
+            [0. 0. 1. 0. 0. 0. 0. 0.]
+
+            # Normalization also works with sparse inputs:
+            >>> init_state_sparse = sp.sparse.csr_matrix([1, 1, 1, 1]) # Unnormalized
+            >>> qsv_op_norm = qml.StatePrep(init_state_sparse, wires=range(2), normalize=True)
+            >>> ket_norm = qsv_op_norm.state_vector()
+            >>> print(ket_norm.toarray().flatten()) # Normalized dense representation
+            [0.5 0.5 0.5 0.5]
+
 
     """
 
@@ -290,15 +318,19 @@ class StatePrep(StatePrepBase):
     # pylint: disable=too-many-arguments,too-many-positional-arguments
     def __init__(
         self,
-        state: TensorLike,
+        state: Union[TensorLike, csr_matrix],
         wires: WiresLike,
         pad_with=None,
         normalize=False,
         id: Optional[str] = None,
         validate_norm: bool = True,
     ):
-
-        state = self._preprocess(state, wires, pad_with, normalize, validate_norm)
+        self.is_sparse = False
+        if isinstance(state, csr_matrix):
+            state = self._preprocess_csr(state, wires, None, normalize, validate_norm)
+            self.is_sparse = True
+        else:
+            state = self._preprocess(state, wires, pad_with, normalize, validate_norm)
 
         self._hyperparameters = {
             "pad_with": pad_with,
@@ -345,6 +377,14 @@ class StatePrep(StatePrepBase):
         return cls(*data, **dict(metadata[0]), wires=metadata[1])
 
     def state_vector(self, wire_order: Optional[WiresLike] = None):
+
+        if self.is_sparse:
+            return (
+                _sparse_statevec_permute_and_embed(self.parameters[0], self.wires, wire_order)
+                if wire_order
+                else self.parameters[0]
+            )
+
         num_op_wires = len(self.wires)
         op_vector_shape = (-1,) + (2,) * num_op_wires if self.batch_size else (2,) * num_op_wires
         op_vector = math.reshape(self.parameters[0], op_vector_shape)
@@ -440,6 +480,59 @@ class StatePrep(StatePrepBase):
 
         return state
 
+    @staticmethod
+    def _preprocess_csr(state, wires, pad_with, normalize, validate_norm):
+        """Validate and pre-process inputs as follows:
+
+        * If the state is batched, the following processing is applied to each state set in the batch.
+        * Check that the state tensor is one-dimensional.
+        * pad_with has to be None.
+        * If normalize is false, check that the last dimension of the state is normalized to one. Else, normalize the
+          state tensor.
+        """
+
+        if pad_with:
+            raise ValueError("Non-zero Padding is not supported for sparse states")
+        shape = state.shape
+
+        # Check shape. Note that csr_matrix is always 2D; scipy should have already checked that the input is a 2D array
+        if shape[0] != 1:
+            raise ValueError(f"State must be a one-dimensional tensor; got shape {shape}.")
+
+        n_states = shape[-1]
+        dim = 2 ** len(Wires(wires))
+        if n_states > dim:
+            raise ValueError(
+                f"State must be of length {dim} or smaller to be padded; got length {n_states}."
+            )
+        if n_states < dim:
+            warn(
+                f"State must be of length {dim}; got length {n_states}. "
+                f"Automatically padding with zeros.",
+                UserWarning,
+            )
+            # pad a csr_matrix with zeros
+            state.resize((1, dim))
+
+        if not validate_norm:
+            return state
+
+        # normalize
+        if np.issubdtype(state.dtype, np.integer):
+            state = state.astype(float)
+
+        norm = sp.sparse.linalg.norm(state)
+
+        if normalize:
+            state /= norm
+
+        elif not math.allclose(norm, 1.0, atol=TOLERANCE):
+            raise ValueError(
+                f"The state must be a vector of norm 1.0; got norm {norm}. "
+                "Use 'normalize=True' to automatically normalize."
+            )
+        return state
+
 
 class QubitDensityMatrix(Operation):
     r"""QubitDensityMatrix(state, wires)
@@ -497,3 +590,44 @@ class QubitDensityMatrix(Operation):
     """int: Number of trainable parameters that the operator depends on."""
 
     grad_method = None
+
+
+def _sparse_statevec_permute_and_embed(
+    state: csr_matrix, wires: list, wire_order: list
+) -> csr_matrix:
+    """Permutes the wires of a statevector represented as a scipy.sparse.csr_matrix. If `wire_order` contains `wires`, then embed the `state` with corresponding orders, padding with bit 0 on other wires.
+
+    Args:
+        state (csr_matrix): the input statevector
+        wires (Iterable[int]): the wires of the input statevector
+        wire_order (Iterable[int]): the wires of the output statevector. E.g., [0, 2, 1] means the permutation of wires 0, 1, 2 to 0, 2, 1. wires=[2, 1] and wire_order=[1, 0, 2] means embedding the input state in a permuted order.
+
+    Returns:
+        csr_matrix: the permuted statevector
+    """
+    wires = Wires(wires)
+    wire_order = Wires(wire_order) if wire_order else wires
+
+    if not wire_order.contains_wires(wires):
+        raise WireError(
+            f"wire_order must contain all wires. Got wires {wires} and wire_order {wire_order}"
+        )
+
+    if wires == wire_order:
+        return state
+
+    index_map = _build_index_map(wires, wire_order)
+    perm_pos = index_map[state.indices]
+    new_csr = csr_matrix((state.data, perm_pos, state.indptr), shape=(1, 2 ** len(wire_order)))
+    return new_csr
+
+
+def _build_index_map(wires, wire_order):
+    n_wires = len(wires)
+    index_map = np.zeros(2**n_wires, dtype=int)
+    for pos in range(2**n_wires):
+        pos_bin = format(pos, f"0{n_wires}b")
+        wire_values_map = {wire: pos_bin[i] for i, wire in enumerate(wires)}
+        pos_bin_perm = [wire_values_map[wire] if wire in wires else "0" for wire in wire_order]
+        index_map[pos] = int("".join(pos_bin_perm), 2)
+    return index_map


Test patch:
diff --git a/tests/ops/qubit/test_state_prep.py b/tests/ops/qubit/test_state_prep.py
index 23e202e51..1c6975e64 100644
--- a/tests/ops/qubit/test_state_prep.py
+++ b/tests/ops/qubit/test_state_prep.py
@@ -14,8 +14,10 @@
 """
 Unit tests for the available qubit state preparation operations.
 """
+# pylint: disable=protected-access
 import numpy as np
 import pytest
+import scipy as sp
 
 import pennylane as qml
 from pennylane.wires import WireError
@@ -406,3 +408,88 @@ class TestStateVector:
             ValueError, match=r"State must be of length 2; got length 1 \(state=\[0\]\)."
         ):
             _ = qml.BasisState([0], wires=[0, 1])
+
+
+class TestSparseStateVector:
+    """Test the sparse_state_vector() method of various state-prep operations."""
+
+    @pytest.mark.parametrize(
+        "num_wires,wire_order,one_position",
+        [
+            (2, None, (1, 0)),
+            (2, [1, 2], (1, 0)),
+            (3, [0, 1, 2], (0, 1, 0)),
+            (3, ["a", 1, 2], (0, 1, 0)),
+            (3, [1, 2, 0], (1, 0, 0)),
+            (3, [1, 2, "a"], (1, 0, 0)),
+            (3, [2, 1, 0], (0, 1, 0)),
+            (4, [3, 2, 0, 1], (0, 0, 0, 1)),
+        ],
+    )
+    def test_StatePrep_sparse_state_vector(self, num_wires, wire_order, one_position):
+        """Tests that StatePrep sparse_state_vector returns kets as expected."""
+        init_state = sp.sparse.csr_matrix([0, 0, 1, 0])
+        qsv_op = qml.StatePrep(init_state, wires=[1, 2])
+        ket = qsv_op.state_vector(wire_order=wire_order)
+        # Convert one position from binary to integer
+        one_position = int("".join([str(i) for i in one_position]), 2)
+        assert ket.shape == (1, 2**num_wires)
+        assert ket[0, one_position] == 1
+        ket[0, one_position] = 0
+        assert ket.count_nonzero() == 0
+
+    def test_preprocess_nonzero_padding_unsupported(self):
+        """Test that sparse_state_vector does not support padding with nonzero values."""
+        init_state = sp.sparse.csr_matrix([0, 0, 1, 0])
+        with pytest.raises(ValueError, match="Non-zero Padding is not supported"):
+            qml.StatePrep._preprocess_csr(
+                init_state, wires=[1, 2], pad_with=1, normalize=False, validate_norm=False
+            )
+
+    def test_preprocess_one_dimensional_tensor(self):
+        """Test that the state tensor is one-dimensional."""
+        init_state = sp.sparse.csr_matrix([[0, 0], [1, 0]])
+        with pytest.raises(ValueError, match="State must be a one-dimensional tensor"):
+            qml.StatePrep._preprocess_csr(
+                init_state, wires=[1, 2], pad_with=None, normalize=False, validate_norm=False
+            )
+
+    def test_preprocess_length_of_tensor(self):
+        """Test that the state tensor is one-dimensional."""
+        init_state = sp.sparse.csr_matrix([0, 0, 2, 0, 1])
+        with pytest.raises(ValueError, match="State must be of length"):
+            qml.StatePrep._preprocess_csr(
+                init_state, wires=[1, 2], pad_with=None, normalize=False, validate_norm=False
+            )
+
+    def test_preprocess_auto_padding_tensor(self):
+        """Test that the state tensor is one-dimensional."""
+        init_state = sp.sparse.csr_matrix([0, 0, 2])
+        with pytest.warns(UserWarning, match="Automatically padding with zeros"):
+            state = qml.StatePrep._preprocess_csr(
+                init_state, wires=[1, 2], pad_with=None, normalize=False, validate_norm=False
+            )
+            assert state.shape == (1, 4), f"Expected shape (1, 4), got {state.shape}"
+
+    def test_preprocess_normalize_false(self):
+        """Test that the state tensor is normalized to one if normalize is False."""
+        init_state = sp.sparse.csr_matrix([0, 0, 2, 0])
+        with pytest.raises(ValueError, match="The state must be a vector of norm 1.0; got norm"):
+            qml.StatePrep._preprocess_csr(
+                init_state, wires=[1, 2], pad_with=None, normalize=False, validate_norm=True
+            )
+
+    def test_preprocess_normalize_true(self):
+        """Test that the state tensor is normalized if normalize is True."""
+        init_state = sp.sparse.csr_matrix([0, 0, 2, 0])
+        processed_state = qml.StatePrep._preprocess_csr(
+            init_state, wires=[1, 2], pad_with=None, normalize=True, validate_norm=True
+        )
+        norm = sp.sparse.linalg.norm(processed_state)
+        assert qml.math.allclose(norm, 1.0)
+
+    def test_StatePrep_sparse_state_vector_bad_wire_order(self):
+        """Tests that the provided wire_order must contain the wires in the operation."""
+        qsv_op = qml.StatePrep(sp.sparse.csr_matrix([0, 0, 0, 1]), wires=[0, 1])
+        with pytest.raises(WireError, match="wire_order must contain all wires"):
+            qsv_op.state_vector(wire_order=[1, 2])