Repository: PennyLaneAI/pennylane
Pull request #3936 (2023-03-23 16:44:39)

Transmon interaction hardware compatible Hamiltonian
Builds on https://github.com/PennyLaneAI/pennylane/pull/3930 

In this PR we introduce
- [x] new dataclass `TransmonSettings` as well as the constructor `transmon_interaction` for an interacting transmon system.
- [x] new dataclass `RydbergSettings`
- [x] unified `drive` function to construct `HardwareHamiltonian`

Future PRs will introduce
- [ ] `rydberg_drive` with parameters `amplitude`, `phase` and `detuning`
- [ ] trainable interaction strength `g` (needs to change the default reordering function for calling Hardware Hamiltonians)
- [ ] ~~`transmon_drive` with parameters `amplitude`, `phase` and `drive_frequency`
(`detuning` and `drive_frequency` are both stored in the `HardwarePulse` as `frequency`, already introduced here)~~ update: trainable frequency _on hardware_ more complicated than expected. Gonna have to revisit this when adding explicit device support.

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.
Qottmann: Can be considered mostly done, all rydberg, transmon and hardware hamiltonian tests pass locally. Could do with few more unit/integration tests for combinations of drive and interaction terms
codecov[bot]: ## [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3936](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (a2231c5) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/340f1c8d2241d0f5947af87dd4565ea555dcbcbd?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (340f1c8) will **decrease** coverage by `0.02%`.
> The diff coverage is `94.44%`.

```diff
@@            Coverage Diff             @@
##           master    #3936      +/-   ##
==========================================
- Coverage   99.74%   99.72%   -0.02%     
==========================================
  Files         345      346       +1     
  Lines       30163    30226      +63     
==========================================
+ Hits        30085    30142      +57     
- Misses         78       84       +6     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/qubit/parametric\_ops\_multi\_qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9wYXJhbWV0cmljX29wc19tdWx0aV9xdWJpdC5weQ==) | `100.00% <ø> (ø)` | |
| [pennylane/pulse/parametrized\_evolution.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3B1bHNlL3BhcmFtZXRyaXplZF9ldm9sdXRpb24ucHk=) | `96.00% <50.00%> (-2.08%)` | :arrow_down: |
| [pennylane/pulse/transmon.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3B1bHNlL3RyYW5zbW9uLnB5) | `89.83% <89.83%> (ø)` | |
| [pennylane/devices/default\_qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdC5weQ==) | `99.69% <100.00%> (+<0.01%)` | :arrow_up: |
| [pennylane/measurements/sample.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmVtZW50cy9zYW1wbGUucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/op\_math/evolution.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2V2b2x1dGlvbi5weQ==) | `100.00% <100.00%> (+2.56%)` | :arrow_up: |
| [pennylane/ops/op\_math/exp.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2V4cC5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/pulse/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3B1bHNlL19faW5pdF9fLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/pulse/hardware\_hamiltonian.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3B1bHNlL2hhcmR3YXJlX2hhbWlsdG9uaWFuLnB5) | `100.00% <100.00%> (ø)` | |
| [pennylane/pulse/rydberg.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3936?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3B1bHNlL3J5ZGJlcmcucHk=) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

Qottmann: Re `wires` and `connections`. `wires` is now a mandatory and a warning is raised when `connections` $\nsubseteq$ `wires`, e.g.
```python3
connections = [[0, 1], [1, 3], [2, 1], [4, 5]]
wires = [0, 1, 2, 3, 4]
omega = 0.5 * np.arange(len(wires))
g = 0.1 * np.arange(len(connections))
anharmonicity = 0.3 * np.arange(len(wires))
H = qml.pulse.transmon_interaction(omega, connections, g, wires)
```

```pycon
UserWarning: Caution, wires and connections do not match. I.e., wires in connections [[0, 1], [1, 3], [2, 1], [4, 5]] are not contained in the wires [0, 1, 2, 3, 4]
```

The same works for string wires:
```python3
connections = [["a", "b"], ["a", "c"], ["d", "e"], ["e", "f"]]
wires = ["a", "b", "c", "d", "e"]
omega = 0.5 * np.arange(len(wires))
g = 0.1 * np.arange(len(connections))

H = qml.pulse.transmon_interaction(omega, connections, g, wires)
print(H.wires)
```

```pycon
UserWarning: Caution, wires and connections do not match. I.e., wires in connections [['a', 'b'], ['a', 'c'], ['d', 'e'], ['e', 'f']] are not contained in the wires ['a', 'b', 'c', 'd', 'e']
<Wires = ['a', 'b', 'c', 'd', 'e', 'f']>
```

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index ebcb6b6b1..e9dd0bb21 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -17,15 +17,17 @@
   [(#3790)](https://github.com/PennyLaneAI/pennylane/pull/3790)
 
 * Added the needed functions and classes to simulate an ensemble of Rydberg atoms:
-  * A new internal `RydbergHamiltonian` class is added, which contains the Hamiltonian of an ensemble of
-    Rydberg atoms.
-  * A new user-facing `rydberg_interaction` function is added, which returns a `RydbergHamiltonian` containing
+  * A new internal `HardwareHamiltonian` class is added, which contains additional information about pulses and settings.
+  * A new user-facing `rydberg_interaction` function is added, which returns a `HardwareHamiltonian` containing
     the Hamiltonian of the interaction of all the Rydberg atoms.
+  * A new user-facing `transmon_interaction` function is added, constructing
+    the Hamiltonian that describes the circuit QED interaction Hamiltonian of superconducting transmon systems.
   * A new user-facing `drive` function is added, which returns a `ParametrizedHamiltonian` (`HardwareHamiltonian`) containing
     the Hamiltonian of the interaction between a driving electro-magnetic field and a group of qubits.
   [(#3749)](https://github.com/PennyLaneAI/pennylane/pull/3749)
   [(#3911)](https://github.com/PennyLaneAI/pennylane/pull/3911)
   [(#3930)](https://github.com/PennyLaneAI/pennylane/pull/3930)
+  [(#3936)](https://github.com/PennyLaneAI/pennylane/pull/3936/)
 
 * Added `Operation.__truediv__` dunder method to be able to divide operators.
   [(#3749)](https://github.com/PennyLaneAI/pennylane/pull/3749)
diff --git a/pennylane/pulse/__init__.py b/pennylane/pulse/__init__.py
index c086265bc..34829d6c4 100644
--- a/pennylane/pulse/__init__.py
+++ b/pennylane/pulse/__init__.py
@@ -68,6 +68,7 @@ Hardware Compatible Hamiltonians
     :toctree: api
 
     ~rydberg_interaction
+    ~transmon_interaction
     ~drive
 
 
@@ -261,3 +262,4 @@ from .parametrized_evolution import ParametrizedEvolution
 from .parametrized_hamiltonian import ParametrizedHamiltonian
 from .hardware_hamiltonian import HardwareHamiltonian, drive
 from .rydberg import rydberg_interaction
+from .transmon import transmon_interaction
diff --git a/pennylane/pulse/hardware_hamiltonian.py b/pennylane/pulse/hardware_hamiltonian.py
index 5f4174e01..164506594 100644
--- a/pennylane/pulse/hardware_hamiltonian.py
+++ b/pennylane/pulse/hardware_hamiltonian.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 """This module contains the classes/functions needed to simulate and execute the evolution of real
 Hardware Hamiltonians"""
-import warnings
+
 from dataclasses import dataclass
 from typing import Callable, List, Union
 
@@ -25,40 +25,39 @@ from pennylane.ops.qubit.hamiltonian import Hamiltonian
 
 from .parametrized_hamiltonian import ParametrizedHamiltonian
 
+# from .transmon import TransmonSettings
+# from .rydberg import RydbergSettings
 
-def drive(amplitude, phase, detuning, wires):
-    r"""Constructs a :class:`ParametrizedHamiltonian` representing the action of a driving electromagnetic
-    field with a qubit.
 
-    .. math::
-        \frac{1}{2} \sum_{j \in \text{wires}} \Omega(t) \left(e^{i \phi(t)} \sigma^+_j + e^{-i \phi(t)} \sigma^-_j \right) -
-        \Delta(t) \sigma^z_j
+def drive(amplitude, phase, wires):
+    r"""Returns a :class:`ParametrizedHamiltonian` representing the action of a driving electromagnetic
+    field with a set of qubits.
 
-    where :math:`\Omega`, :math:`\phi` and :math:`\Delta` correspond to the amplitude, phase and detuning of the electromagnetic
-    driving field and :math:`j` corresponds to the wire index. We are describing the Hamiltonian in terms of ladder operators
-    :math:`\sigma^\pm = \frac{1}{2}(\sigma_x \pm i \sigma_y)`.
-    Note that the detuning :math:`\Delta := \omega_q - \nu` is defined as the difference between the qubit frequency :math:`\omega_q`
-    and the electromagntic field driving frequency :math:`\nu`. For more details, see the theoretical background section below.
+    .. math::
+        \frac{1}{2} \sum_{j \in \text{wires}} \Omega(t) \left(e^{i \phi(t)} \sigma^+_j + e^{-i \phi(t)} \sigma^-_j \right)
 
-    Common hardware systems are superconducting qubits and neutral atoms. The electromagnetic field of the drive is realized by microwave
-    and laser fields, respectively, operating at very differnt wavelengths.
+    where :math:`\Omega` and :math:`\phi` correspond to the amplitude and phase of the
+    electromagnetic driving field and :math:`j` corresponds to the wire index. We are describing the Hamiltonian
+    in terms of ladder operators :math:`\sigma^\pm = \frac{1}{2}(\sigma_x \pm i \sigma_y)`. Note that depending on the
+    hardware realization (neutral atoms, superconducting qubits), there are different conventions and notations.
+    E.g., for superconducting qubits it is common to describe the exponent of the phase factor as :math:`\exp(i(\phi(t) + \nu t))`, where :math:`\nu` is the
+    drive frequency. We describe their relations in the theoretical background section below.
 
-    Note that to avoid nummerical problems due to using both very large and very small numbers, it is advisable to match
-    the order of magnitudes of frequency and time arguments. For example, when frequencies are of order MHz (microwave pulses for superconducting systems),
-    then one can ommit the explicit factor :math:`10^6` by treating the times passed to the constructed :class:`ParametrizedHamiltonian` in :math:`\mu s = 10^{-6}s`
-    to be able to use numerical units of order :math:`\mathcal{O}(1)`. We further elaborate on that in the examples below.
+    Common hardware systems are superconducting qubits and neutral atoms. The electromagnetic field of the drive is
+    realized by microwave and laser fields, respectively, operating at very different wavelengths.
+    To avoid nummerical problems due to using both very large and very small numbers, it is advisable to match
+    the order of magnitudes of frequency and time arguments.
+    Read the usage details for more information on how to choose :math:`\Omega` and :math:`\phi`.
 
     Args:
         amplitude (Union[float, Callable]): float or callable returning the amplitude of an
             electromagnetic field
         phase (Union[float, Callable]): float or callable returning the phase (in radians) of the electromagnetic field
-        detuning (Union[float, Callable]): float or callable returning the detuning of an
-            electromagnetic field
         wires (Union[int, List[int]]): integer or list containing wire values for the qubits that
             the electromagnetic field acts on
 
     Returns:
-        HardwareHamiltonian: a :class:`~.ParametrizedHamiltonian` representing the action of the electromagnetic field
+        ParametrizedHamiltonian: a :class:`~.ParametrizedHamiltonian` representing the action of the electromagnetic field
         on the qubits.
 
     .. seealso::
@@ -68,19 +67,18 @@ def drive(amplitude, phase, detuning, wires):
 
     **Example**
 
-    We create a Hamiltonian describing a electromagnetic field acting on 4 qubits with a fixed detuning and
+    We create a Hamiltonian describing an electromagnetic field acting on 4 qubits with a fixed
     phase, as well as a parametrized, time-dependent amplitude. The Hamiltonian includes an interaction term for
     inter-qubit interactions.
 
-    .. code-block:: python
+    .. code-block:: python3
 
         wires = [0, 1, 2, 3]
         H_int = sum([qml.PauliX(i) @ qml.PauliX((i+1)%len(wires)) for i in wires])
 
         amplitude = lambda p, t: p * jnp.sin(jnp.pi * t)
         phase = jnp.pi / 2
-        detuning = 3 * jnp.pi / 4
-        H_d = qml.pulse.drive(amplitude, phase, detuning, wires)
+        H_d = qml.pulse.drive(amplitude, phase, wires)
 
     >>> H_int
     (1) [X0 X1]
@@ -88,11 +86,12 @@ def drive(amplitude, phase, detuning, wires):
     + (1) [X2 X3]
     + (1) [X3 X0]
     >>> H_d
-    ParametrizedHamiltonian: terms=3
+    ParametrizedHamiltonian: terms=2
 
-    The first two terms of the drive Hamiltonian ``H_d`` correspond to the two terms :math:`\Omega e^{i \phi(t)} \sigma^+_j + \Omega e^{-i \phi(t)} \sigma^-_j`,
-    describing a drive between the ground and excited states. The third term corresponding to the shift term
-    due to detuning from resonance. In this case, the drive term corresponds to a global drive, as it acts on all 4 wires of
+    The terms of the drive Hamiltonian ``H_d`` correspond to the two terms
+    :math:`\Omega e^{i \phi(t)} \sigma^+_j + \Omega e^{-i \phi(t)} \sigma^-_j`,
+    describing a drive between the ground and excited states.
+    In this case, the drive term corresponds to a global drive, as it acts on all 4 wires of
     the device.
 
     The full Hamiltonian can be evaluated:
@@ -108,9 +107,9 @@ def drive(amplitude, phase, detuning, wires):
 
     >>> params = [2.4]
     >>> circuit(params)
-    Array(0.77627534, dtype=float64)
+    Array(0.32495208, dtype=float64)
     >>> jax.grad(circuit)(params)
-    [Array(-0.0159532, dtype=float64)]
+    [Array(1.31956098, dtype=float64)]
 
     We can also create a Hamiltonian with multiple local drives. The following circuit corresponds to the
     evolution where an additional local drive that changes in time is acting on wires ``[0, 1]`` is added to the Hamiltonian:
@@ -119,8 +118,7 @@ def drive(amplitude, phase, detuning, wires):
 
         amplitude_local = lambda p, t: p[0] * jnp.sin(2 * jnp.pi * t) + p[1]
         phase_local = lambda p, t: p * jnp.exp(-0.25 * t)
-        detuning_local = jnp.pi / 4
-        H_local = qml.pulse.drive(amplitude_local, phase_local, detuning_local, [0, 1])
+        H_local = qml.pulse.drive(amplitude_local, phase_local, [0, 1])
 
         H = H_int + H_d + H_local
 
@@ -136,12 +134,12 @@ def drive(amplitude, phase, detuning, wires):
         params = (p_global, p_amp, p_phase)
 
     >>> circuit_local(params)
-    Array(0.4494223, dtype=float64)
+    Array(-0.5334795, dtype=float64)
     >>> jax.grad(circuit_local)(params)
-    (Array(0.17258209, dtype=float64),
-     [Array(-0.39050511, dtype=float64, weak_type=True),
-      Array(-0.15865324, dtype=float64, weak_type=True)],
-     Array(-0.16458317, dtype=float64))
+    (Array(0.01654573, dtype=float64),
+     [Array(-0.04422795, dtype=float64, weak_type=True),
+      Array(-0.51375441, dtype=float64, weak_type=True)],
+     Array(0.21901967, dtype=float64))
 
     .. details::
         :title: Theoretical background
@@ -151,18 +149,20 @@ def drive(amplitude, phase, detuning, wires):
 
         .. math::
             H = \frac{1}{2} \Omega(t) \sum_{j \in \text{wires}} \left(e^{i (\phi(t) + \nu t)} \sigma^+_j + e^{-i (\phi(t) + \nu t)} \sigma^-_j \right)
-            + \frac{1}{2} \omega_q \sum_{j \in \text{wires}} \sigma^z_j,
+            + \omega_q \sum_{j \in \text{wires}} \sigma^z_j,
 
         with amplitude :math:`\Omega`, phase :math:`\phi` and drive frequency :math:`\nu` of the electromagnetic field, as well as the qubit frequency :math:`\omega_q`.
-        We can move to the rotating frame of the driving field by applying :math:`U = e^{-i\nu t \sigma^z / 2}` which yields the new Hamiltonian
+        We can move to the rotating frame of the driving field by applying :math:`U = e^{-i\nu t \sigma^z}` which yields the new Hamiltonian
 
         .. math::
             H = \frac{1}{2} \Omega(t) \sum_{j \in \text{wires}} \left(e^{i \phi(t)} \sigma^+_j + e^{-i \phi(t)} \sigma^-_j \right)
-            - \frac{1}{2} (\nu - \omega_q) \sum_{j \in \text{wires}} \sigma^z_j
+            - (\nu - \omega_q) \sum_{j \in \text{wires}} \sigma^z_j
 
-        We can define :math:`\Delta = \nu - \omega_q` to arrive at the definition above. Note that a potential anharmonicity term,
-        as is common for transmon systems when taking into account higher energy levels,
-        is unaffected by this transformation.
+        The latter formulation is more common in neutral atom systems where we define the detuning from the atomic energy gap
+        as :math:`\Delta = \nu - \omega_q`. This is because here all atoms have the same energy gap, whereas for superconducting
+        qubits that is typically not the case.
+        Note that a potential anharmonicity term, as is common for transmon systems when taking into account higher energy
+        levels, is unaffected by this transformation.
 
     .. details::
         **Neutral Atom Rydberg systems**
@@ -179,71 +179,61 @@ def drive(amplitude, phase, detuning, wires):
             H_i = qml.pulse.rydberg_interaction(atom_coordinates, wires)
 
         We can now simulate driving those atoms with an oscillating amplitude :math:`\Omega` that is trainable, for a duration of :math:`10 \mu s`.
+
+        .. code-block:: python3
+
+            amplitude = lambda p, t: p * jnp.sin(jnp.pi * t)
+            phase = jnp.pi / 2
+
+            H_d = qml.pulse.drive(amplitude, phase, wires)
+
+            # detuning term
+            H_z = qml.dot([-3*np.pi/4]*len(wires), [qml.PauliZ(i) for i in wires])
+
+
         The total Hamiltonian of that evolution is given by
 
         .. math::
             \frac{1}{2} p \sin(\pi t) \sum_{j \in \text{wires}} \left(e^{i \pi/2} \sigma^+_j + e^{-i \pi/2} \sigma^-_j \right) -
-            \frac{1}{2} \frac{3 \pi}{4} \sum_{j \in \text{wires}} \sigma^z_j + \sum_{k<\ell} V_{k \ell} n_k n_\ell
+            \frac{3 \pi}{4} \sum_{j \in \text{wires}} \sigma^z_j + \sum_{k<\ell} V_{k \ell} n_k n_\ell
 
         and can be executed and differentiated via the following code.
 
         .. code-block:: python3
 
-            amplitude = lambda p, t: p * jnp.sin(jnp.pi * t)
-            phase = jnp.pi / 2
-            detuning = 3 * jnp.pi / 4
-            H_d = qml.pulse.drive(amplitude, phase, detuning, wires)
-
             dev = qml.device("default.qubit.jax", wires=wires)
-
             @qml.qnode(dev, interface="jax")
             def circuit(params):
-                qml.evolve(H_i + H_d)(params, t=[0, 10])
+                qml.evolve(H_i + H_z + H_d)(params, t=[0, 10])
                 return qml.expval(qml.PauliZ(1))
 
         >>> params = [2.4]
         >>> circuit(params)
-        Array(0.94301294, dtype=float64)
+        Array(0.6962041, dtype=float64)
         >>> jax.grad(circuit)(params)
-        [Array(0.59484969, dtype=float64)]
-
+        [Array(1.75825695, dtype=float64)]
     """
     if isinstance(wires, int):
         wires = [wires]
-    trivial_detuning = not callable(detuning) and qml.math.isclose(detuning, 0.0)
 
     if not callable(amplitude) and qml.math.isclose(amplitude, 0.0):
-        if trivial_detuning:
-            raise ValueError(
-                f"Expected non-zero value for at least one of either amplitude or detuning, but received amplitude={amplitude} and detuning={detuning}. All terms are zero."
-            )
-
-        coeffs = []
-        observables = []
+        raise ValueError(
+            f"Expected non-zero value or callable for amplitude, but received amplitude={amplitude}. All terms are zero."
+        )
 
     # TODO: use sigma+ and sigma- (not necessary as terms are the same, but for consistency)
-    # We compute the `coeffs` and `observables` of the electromagnetic field
-    else:
-        # We compute the `coeffs` and `observables` of the EM field
-        coeffs = [
-            amplitude_and_phase(qml.math.cos, amplitude, phase),
-            amplitude_and_phase(qml.math.sin, amplitude, phase),
-        ]
+    # We compute the `coeffs` and `observables` of the EM field
+    coeffs = [
+        amplitude_and_phase(qml.math.cos, amplitude, phase),
+        amplitude_and_phase(qml.math.sin, amplitude, phase),
+    ]
 
-        drive_x_term = 0.5 * sum(qml.PauliX(wire) for wire in wires)
-        drive_y_term = -0.5 * sum(qml.PauliY(wire) for wire in wires)
+    drive_x_term = 0.5 * sum(qml.PauliX(wire) for wire in wires)
+    drive_y_term = -0.5 * sum(qml.PauliY(wire) for wire in wires)
 
-        observables = [drive_x_term, drive_y_term]
+    observables = [drive_x_term, drive_y_term]
 
-    if not trivial_detuning:
-        detuning_term = -1.0 * sum(qml.PauliZ(wire) for wire in wires)
-        coeffs.append(detuning)
-        observables.append(detuning_term)
-
-    # We convert the pulse data into a list of ``HardwarePulse`` objects
-    pulses = [HardwarePulse(amplitude, phase, detuning, wires)]
-
-    return HardwareHamiltonian(coeffs, observables, pulses=pulses)
+    return HardwareHamiltonian(coeffs, observables)
 
 
 class HardwareHamiltonian(ParametrizedHamiltonian):
@@ -254,12 +244,13 @@ class HardwareHamiltonian(ParametrizedHamiltonian):
     but on top of that also contains attributes that store parameteres relevant for real hardware execution.
 
     .. warning::
-
         This class should NEVER be initialized directly! Please use the functions
         :func:`rydberg_interaction` and :func:`drive` instead.
 
+
     .. seealso:: :func:`rydberg_interaction`, :func:`drive`, :class:`ParametrizedHamiltonian`
 
+
     Args:
         coeffs (Union[float, callable]): coefficients of the Hamiltonian expression, which may be
             constants or parametrized functions. All functions passed as ``coeffs`` must have two
@@ -267,15 +258,15 @@ class HardwareHamiltonian(ParametrizedHamiltonian):
         observables (Iterable[Observable]): observables in the Hamiltonian expression, of same
             length as ``coeffs``
 
+
     Keyword Args:
-        register (list): list of coordinates (in micrometers) of each atom in the ensemble
-        pulses (list): list of ``HardwarePulse`` classes containing the information about the
-            amplitude, phase, detuning and wires of each pulse
-        interaction_coeff (float): Rydberg interaction constant in units: :math:`\text{MHz} \times \mu m^6`.
-            Defaults to :math:`862690 \text{MHz} \times \mu m^6`.
+        settings Union[RydbergSettings, TransmonSettings]: Dataclass containing the hardware specific settings. Default is ``None``.
+        pulses (list[HardwarePulse]): list of ``HardwarePulse`` dataclasses containing the information about the
+            amplitude, phase, drive frequency and wires of each pulse
 
     Returns:
         HardwareHamiltonian: class representing the Hamiltonian of Rydberg or Transmon device.
+
     """
 
     # pylint: disable=too-many-arguments
@@ -283,13 +274,11 @@ class HardwareHamiltonian(ParametrizedHamiltonian):
         self,
         coeffs,
         observables,
-        register: list = None,
         pulses: List["HardwarePulse"] = None,
-        interaction_coeff: float = 862690,
+        settings: Union["RydbergSettings", "TransmonSettings"] = None,
     ):
-        self.register = register
+        self.settings = settings
         self.pulses = [] if pulses is None else pulses
-        self.interaction_coeff = interaction_coeff
         super().__init__(coeffs, observables)
 
     def __call__(self, params, t):
@@ -298,42 +287,38 @@ class HardwareHamiltonian(ParametrizedHamiltonian):
 
     def __add__(self, other):
         if isinstance(other, HardwareHamiltonian):
-            # Update coeffs, obs and hardware attributes
-            if self.register is not None:
-                if other.register is not None:
-                    raise ValueError("We cannot add two Hamiltonians with an interaction term!")
-                if not self.wires.contains_wires(other.wires):
-                    warnings.warn("The wires of the drive fields are not present in the ensemble.")
-            elif other.register is not None and not other.wires.contains_wires(self.wires):
-                warnings.warn("The wires of the drive fields are not present in the ensemble.")
-
-            new_register = self.register or other.register
+            if self.settings is None and other.settings is None:
+                new_settings = None
+            else:
+                new_settings = self.settings + other.settings
+
             new_pulses = self.pulses + other.pulses
+
             new_ops = self.ops + other.ops
             new_coeffs = self.coeffs + other.coeffs
             return HardwareHamiltonian(
-                new_coeffs, new_ops, register=new_register, pulses=new_pulses
+                new_coeffs, new_ops, settings=new_settings, pulses=new_pulses
             )
 
         ops = self.ops.copy()
         coeffs = self.coeffs.copy()
-        register = self.register
+        settings = self.settings
         pulses = self.pulses
 
         if isinstance(other, (Hamiltonian, ParametrizedHamiltonian)):
             new_coeffs = coeffs + list(other.coeffs.copy())
             new_ops = ops + other.ops.copy()
-            return HardwareHamiltonian(new_coeffs, new_ops, register=register, pulses=pulses)
+            return HardwareHamiltonian(new_coeffs, new_ops, settings=settings, pulses=pulses)
 
         if isinstance(other, qml.ops.SProd):  # pylint: disable=no-member
             new_coeffs = coeffs + [other.scalar]
             new_ops = ops + [other.base]
-            return HardwareHamiltonian(new_coeffs, new_ops, register=register, pulses=pulses)
+            return HardwareHamiltonian(new_coeffs, new_ops, settings=settings, pulses=pulses)
 
         if isinstance(other, Operator):
             new_coeffs = coeffs + [1]
             new_ops = ops + [other]
-            return HardwareHamiltonian(new_coeffs, new_ops, register=register, pulses=pulses)
+            return HardwareHamiltonian(new_coeffs, new_ops, settings=settings, pulses=pulses)
 
         return NotImplemented
 
@@ -342,9 +327,7 @@ class HardwareHamiltonian(ParametrizedHamiltonian):
         ParametrizedHamiltonian. Ensures that this returns a HardwareHamiltonian where
         the order of the parametrized coefficients and operators matches the order of
         the hamiltonians, i.e. that
-
         ParametrizedHamiltonian + HardwareHamiltonian
-
         returns a HardwareHamiltonian where the call expects params = [params_PH] + [params_RH]
         """
         if isinstance(other, ParametrizedHamiltonian):
@@ -355,7 +338,7 @@ class HardwareHamiltonian(ParametrizedHamiltonian):
             new_ops = other.ops.copy() + ops
 
             return HardwareHamiltonian(
-                new_coeffs, new_ops, register=self.register, pulses=self.pulses
+                new_coeffs, new_ops, settings=self.settings, pulses=self.pulses
             )
 
         return self.__add__(other)
@@ -370,15 +353,17 @@ class HardwarePulse:
         amplitude (Union[float, Callable]): float or callable returning the amplitude of an EM
             field
         phase (Union[float, Callable]): float containing the phase (in radians) of the EM field
-        detuning (Union[float, Callable]): float or callable returning the detuning of a
-            EM field
+        frequency (Union[float, Callable]): float or callable returning the frequency of a
+            EM field. In the case of superconducting transmon systems this is the drive frequency.
+            In the case of neutral atom rydberg systems this is the detuning between the drive frequency
+            and energy gap.
         wires (Union[int, List[int]]): integer or list containing wire values that the EM field
             acts on
     """
 
     amplitude: Union[float, Callable]
     phase: Union[float, Callable]
-    detuning: Union[float, Callable]
+    frequency: Union[float, Callable]
     wires: List[Wires]
 
     def __post_init__(self):
@@ -388,7 +373,7 @@ class HardwarePulse:
         return (
             self.amplitude == other.amplitude
             and self.phase == other.phase
-            and self.detuning == other.detuning
+            and self.frequency == other.frequency
             and self.wires == other.wires
         )
 
diff --git a/pennylane/pulse/rydberg.py b/pennylane/pulse/rydberg.py
index ae77837ce..8cf691953 100644
--- a/pennylane/pulse/rydberg.py
+++ b/pennylane/pulse/rydberg.py
@@ -13,10 +13,10 @@
 # limitations under the License.
 """This module contains the classes/functions needed to simulate the evolution of ensembles of
 individual (trapped) rydberg atoms under the excitation of several laser fields."""
-
+from dataclasses import dataclass
 import numpy as np
-
 import pennylane as qml
+
 from pennylane.pulse import HardwareHamiltonian
 
 
@@ -109,6 +109,37 @@ def rydberg_interaction(
             observables.append(qml.prod(qml.Projector([1], wire1), qml.Projector([1], wire2)))
             # Rydberg projectors
 
-    return HardwareHamiltonian(
-        coeffs, observables, register=register, interaction_coeff=interaction_coeff
-    )
+    settings = RydbergSettings(register, interaction_coeff)
+
+    return HardwareHamiltonian(coeffs, observables, settings=settings)
+
+
+@dataclass
+class RydbergSettings:
+    """Dataclass that contains the information of a Rydberg setup.
+
+    Args:
+        register (list): coordinates of atoms
+        interaction_coeff (float): interaction coefficient C6 from C6/(Rij)**6 term in :func:`rydberg_interaction`
+    """
+
+    register: list
+    interaction_coeff: float = 0.0
+
+    def __eq__(self, other):
+        return (
+            qml.math.all(self.register == other.register)
+            and self.interaction_coeff == other.interaction_coeff
+        )
+
+    def __add__(self, other):
+        if other is not None:
+            raise ValueError(
+                "Cannot add two ``HardwareHamiltonian`` instaces with an interaction term"
+                f"Obtained two instances with settings {self} and {other}"
+                "You most likely try to add two terms generated by help(qml.pulse.rydberg_interaction)"
+            )
+
+        return self
+
+    __radd__ = __add__
diff --git a/pennylane/pulse/transmon.py b/pennylane/pulse/transmon.py
new file mode 100644
index 000000000..860d74b55
--- /dev/null
+++ b/pennylane/pulse/transmon.py
@@ -0,0 +1,217 @@
+# Copyright 2018-2023 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""This module contains the classes/functions specific for simulation of superconducting transmon hardware systems"""
+import warnings
+
+from dataclasses import dataclass
+from typing import Callable, List, Union
+
+import pennylane as qml
+import pennylane.numpy as np
+from pennylane.pulse import HardwareHamiltonian
+from pennylane.typing import TensorLike
+from pennylane.wires import Wires
+
+
+# TODO ladder operators once there is qudit support
+# pylint: disable=unused-argument
+def a(wire, d=2):
+    """creation operator"""
+    return qml.s_prod(0.5, qml.PauliX(wire)) + qml.s_prod(0.5j, qml.PauliY(wire))
+
+
+def ad(wire, d=2):
+    """annihilation operator"""
+    return qml.s_prod(0.5, qml.PauliX(wire)) + qml.s_prod(-0.5j, qml.PauliY(wire))
+
+
+# pylint: disable=too-many-arguments
+def transmon_interaction(
+    omega: Union[float, list],
+    connections: list,
+    g: Union[float, list],
+    wires: list,
+    anharmonicity=None,
+    d=2,
+):
+    r"""Returns a :class:`ParametrizedHamiltonian` representing the circuit QED Hamiltonian of a superconducting transmon system.
+
+    The Hamiltonian is given by
+
+    .. math::
+
+        H = \sum_{q\in \text{wires}} \omega_q a^\dagger_q a_q
+        + \sum_{(i, j) \in \mathcal{C}} g_{ij} \left(a^\dagger_i a_j + a_j^\dagger a_i \right)
+        + \sum_{q\in \text{wires}} \alpha_q a^\dagger_q a^\dagger_q a_q a_q
+
+    where :math:`[a^\dagger_p, a_q] = i \delta_{pq}` are bosonic creation and annihilation operators.
+    The first term describes the dressed qubit frequencies :math:`\omega_q`, the second term their
+    coupling :math:`g_{ij}` and the last the anharmonicity :math:`\alpha_q`, which all can vary for
+    different qubits. In practice, the bosonic operators are restricted to a finite dimension of the
+    local Hilbert space (default ``d=2`` corresponds to qubits).
+    In that case, the anharmonicity is set to :math:`\alpha=0` and ignored.
+
+    The values of :math:`\omega` and :math:`\alpha` are typically around :math:`5 \times 2\pi \text{GHz}` and :math:`0.3 \times 2\pi \text{GHz}`, respectively.
+    It is common for different qubits to be out of tune with different energy gaps. The coupling strength
+    :math:`g` typically varies betwewen :math:`[0.001, 0.1] \times 2\pi \text{GHz}`. For some example parameters,
+    see e.g. `arXiv:1804.04073 <https://arxiv.org/abs/1804.04073>`_,
+    `arXiv:2203.06818 <https://arxiv.org/abs/2203.06818>`_, or `arXiv:2210.15812 <https://arxiv.org/abs/2210.15812>`_.
+
+    .. note:: Currently only supporting ``d=2`` with qudit support planned in the future.
+
+    .. seealso::
+
+        :func:`~.drive`
+
+    Args:
+        omega (Union[float, list[float]]): List of dressed qubit frequencies in GHz. Needs to match the length of ``wires``.
+            When passing a single float all qubits are assumed to have that same frequency.
+        connections (list[tuple(int)]): List of connections ``(i, j)`` between qubits i and j.
+            When the wires in ``connections`` are not contained in ``wires``, a warning is raised.
+        g (Union[float, list[float]]): List of coupling strengths in GHz. Needs to match the length of ``connections``.
+            When passing a single float need explicit ``wires``.
+        anharmonicity (Union[float, list[float]]): List of anharmonicities in GHz. Ignored when ``d=2``.
+            When passing a single float all qubits are assumed to have that same anharmonicity.
+        wires (list): Needs to be of the same length as omega. Note that there can be additional
+            wires in the resulting operator from the ``connections``, which are treated independently.
+        d (int): Local Hilbert space dimension. Defaults to ``d=2`` and is currently the only supported value.
+
+    Returns:
+        HardwareHamiltonian: a :class:`~.ParametrizedHamiltonian` representing the transmon interaction
+
+    **Example**
+
+    We can set up the transmon interaction Hamiltonian with uniform coefficients by passing ``float`` values.
+
+    .. code-block::
+
+        connections = [[0, 1], [1, 3], [2, 1], [4, 5]]
+        H = qml.pulse.transmon_interaction(omega=0.5, connections=connections, g=1.)
+
+    The resulting :class:`~.ParametrizedHamiltonian` consists of ``4`` coupling terms and ``6`` qubits
+    because there are six different wire indices in ``connections``.
+
+    >>> print(H)
+    ParametrizedHamiltonian: terms=10
+
+    We can also provide individual values for each of the qubit energies and connections.
+
+    .. code-block::
+
+        omega = [0.5, 0.4, 0.3, 0.2, 0.1, 0.]
+        g = [1., 2., 3., 4.]
+        H = qml.pulse.transmon_interaction(omega=omega, connections=connections, g=g)
+
+    """
+    if d != 2:
+        raise NotImplementedError(
+            "Currently only supporting qubits. Qutrits and qudits are planned in the future."
+        )
+
+    # if wires is None and qml.math.ndim(omega) == 0:
+    #     raise ValueError(
+    #         f"Cannot instantiate wires automatically. Either need specific wires or a list of omega."
+    #         f"Received wires {wires} and omega of type {type(omega)}"
+    #     )
+
+    # wires = wires or list(range(len(omega)))
+
+    n_wires = len(wires)
+
+    if not Wires(wires).contains_wires(Wires(np.unique(connections).tolist())):
+        warnings.warn(
+            f"Caution, wires and connections do not match. "
+            f"I.e., wires in connections {connections} are not contained in the wires {wires}"
+        )
+
+    # Prepare coefficients
+    if anharmonicity is None:
+        anharmonicity = [0.0] * n_wires
+
+    # TODO: make coefficients callable / trainable. Currently not supported
+    if qml.math.ndim(omega) == 0:
+        omega = [omega] * n_wires
+    if len(omega) != n_wires:
+        raise ValueError(
+            f"Number of qubit frequencies omega = {omega} does not match the provided wires = {wires}"
+        )
+
+    if qml.math.ndim(g) == 0:
+        g = [g] * len(connections)
+    if len(g) != len(connections):
+        raise ValueError(
+            f"Number of coupling terms {g} does not match the provided connections = {connections}"
+        )
+
+    # qubit term
+    coeffs = list(omega)
+    observables = [ad(i, d) @ a(i, d) for i in wires]
+
+    # coupling term term
+    coeffs += list(g)
+    observables += [ad(i, d) @ a(j, d) + ad(j, d) @ a(i, d) for (i, j) in connections]
+
+    # TODO Qudit support. Currently not supported but will be in the future.
+    # if d>2:
+    #     if anharmonicity is None:
+    #         anharmonicity = [0.] * n_wires
+    #     if qml.math.ndim(anharmonicity)==0:
+    #         anharmonicity = [anharmonicity] * n_wires
+    #     if len(anharmonicity) != n_wires:
+    #         raise ValueError(f"Number of qubit anharmonicities anharmonicity = {anharmonicity} does not match the provided wires = {wires}")
+    #     # anharmonicity term
+    #     coeffs += list(anharmonicity)
+    #     observables += [ad(i, d) @ ad(i, d) @ a(i, d) @ a(i, d) for i in wires]
+
+    settings = TransmonSettings(connections, omega, g, anharmonicity=anharmonicity)
+
+    return HardwareHamiltonian(coeffs, observables, settings=settings)
+
+
+@dataclass
+class TransmonSettings:
+    """Dataclass that contains the information of a Transmon setup.
+
+    .. see-also:: :func:`transmon_interaction`
+
+    Args:
+            connections (List): List `[[idx_q0, idx_q1], ..]` of connected qubits (wires)
+            omega (List[float, Callable]):
+            anharmonicity (List[float, Callable]):
+            g (List[list, TensorLike, Callable]):
+
+    """
+
+    connections: List
+    omega: Union[float, Callable]
+    g: Union[list, TensorLike, Callable]
+    anharmonicity: Union[float, Callable]
+
+    def __eq__(self, other):
+        return (
+            qml.math.all(self.connections == other.connections)
+            and qml.math.all(self.omega == other.omega)
+            and qml.math.all(self.g == other.g)
+            and qml.math.all(self.anharmonicity == other.anharmonicity)
+        )
+
+    def __add__(self, other):
+        if other is not None:
+            new_connections = list(self.connections) + list(other.connections)
+            new_omega = list(self.omega) + list(other.omega)
+            new_g = list(self.g) + list(other.g)
+            new_anh = list(self.anharmonicity) + list(other.anharmonicity)
+            return TransmonSettings(new_connections, new_omega, new_g, anharmonicity=new_anh)
+
+        return self


Test patch:
diff --git a/tests/pulse/test_hardware_hamiltonian.py b/tests/pulse/test_hardware_hamiltonian.py
index 48587fcfd..b87583d0f 100644
--- a/tests/pulse/test_hardware_hamiltonian.py
+++ b/tests/pulse/test_hardware_hamiltonian.py
@@ -14,8 +14,6 @@
 """
 Unit tests for the HardwareHamiltonian class.
 """
-import warnings
-
 # pylint: disable=too-few-public-methods
 import numpy as np
 import pytest
@@ -28,6 +26,8 @@ from pennylane.pulse.hardware_hamiltonian import (
     amplitude_and_phase,
     _reorder_parameters,
 )
+from pennylane.pulse.transmon import TransmonSettings
+from pennylane.pulse.rydberg import RydbergSettings
 from pennylane.wires import Wires
 
 atom_coordinates = [[0, 0], [0, 5], [5, 0], [10, 5], [5, 10], [10, 10]]
@@ -45,47 +45,64 @@ def f2(p, t):
 param = [1.2, 2.3]
 
 
+connections = [[0, 1], [1, 3], [2, 1], [4, 5]]
+wires = [0, 1, 2, 3, 4, 5]
+omega = 0.5 * np.arange(len(wires))
+g = 0.1 * np.arange(len(connections))
+anharmonicity = 0.3 * np.arange(len(wires))
+transmon_settings = TransmonSettings(connections, omega, g, anharmonicity=None)
+
+atom_coordinates = [[0, 0], [0, 5], [5, 0], [10, 5], [5, 10], [10, 10]]
+rydberg_settings = RydbergSettings(atom_coordinates, 1)
+
+
 class TestHardwareHamiltonian:
     """Unit tests for the properties of the HardwareHamiltonian class."""
 
     # pylint: disable=protected-access
     def test_initialization(self):
         """Test the HardwareHamiltonian class is initialized correctly."""
-        rm = HardwareHamiltonian(coeffs=[], observables=[], register=atom_coordinates)
+        rm = HardwareHamiltonian(coeffs=[], observables=[])
 
-        assert qml.math.allequal(rm.register, atom_coordinates)
         assert rm.pulses == []
         assert rm.wires == Wires([])
-        assert rm.interaction_coeff == 862690
-
-    def test_add_rydberg_hamiltonian(self):
+        assert rm.settings is None
+
+    @pytest.mark.parametrize(
+        "settings",
+        [
+            None,
+            transmon_settings,
+            rydberg_settings,
+        ],
+    )
+    def test_add_hardware_hamiltonian(self, settings):
         """Test that the __add__ dunder method works correctly."""
         rm1 = HardwareHamiltonian(
             coeffs=[1, 2],
             observables=[qml.PauliX(4), qml.PauliZ(8)],
-            register=atom_coordinates,
             pulses=[HardwarePulse(1, 2, 3, [4, 8])],
+            settings=settings,
         )
         rm2 = HardwareHamiltonian(
             coeffs=[2],
             observables=[qml.PauliY(8)],
             pulses=[HardwarePulse(5, 6, 7, 8)],
         )
-        with warnings.catch_warnings():
-            # We make sure that no warning is raised
-            warnings.simplefilter("error")
-            sum_rm = rm1 + rm2
+
+        sum_rm = rm1 + rm2
+
         assert isinstance(sum_rm, HardwareHamiltonian)
         assert qml.math.allequal(sum_rm.coeffs, [1, 2, 2])
         assert all(
             qml.equal(op1, op2)
             for op1, op2 in zip(sum_rm.ops, [qml.PauliX(4), qml.PauliZ(8), qml.PauliY(8)])
         )
-        assert qml.math.allequal(sum_rm.register, atom_coordinates)
         assert sum_rm.pulses == [
             HardwarePulse(1, 2, 3, [4, 8]),
             HardwarePulse(5, 6, 7, 8),
         ]
+        assert sum_rm.settings == settings
 
     def test_add_parametrized_hamiltonian(self):
         """Tests that adding a `HardwareHamiltonian` and `ParametrizedHamiltonian` works as
@@ -122,36 +139,24 @@ class TestHardwareHamiltonian:
         assert res2.ops_parametrized == []
         assert res2.wires == qml.wires.Wires(h_wires)
 
-    def test_add_raises_error(self):
-        """Test that an error is raised if two HardwareHamiltonians with registers are added."""
-        rm1 = HardwareHamiltonian(
-            coeffs=[1],
-            observables=[qml.PauliX(0)],
-            register=atom_coordinates,
-            pulses=[HardwarePulse(1, 2, 3, 4)],
-        )
-        with pytest.raises(
-            ValueError, match="We cannot add two Hamiltonians with an interaction term"
-        ):
-            _ = rm1 + rm1
-
+    @pytest.mark.xfail
     def test_add_raises_warning(self):
         """Test that an error is raised when adding two HardwareHamiltonians where one Hamiltonian
         contains pulses on wires that are not present in the register."""
         coords = [[0, 0], [0, 5], [5, 0]]
 
         Hd = rydberg_interaction(register=coords, wires=[0, 1, 2])
-        Ht = drive(2, 3, 4, wires=3)
+        Ht = drive(2, 3, wires=3)
 
         with pytest.warns(
             UserWarning,
-            match="The wires of the drive fields are not present in the ensemble.",
+            match="The wires of the laser fields are not present in the Rydberg ensemble",
         ):
             _ = Hd + Ht
 
         with pytest.warns(
             UserWarning,
-            match="The wires of the drive fields are not present in the ensemble.",
+            match="The wires of the laser fields are not present in the Rydberg ensemble",
         ):
             _ = Ht + Hd
 
@@ -167,16 +172,13 @@ class TestHardwareHamiltonian:
         def phase(p, t):
             return p * t
 
-        def detuning(p, t):
-            return np.cos(t) * p
-
-        H_global = qml.pulse.drive(amp, phase, detuning, wires=[0])
-        H_global += qml.pulse.drive(amp, phase, detuning, wires=[0])
+        H_global = qml.pulse.drive(amp, phase, wires=[0])
+        H_global += qml.pulse.drive(amp, phase, wires=[0])
         H_global += np.polyval * qml.PauliX(0)
 
         # start with HardwareHamiltonians, then add ParametrizedHamiltonian
-        params = [np.array([1.2, 2.3, 3.4]), 4.5, 5.6]
-        params += [np.array([1.2, 2.3, 3.4]), 4.5, 5.6]
+        params = [np.array([1.2, 2.3, 3.4]), 4.5]
+        params += [np.array([1.2, 2.3, 3.4]), 4.5]
         params += [np.ones(2)]
 
         assert isinstance(H_global, HardwareHamiltonian)
@@ -194,22 +196,20 @@ class TestHardwareHamiltonian:
         def phase(p, t):
             return p * t
 
-        def detuning(p, t):
-            return np.cos(t) * p
-
         # start with ParametrizedHamiltonian, add on HardwareHamiltonians
         H_global = np.polyval * qml.PauliX(0)
-        H_global += qml.pulse.drive(amp, phase, detuning, wires=[0])
-        H_global += qml.pulse.drive(amp, phase, detuning, wires=[0])
+        H_global += qml.pulse.drive(amp, phase, wires=[0])
+        H_global += qml.pulse.drive(amp, phase, wires=[0])
 
         params = [np.ones(2)]
-        params += [np.array([1.2, 2.3, 3.4]), 4.5, 5.6]
-        params += [np.array([1.2, 2.3, 3.4]), 4.5, 5.6]
+        params += [np.array([1.2, 2.3, 3.4]), 4.5]
+        params += [np.array([1.2, 2.3, 3.4]), 4.5]
 
         assert isinstance(H_global, HardwareHamiltonian)
         H_global(params, 2)  # no error raised
 
 
+# pylint: disable=no-member
 class TestInteractionWithOperators:
     """Test that the interaction between a ``HardwareHamiltonian`` and other operators work as
     expected."""
@@ -231,7 +231,7 @@ class TestInteractionWithOperators:
         """Test that a Hamiltonian and SProd can be added to a HardwareHamiltonian, and
         will be incorporated in the H_fixed term, with their coefficients included in H_coeffs_fixed.
         """
-        R = drive(amplitude=f1, phase=0, detuning=f2, wires=[0, 1])
+        R = drive(amplitude=f1, phase=0, wires=[0, 1])
         params = [1, 2]
         # Adding on the right
         new_pH = R + H
@@ -252,8 +252,8 @@ class TestInteractionWithOperators:
     def test_add_other_operators(self, op):
         """Test that a Hamiltonian, SProd, Tensor or Operator can be added to a
         ParametrizedHamiltonian, and will be incorporated in the H_fixed term"""
-        R = drive(amplitude=f1, phase=0, detuning=f2, wires=[0, 1])
-        params = [1, 2]
+        R = drive(amplitude=f1, phase=0, wires=[0, 1])
+        params = [1]
 
         # Adding on the right
         new_pH = R + op
@@ -270,7 +270,7 @@ class TestInteractionWithOperators:
         new_pH(params, 2)  # confirm calling does not raise error
 
     def test_unknown_type_raises_error(self):
-        R = drive(amplitude=f1, phase=0, detuning=f2, wires=[0, 1])
+        R = drive(amplitude=f1, phase=0, wires=[0, 1])
         with pytest.raises(TypeError, match="unsupported operand type"):
             R += 3
 
@@ -282,14 +282,11 @@ class TestDrive:
         """Test that the attributes and the number of terms of the ``ParametrizedHamiltonian`` returned by
         ``drive`` are correct."""
 
-        Hd = drive(amplitude=1, phase=2, detuning=3, wires=[1, 2])
+        Hd = drive(amplitude=1, phase=2, wires=[1, 2])
 
         assert isinstance(Hd, HardwareHamiltonian)
-        assert Hd.interaction_coeff == 862690
         assert Hd.wires == Wires([1, 2])
-        assert Hd.register is None
-        assert len(Hd.ops) == 3  # 2 amplitude/phase terms and one detuning term of the Hamiltonian
-        assert Hd.pulses == [HardwarePulse(1, 2, 3, [1, 2])]
+        assert len(Hd.ops) == 2  # 2 amplitude/phase terms
 
     def test_multiple_local_drives(self):
         """Test that adding multiple drive terms behaves as expected"""
@@ -297,105 +294,46 @@ class TestDrive:
         def fa(p, t):
             return np.sin(p * t)
 
-        def fb(p, t):
-            return np.cos(p * t)
-
-        H1 = drive(amplitude=fa, phase=1, detuning=3, wires=[0, 3])
-        H2 = drive(amplitude=1, phase=3, detuning=fb, wires=[1, 2])
+        H1 = drive(amplitude=fa, phase=1, wires=[0, 3])
+        H2 = drive(amplitude=1, phase=3, wires=[1, 2])
         Hd = H1 + H2
 
         ops_expected = [
-            qml.Hamiltonian([-1, -1], [qml.PauliZ(0), qml.PauliZ(3)]),
             qml.Hamiltonian([0.5, 0.5], [qml.PauliX(1), qml.PauliX(2)]),
             qml.Hamiltonian([-0.5, -0.5], [qml.PauliY(1), qml.PauliY(2)]),
             qml.Hamiltonian([0.5, 0.5], [qml.PauliX(0), qml.PauliX(3)]),
             qml.Hamiltonian([-0.5, -0.5], [qml.PauliY(0), qml.PauliY(3)]),
-            qml.Hamiltonian([-1, -1], [qml.PauliZ(1), qml.PauliZ(2)]),
         ]
         coeffs_expected = [
-            3,
             np.cos(3),
             np.sin(3),
             AmplitudeAndPhase(np.cos, fa, 1),
             AmplitudeAndPhase(np.sin, fa, 1),
-            fb,
         ]
         H_expected = HardwareHamiltonian(coeffs_expected, ops_expected)
 
         # structure of Hamiltonian is as expected
         assert isinstance(Hd, HardwareHamiltonian)
-        assert Hd.interaction_coeff == 862690
-        assert Hd.wires == Wires([0, 3, 1, 2])
-        assert Hd.register is None
-        assert len(Hd.ops) == 6  # 2 terms for amplitude/phase and one detuning for each drive
+        assert Hd.wires == Wires([1, 2, 0, 3])
+        assert Hd.settings is None
+        assert len(Hd.ops) == 4  # 2 terms for amplitude/phase
 
         # coefficients are correct
         # Callable coefficients are shifted to the end of the list.
-        assert Hd.coeffs[0:3] == [3, np.cos(3), np.sin(3)]
+        assert Hd.coeffs[:2] == [np.cos(3), np.sin(3)]
+        assert isinstance(Hd.coeffs[2], AmplitudeAndPhase)
         assert isinstance(Hd.coeffs[3], AmplitudeAndPhase)
-        assert isinstance(Hd.coeffs[4], AmplitudeAndPhase)
-        assert Hd.coeffs[5] is fb
 
         # pulses were added correctly
-        assert len(Hd.pulses) == 2
-        assert Hd.pulses == H1.pulses + H2.pulses
+        assert Hd.pulses == []
 
         # Hamiltonian is as expected
         assert qml.equal(Hd([0.5, -0.5], t=5), H_expected([0.5, -0.5], t=5))
 
-    def test_no_amplitude(self):
-        """Test that when amplitude is not specified, the drive term is correctly defined."""
-
-        def f(p, t):
-            return np.cos(p * t)
-
-        Hd = drive(amplitude=0, phase=1, detuning=f, wires=[0, 3])
-
-        ops_expected = [qml.Hamiltonian([-1, -1], [qml.PauliZ(0), qml.PauliZ(3)])]
-        coeffs_expected = [f]
-        H_expected = HardwareHamiltonian(coeffs_expected, ops_expected)
-
-        assert qml.equal(Hd([0.1], 10), H_expected([0.1], 10))
-        assert isinstance(Hd, HardwareHamiltonian)
-        assert Hd.interaction_coeff == 862690
-        assert Hd.wires == Wires([0, 3])
-        assert Hd.register is None
-        assert len(Hd.coeffs) == 1
-        assert Hd.coeffs[0] is f
-        assert len(Hd.ops) == 1
-        assert qml.equal(Hd.ops[0], ops_expected[0])
-
-    def test_no_detuning(self):
-        """Test that when detuning not specified, the drive term is correctly defined."""
-
-        def f(p, t):
-            return np.cos(p * t)
-
-        Hd = drive(amplitude=f, phase=1, detuning=0, wires=[0, 3])
-
-        ops_expected = [
-            qml.Hamiltonian([0.5, 0.5], [qml.PauliX(0), qml.PauliX(3)]),
-            qml.Hamiltonian([-0.5, -0.5], [qml.PauliY(0), qml.PauliY(3)]),
-        ]
-        coeffs_expected = [
-            AmplitudeAndPhase(np.cos, f, 1),
-            AmplitudeAndPhase(np.sin, f, 1),
-        ]
-        H_expected = HardwareHamiltonian(coeffs_expected, ops_expected)
-
-        assert qml.equal(Hd([0.1], 10), H_expected([0.1], 10))
-        assert isinstance(Hd, HardwareHamiltonian)
-        assert Hd.interaction_coeff == 862690
-        assert Hd.wires == Wires([0, 3])
-        assert Hd.register is None
-        assert all(isinstance(coeff, AmplitudeAndPhase) for coeff in Hd.coeffs)
-        assert len(Hd.coeffs) == 2
-        assert all(qml.equal(op, op_expected) for op, op_expected in zip(Hd.ops, ops_expected))
-
     def test_no_amplitude_no_detuning(self):
         """Test that the correct error is raised if both amplitude and detuning are trivial."""
-        with pytest.raises(ValueError, match="Expected non-zero value for at least one of either"):
-            _ = drive(0, np.pi, 0, wires=[0])
+        with pytest.raises(ValueError, match="Expected non-zero value or callable"):
+            _ = drive(0, np.pi, wires=[0])
 
 
 def callable_amp(p, t):
@@ -480,50 +418,36 @@ class TestAmplitudeAndPhase:
         creates AmplitudeAndPhase callables, and the resulting Hamiltonian
         can be called successfully"""
 
-        detuning = 2
-
-        Hd = drive(sine_func, cosine_fun, detuning, wires=[0, 1])
+        Hd = drive(sine_func, cosine_fun, wires=[0, 1])
 
-        assert len(Hd.coeffs) == 3
+        assert len(Hd.coeffs) == 2
+        assert isinstance(Hd.coeffs[0], AmplitudeAndPhase)
         assert isinstance(Hd.coeffs[1], AmplitudeAndPhase)
-        assert isinstance(Hd.coeffs[2], AmplitudeAndPhase)
         t = 1.7
 
         evaluated_H = Hd([3.4, 5.6], t)
 
-        expected_H_fixed = qml.s_prod(
-            detuning, qml.Hamiltonian([-1, -1], [qml.PauliZ(0), qml.PauliZ(1)])
-        )
-
         c1 = np.sin(3.4 * t) * np.cos(np.cos(5.6 * t))
         c2 = np.sin(3.4 * t) * np.sin(np.cos(5.6 * t))
         expected_H_parametrized = qml.sum(
             qml.s_prod(c1, qml.Hamiltonian([0.5, 0.5], [qml.PauliX(0), qml.PauliX(1)])),
             qml.s_prod(c2, qml.Hamiltonian([-0.5, -0.5], [qml.PauliY(0), qml.PauliY(1)])),
         )
-
-        assert qml.equal(evaluated_H[0], expected_H_fixed)
-        assert qml.equal(evaluated_H[1], expected_H_parametrized)
+        assert qml.equal(evaluated_H, expected_H_parametrized)
 
     def test_callable_phase_hamiltonian(self):
         """Test that using callable phase in drive creates AmplitudeAndPhase
         callables, and the resulting Hamiltonian can be called"""
 
-        detuning = 2
-
-        Hd = drive(7.2, sine_func, detuning, wires=[0, 1])
+        Hd = drive(7.2, sine_func, wires=[0, 1])
 
-        assert len(Hd.coeffs) == 3
+        assert len(Hd.coeffs) == 2
+        assert isinstance(Hd.coeffs[0], AmplitudeAndPhase)
         assert isinstance(Hd.coeffs[1], AmplitudeAndPhase)
-        assert isinstance(Hd.coeffs[2], AmplitudeAndPhase)
         t = 1.7
 
         evaluated_H = Hd([5.6], t)
 
-        expected_H_fixed = qml.s_prod(
-            detuning, qml.Hamiltonian([-1, -1], [qml.PauliZ(0), qml.PauliZ(1)])
-        )
-
         c1 = 7.2 * np.cos(np.sin(5.6 * t))
         c2 = 7.2 * np.sin(np.sin(5.6 * t))
         expected_H_parametrized = qml.sum(
@@ -531,28 +455,21 @@ class TestAmplitudeAndPhase:
             qml.s_prod(c2, qml.Hamiltonian([-0.5, -0.5], [qml.PauliY(0), qml.PauliY(1)])),
         )
 
-        assert qml.equal(evaluated_H[0], expected_H_fixed)
-        assert qml.equal(evaluated_H[1], expected_H_parametrized)
+        assert qml.equal(evaluated_H, expected_H_parametrized)
 
     def test_callable_amplitude_hamiltonian(self):
         """Test that using callable amplitude in drive creates AmplitudeAndPhase
         callables, and the resulting Hamiltonian can be called"""
 
-        detuning = 2
+        Hd = drive(sine_func, 4.3, wires=[0, 1])
 
-        Hd = drive(sine_func, 4.3, detuning, wires=[0, 1])
-
-        assert len(Hd.coeffs) == 3
+        assert len(Hd.coeffs) == 2
+        assert isinstance(Hd.coeffs[0], AmplitudeAndPhase)
         assert isinstance(Hd.coeffs[1], AmplitudeAndPhase)
-        assert isinstance(Hd.coeffs[2], AmplitudeAndPhase)
         t = 1.7
 
         evaluated_H = Hd([3.4], t)
 
-        expected_H_fixed = qml.s_prod(
-            detuning, qml.Hamiltonian([-1, -1], [qml.PauliZ(0), qml.PauliZ(1)])
-        )
-
         c1 = np.sin(3.4 * t) * np.cos(4.3)
         c2 = np.sin(3.4 * t) * np.sin(4.3)
         expected_H_parametrized = qml.sum(
@@ -560,8 +477,7 @@ class TestAmplitudeAndPhase:
             qml.s_prod(c2, qml.Hamiltonian([-0.5, -0.5], [qml.PauliY(0), qml.PauliY(1)])),
         )
 
-        assert qml.equal(evaluated_H[0], expected_H_fixed)
-        assert qml.equal(evaluated_H[1], expected_H_parametrized)
+        assert qml.equal(evaluated_H, expected_H_parametrized)
 
     COEFFS_AND_PARAMS = [
         (
@@ -606,10 +522,10 @@ class TestHardwarePulse:
 
     def test_init(self):
         """Test the initialization of the ``HardwarePulse`` class."""
-        p = HardwarePulse(amplitude=4, detuning=9, phase=8, wires=[0, 4, 7])
+        p = HardwarePulse(amplitude=4, phase=8, frequency=3, wires=[0, 4, 7])
         assert p.amplitude == 4
         assert p.phase == 8
-        assert p.detuning == 9
+        assert p.frequency == 3
         assert p.wires == Wires([0, 4, 7])
 
     def test_equal(self):
@@ -636,10 +552,7 @@ class TestIntegration:
         def fa(p, t):
             return jnp.polyval(p, t)
 
-        def fb(p, t):
-            return p[0] * jnp.sin(p[1] * t)
-
-        Ht = drive(amplitude=fa, phase=0, detuning=fb, wires=1)
+        Ht = drive(amplitude=fa, phase=0, wires=1)
 
         dev = qml.device("default.qubit", wires=wires)
 
@@ -657,7 +570,7 @@ class TestIntegration:
             qml.evolve(Hd + Ht)(params, ts)
             return qml.expval(H_obj)
 
-        params = (jnp.ones(5), jnp.array([1.0, jnp.pi]))
+        params = (jnp.ones(5), jnp.array([1.0]))
         res = qnode(params)
         res_jit = qnode_jit(params)
 
@@ -676,18 +589,12 @@ class TestIntegration:
         def fa(p, t):
             return jnp.polyval(p, t)
 
-        def fb(p, t):
-            return p[0] * jnp.sin(p[1] * t)
-
         def fc(p, t):
             return p[0] * jnp.sin(t) + jnp.cos(p[1] * t)
 
-        def fd(p, t):
-            return p * jnp.cos(t)
-
-        H1 = drive(amplitude=fa, phase=0, detuning=fb, wires=wires)
-        H2 = drive(amplitude=fc, phase=3 * jnp.pi, detuning=0, wires=4)
-        H3 = drive(amplitude=0, phase=0, detuning=fd, wires=[3, 0])
+        H1 = drive(amplitude=fa, phase=0, wires=wires)
+        H2 = drive(amplitude=fc, phase=3 * jnp.pi, wires=4)
+        H3 = drive(amplitude=1.0, phase=0, wires=[3, 0])
 
         dev = qml.device("default.qubit", wires=wires)
 
@@ -706,10 +613,8 @@ class TestIntegration:
             return qml.expval(H_obj)
 
         params = (
-            jnp.ones(5),
             jnp.array([1.0, jnp.pi]),
             jnp.array([jnp.pi / 2, 0.5]),
-            jnp.array(-0.5),
         )
         res = qnode(params)
         res_jit = qnode_jit(params)
@@ -732,10 +637,7 @@ class TestIntegration:
         def fb(p, t):
             return p[0] * jnp.sin(p[1] * t)
 
-        def fc(p, t):
-            return p[0] * jnp.sin(t) + jnp.cos(p[1] * t)
-
-        H_drive = drive(amplitude=fa, phase=fb, detuning=fc, wires=1)
+        H_drive = drive(amplitude=fa, phase=fb, wires=1)
 
         dev = qml.device("default.qubit", wires=wires)
 
@@ -753,7 +655,7 @@ class TestIntegration:
             qml.evolve(H_drift + H_drive)(params, ts)
             return qml.expval(H_obj)
 
-        params = (jnp.ones(5), jnp.array([1.0, jnp.pi]), jnp.array([jnp.pi / 2, 0.5]))
+        params = (jnp.ones(5), jnp.array([1.0, jnp.pi]))
         res = qnode(params)
         res_jit = qnode_jit(params)
 
diff --git a/tests/pulse/test_parametrized_hamiltonian_pytree.py b/tests/pulse/test_parametrized_hamiltonian_pytree.py
index 4d43bf190..0657f36c3 100644
--- a/tests/pulse/test_parametrized_hamiltonian_pytree.py
+++ b/tests/pulse/test_parametrized_hamiltonian_pytree.py
@@ -44,9 +44,10 @@ def f2(p, t):
     return np.cos(p * t**2)
 
 
-PH = qml.dot([1, 2, f1, f2], [qml.PauliX(0), qml.PauliY(1), qml.PauliZ(2), qml.Hadamard(3)])
+PH = qml.dot([1, f1, f2], [qml.PauliX(0), qml.PauliY(1), qml.Hadamard(3)])
 
-RH = drive(amplitude=f1, phase=f2, detuning=1, wires=[0, 1, 2])
+RH = drive(amplitude=f1, phase=f2, wires=[0, 1, 2])
+RH += qml.dot([1.0], [qml.PauliZ(0)])
 
 # Hamiltonians and the parameters for the individual coefficients
 HAMILTONIANS_WITH_COEFF_PARAMETERS = [
@@ -64,8 +65,8 @@ HAMILTONIANS_WITH_COEFF_PARAMETERS = [
 class TestParametrizedHamiltonianPytree:
     """Unit tests for the ParametrizedHamiltonianPytree class."""
 
-    @pytest.mark.parametrize("H, fn, coeffs, params", HAMILTONIANS_WITH_COEFF_PARAMETERS)
-    def test_attributes(self, H, fn, coeffs, params):
+    @pytest.mark.parametrize("H, fn, coeffs_callable, params", HAMILTONIANS_WITH_COEFF_PARAMETERS)
+    def test_attributes(self, H, fn, coeffs_callable, params):
         """Test that the attributes of the ParametrizedHamiltonianPytree class are initialized
         correctly."""
         from jax.experimental import sparse
@@ -78,7 +79,7 @@ class TestParametrizedHamiltonianPytree:
         assert isinstance(H_pytree.mats_parametrized, tuple)
         assert qml.math.allclose(
             [c(p, 2) for c, p in zip(H_pytree.coeffs_parametrized, params)],
-            [c(p, 2) for c, p in zip(coeffs, params)],
+            [c(p, 2) for c, p in zip(coeffs_callable, params)],
             atol=1e-7,
         )
         assert H_pytree.reorder_fn == fn
@@ -108,7 +109,7 @@ class TestParametrizedHamiltonianPytree:
         assert qml.math.allclose(
             res.coeffs,
             (
-                1,
+                1.0,
                 amplitude_and_phase(jnp.cos, f1, f2)(params, time),
                 amplitude_and_phase(jnp.sin, f1, f2)(params, time),
             ),
diff --git a/tests/pulse/test_rydberg.py b/tests/pulse/test_rydberg.py
index 31338d2e0..b53933bde 100644
--- a/tests/pulse/test_rydberg.py
+++ b/tests/pulse/test_rydberg.py
@@ -22,6 +22,7 @@ import pennylane as qml
 from pennylane.pulse import HardwareHamiltonian, rydberg_interaction
 
 from pennylane.wires import Wires
+from pennylane.pulse.rydberg import RydbergSettings
 
 atom_coordinates = [[0, 0], [0, 5], [5, 0], [10, 5], [5, 10], [10, 10]]
 wires = [1, 6, 0, 2, 4, 3]
@@ -34,15 +35,15 @@ class TestRydbergInteraction:
         """Test that the attributes and the number of terms of the ``ParametrizedHamiltonian`` returned by
         ``rydberg_interaction`` are correct."""
         Hd = rydberg_interaction(register=atom_coordinates, wires=wires, interaction_coeff=1)
+        settings = RydbergSettings(atom_coordinates, 1)
 
         assert isinstance(Hd, HardwareHamiltonian)
-        assert Hd.interaction_coeff == 1
         assert Hd.wires == Wires(wires)
-        assert qml.math.allequal(Hd.register, atom_coordinates)
         N = len(wires)
         num_combinations = N * (N - 1) / 2  # number of terms on the rydberg_interaction hamiltonian
         assert len(Hd.ops) == num_combinations
         assert Hd.pulses == []
+        assert Hd.settings == settings
 
     def test_wires_is_none(self):
         """Test that when wires is None the wires correspond to an increasing list of values with
@@ -79,3 +80,52 @@ class TestRydbergInteraction:
         # Only 3 of the interactions will be non-negligible
         assert H_res.coeffs == [2.5**-6, 5**-6, 2.5**-6]
         assert qml.equal(H_res([], t=5), H_exp([], t=5))
+
+
+# For rydberg settings test
+register0 = [[0.0, 1.0], [0.0, 2.0]]
+register1 = [[2.0, 0.3], [1.0, 4.0], [0.5, 0.4]]
+
+
+class TestRydbergSettings:
+    """Unit tests for TransmonSettings dataclass"""
+
+    def test_init(self):
+        """Test the initialization of the ``RydbergSettings`` class."""
+        settings = RydbergSettings(register0)
+        assert settings.register == register0
+        assert settings.interaction_coeff == 0.0
+
+    def test_equal(self):
+        """Test the ``__eq__`` method of the ``RydbergSettings`` class."""
+        settings0 = RydbergSettings(register0)
+        settings1 = RydbergSettings(register1, interaction_coeff=2.0)
+        settings2 = RydbergSettings(register0, interaction_coeff=0.0)
+        assert settings0 != settings1
+        assert settings1 != settings2
+        assert settings0 == settings2
+
+    def test_add_two_settings(
+        self,
+    ):
+        """Test that two RydbergSettings are correctly added"""
+
+        settings0 = RydbergSettings(register0, interaction_coeff=2.0)
+        settings1 = None
+
+        settings01 = settings0 + settings1
+        settings10 = settings1 + settings0
+        assert settings01.register == register0
+        assert settings01.interaction_coeff == 2.0
+        assert settings10.register == register0
+        assert settings10.interaction_coeff == 2.0
+
+    # pylint: disable=unused-variable
+    def test_raises_error_two_interaction_terms(
+        self,
+    ):
+        """Raises error when attempting to add two non-trivial RydbergSettings"""
+        settings0 = RydbergSettings(register0)
+        settings1 = RydbergSettings(register1)
+        with pytest.raises(ValueError, match="Cannot add two"):
+            res = settings0 + settings1
diff --git a/tests/pulse/test_transmon.py b/tests/pulse/test_transmon.py
new file mode 100644
index 000000000..a207e1c04
--- /dev/null
+++ b/tests/pulse/test_transmon.py
@@ -0,0 +1,183 @@
+# Copyright 2018-2023 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""
+Unit tests for the HardwareHamiltonian class.
+"""
+# pylint: disable=too-few-public-methods,redefined-outer-name
+import numpy as np
+import pytest
+
+import pennylane as qml
+from pennylane.pulse import HardwareHamiltonian, transmon_interaction
+from pennylane.pulse.transmon import TransmonSettings, a, ad
+
+from pennylane.wires import Wires
+
+connections = [[0, 1], [1, 3], [2, 1], [4, 5]]
+wires = [0, 1, 2, 3, 4, 5]
+omega = 0.5 * np.arange(len(wires))
+g = 0.1 * np.arange(len(connections))
+anharmonicity = 0.3 * np.arange(len(wires))
+
+
+class TestTransmonInteraction:
+    """Unit tests for the ``transmon_interaction`` function."""
+
+    def test_attributes_and_number_of_terms(self):
+        """Test that the attributes and the number of terms of the ``ParametrizedHamiltonian`` returned by
+        ``transmon_interaction`` are correct."""
+        Hd = transmon_interaction(
+            connections=connections, omega=omega, g=g, anharmonicity=None, wires=wires, d=2
+        )
+        settings = TransmonSettings(connections, omega, g, anharmonicity=[0.0] * len(wires))
+
+        assert isinstance(Hd, HardwareHamiltonian)
+        assert Hd.settings == settings
+        assert Hd.wires == Wires(wires)
+
+        num_combinations = len(wires) + len(connections)
+        assert len(Hd.ops) == num_combinations
+        assert Hd.pulses == []
+
+    def test_coeffs(self):
+        """Test that the generated coefficients are correct."""
+        Hd = qml.pulse.transmon_interaction(
+            omega, connections, g, wires=wires, anharmonicity=anharmonicity, d=2
+        )
+        assert all(Hd.coeffs == np.concatenate([omega, g]))
+
+    @pytest.mark.skip
+    def test_coeffs_d(self):
+        """Test that generated coefficients are correct for d>2"""
+        Hd2 = qml.pulse.transmon_interaction(
+            omega=omega, connections=connections, g=g, wires=wires, anharmonicity=anharmonicity, d=3
+        )
+        assert all(Hd2.coeffs == np.concatenate([omega, g, anharmonicity]))
+
+    def test_float_omega_with_explicit_wires(self):
+        """Test that a single float omega with explicit wires yields the correct Hamiltonian"""
+        wires = range(6)
+        H = qml.pulse.transmon_interaction(omega=1.0, connections=connections, g=g, wires=wires)
+
+        assert H.coeffs[:6] == [1.0] * 6
+        assert all(H.coeffs[6:] == g)
+        for o1, o2 in zip(H.ops[:6], [ad(i, 2) @ a(i, 2) for i in wires]):
+            assert qml.equal(o1, o2)
+
+    def test_single_callable_omega_with_explicit_wires(self):
+        """Test that a single callable omega with explicit wires yields the correct Hamiltonian"""
+        wires0 = np.arange(10)
+        H = qml.pulse.transmon_interaction(
+            omega=np.polyval, connections=[[i, (i + 1) % 10] for i in wires0], g=0.5, wires=wires0
+        )
+
+        assert qml.math.allclose(H.coeffs[:10], 0.5)
+        assert H.coeffs[10:] == [np.polyval] * 10
+        for o1, o2 in zip(H.ops[10:], [ad(i, 2) @ a(i, 2) for i in wires0]):
+            assert qml.equal(o1, o2)
+
+    def test_d_neq_2_raises_error(self):
+        """Test that setting d != 2 raises error"""
+        with pytest.raises(NotImplementedError, match="Currently only supporting qubits."):
+            _ = transmon_interaction(connections=connections, omega=[0.1], wires=[0], g=0.2, d=3)
+
+    def test_wrong_g_len_raises_error(self):
+        """Test that providing list of g with wrong length raises error"""
+        with pytest.raises(ValueError, match="Number of coupling terms"):
+            _ = transmon_interaction(connections=connections, omega=[0.1], g=[0.2, 0.2], wires=[0])
+
+    def test_omega_and_wires_dont_match(self):
+        """Test that providing missmatching omega and wires raises error"""
+        with pytest.raises(ValueError, match="Number of qubit frequencies omega"):
+            _ = transmon_interaction(omega=[1, 2, 3], wires=[0, 1], connections=[], g=[])
+
+    def test_wires_and_connections_and_not_containing_each_other_raise_warning(
+        self,
+    ):
+        """Test that when wires and connections to not contain each other, a warning is raised"""
+        with pytest.warns(UserWarning, match="Caution, wires and connections do not match."):
+            _ = qml.pulse.transmon_interaction(
+                omega=0.5, connections=[[0, 1], [2, 3]], g=0.5, wires=[4, 5, 6]
+            )
+
+        with pytest.warns(UserWarning, match="Caution, wires and connections do not match."):
+            _ = qml.pulse.transmon_interaction(
+                omega=0.5, connections=[[0, 1], [2, 3]], g=0.5, wires=[0, 1, 2]
+            )
+
+        with pytest.warns(UserWarning, match="Caution, wires and connections do not match."):
+            connections = [["a", "b"], ["a", "c"], ["d", "e"], ["e", "f"]]
+            wires = ["a", "b", "c", "d", "e"]
+            omega = 0.5 * np.arange(len(wires))
+            g = 0.1 * np.arange(len(connections))
+
+            H = qml.pulse.transmon_interaction(omega, connections, g, wires)
+            assert H.wires == Wires(["a", "b", "c", "d", "e", "f"])
+
+
+# For transmon settings test
+connections0 = [[0, 1], [0, 2]]
+omega0 = [1.0, 2.0, 3.0]
+g0 = [0.5, 0.3]
+
+
+connections1 = [[2, 3], [1, 4], [5, 4]]
+omega1 = [4.0, 5.0, 6.0]
+g1 = [0.1, 0.2, 0.3]
+
+
+class TestTransmonSettings:
+    """Unit tests for TransmonSettings dataclass"""
+
+    def test_init(self):
+        """Test the initialization of the ``TransmonSettings`` class."""
+        settings = TransmonSettings(connections0, omega0, g0, [0.0] * len(omega0))
+        assert settings.connections == connections0
+        assert settings.omega == omega0
+        assert settings.g == g0
+        assert settings.anharmonicity == [0.0] * len(omega0)
+
+    def test_equal(self):
+        """Test the ``__eq__`` method of the ``TransmonSettings`` class."""
+        settings0 = TransmonSettings(connections0, omega0, g0, [0.0] * len(omega0))
+        settings1 = TransmonSettings(connections1, omega1, g1, [0.0] * len(omega1))
+        settings2 = TransmonSettings(connections0, omega0, g0, [0.0] * len(omega0))
+        assert settings0 != settings1
+        assert settings1 != settings2
+        assert settings0 == settings2
+
+    def test_add_two_settings(
+        self,
+    ):
+        """Test that two settings are correctly added"""
+        settings0 = TransmonSettings(connections0, omega0, g0, [0.0] * len(omega0))
+        settings1 = TransmonSettings(connections1, omega1, g1, [0.0] * len(omega1))
+        settings = settings0 + settings1
+        assert settings.connections == connections0 + connections1
+        assert settings.omega == omega0 + omega1
+        assert settings.g == g0 + g1
+
+    def test_add_two_settings_with_one_anharmonicity_None(
+        self,
+    ):
+        """Test that two settings are correctly added when one has non-trivial anharmonicity"""
+        anharmonicity = [1.0] * len(omega0)
+        settings0 = TransmonSettings(connections0, omega0, g0, anharmonicity=anharmonicity)
+        settings1 = TransmonSettings(connections1, omega1, g1, [0.0] * len(omega1))
+
+        settings01 = settings0 + settings1
+        assert settings01.anharmonicity == anharmonicity + [0.0] * len(omega1)
+
+        settings10 = settings1 + settings0
+        assert settings10.anharmonicity == [0.0] * len(omega0) + anharmonicity