diff --git a/.gitattributes b/.gitattributes
old mode 100644
new mode 100755
index 5ba79e1336fb7442cdb5f44c6acd150621148302..6ace612063f48e24b95b697a461f39bd10a2b911
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,3 +2,5 @@ tests/keplerian-orbits-1-0-2.h5 filter=lfs diff=lfs merge=lfs -text
 tests/esa-orbits-1-0-2.h5 filter=lfs diff=lfs merge=lfs -text
 tests/keplerian-orbits-2-0-dev.h5 filter=lfs diff=lfs merge=lfs -text
 tests/esa-trailing-orbits-2-0-dev.h5 filter=lfs diff=lfs merge=lfs -text
+tests/keplerian-fplan-1-1.h5 filter=lfs diff=lfs merge=lfs -text
+tests/esa-trailing-fplan-1-1.h5 filter=lfs diff=lfs merge=lfs -text
diff --git a/lisainstrument/instrument.py b/lisainstrument/instrument.py
old mode 100644
new mode 100755
index 1062db019c1f58802a8099bd4abb1acd737119f0..aa4ad91cea311f3105e19dd621c19077bbb28778
--- a/lisainstrument/instrument.py
+++ b/lisainstrument/instrument.py
@@ -74,7 +74,7 @@ class Instrument:
                  # Artifacts
                  glitches=None,
                  # Laser locking and frequency plan
-                 lock='N1-12', offsets_freqs='default',
+                 lock='N1-12', fplan='static',
                  # Laser sources
                  laser_asds=28.2, central_freq=2.816E14,
                  # Laser phase modulation
@@ -124,9 +124,8 @@ class Instrument:
             glitches: path to glitch file, or dictionary of glitch signals per injection point
             lock: pre-defined laser locking configuration ('N1-12' non-swap N1 with 12 primary laser),
                 or 'six' for 6 lasers locked on cavities, or a dictionary of locking conditions
-            offsets_freqs: dictionary of laser frequency offsets [Hz], or 'default'
-            # fplan: path to frequency-plan file, or dictionary of locking beatnote frequencies [Hz],
-            #     or None for a default set of constant locking beatnote frequencies
+            fplan: path to frequency-plan file, dictionary of locking beatnote frequencies [Hz],
+                or 'static' for a default set of constant locking beatnote frequencies
             laser_asds: dictionary of amplitude spectral densities for laser noise [Hz/sqrt(Hz)]
             central_freq: laser central frequency from which all offsets are computed [Hz]
             modulation_asds: dictionary of amplitude spectral densities for modulation noise
@@ -235,6 +234,7 @@ class Instrument:
         # Instrument topology
         self.central_freq = float(central_freq)
         self.init_lock(lock)
+        self.init_fplan(fplan)
 
         # Laser and modulation noise
         self.laser_asds = ForEachMOSA(laser_asds)
@@ -360,16 +360,6 @@ class Instrument:
         else:
             self.mosa_angles = ForEachMOSA(mosa_angles)
 
-        # Frequency plan
-        if offsets_freqs == 'default':
-            # Default based on default for LISANode
-            self.offsets_freqs = ForEachMOSA({
-                '12': 8.1E6, '23': 9.2E6, '31': 10.3E6,
-                '13': 1.4E6, '32': -11.6E6, '21': -9.5E6,
-            })
-        else:
-            self.offsets_freqs = ForEachMOSA(offsets_freqs)
-
         # Orbits, gravitational waves, glitches
         self.init_orbits(orbits, orbit_dataset)
         self.init_gws(gws)
@@ -470,15 +460,16 @@ class Instrument:
         """Initialize laser locking configuration."""
         if lock == 'six':
             logger.info("Using pre-defined locking configuration 'six'")
-            self.lock_config = 'six'
+            self.lock_config = None # not a standard lock config
             self.lock = {'12': 'cavity', '23': 'cavity', '31': 'cavity', '13': 'cavity', '32': 'cavity', '21': 'cavity'}
         elif isinstance(lock, str):
             logger.info("Using pre-defined locking configuration '%s'", lock)
+            self.lock_config = lock
             match = re.match(r'^(N[1-6])-(12|23|31|13|32|21)$', lock)
             if match:
-                self.lock_config = (match.group(1), match.group(2))
-                lock_12 = self.LOCK_TOPOLOGIES[self.lock_config[0]] # with 12 as primary
-                cycle = self.INDEX_CYCLES[self.lock_config[1]] # correspondance to lock_12
+                topology, primary = match.group(1), match.group(2)
+                lock_12 = self.LOCK_TOPOLOGIES[topology] # with 12 as primary
+                cycle = self.INDEX_CYCLES[primary] # correspondance to lock_12
                 self.lock = {mosa: lock_12[cycle[mosa]] for mosa in self.MOSAS}
             else:
                 raise ValueError(f"unsupported pre-defined locking configuration '{lock}'")
@@ -486,11 +477,68 @@ class Instrument:
             logger.info("Using explicit locking configuration '%s'", lock)
             if (set(lock.keys()) != set(self.MOSAS) or
                 set(lock.values()) != set(['cavity', 'distant', 'adjacent'])):
-                raise ValueError(f"invalid locking dictionary '{lock}'")
+                raise ValueError(f"invalid locking configuration '{lock}'")
+            self.lock_config = None
             self.lock = lock
         else:
             raise ValueError(f"invalid locking configuration '{lock}'")
 
+    def init_fplan(self, fplan):
+        """Initialize frequency plan.
+
+        Args:
+            fplan: `fplan` parameter, c.f. `__init__()`
+        """
+        if fplan == 'static':
+            logger.info("Using default set of locking beatnote frequencies")
+            self.fplan_file = None
+            self.fplan = ForEachMOSA({
+                '12': 8E6, '23': 9E6, '31': 10E6,
+                '13': -8.2E6, '32': -8.5E6, '21': -8.7E6,
+            })
+        elif isinstance(fplan, str):
+            logger.info("Using frequency-plan file '%s'", fplan)
+            self.fplan_file = fplan
+            # Without a standard lock config, there is no dataset
+            # in the frequency-plan file and therefore we cannot use it
+            if self.lock_config is None:
+                raise ValueError("cannot use frequency-plan for non standard lock configuration")
+            with File(self.fplan_file, 'r') as fplanf:
+                version = Version(fplanf.attrs['version'])
+                logger.debug("Using frequency-plan file version %s", version)
+                # Warn for frequency-plan file development version
+                if version.is_devrelease:
+                    logger.warning("You are using an frequency-plan file in a development version")
+                # Switch between various fplan file standards
+                if version in SpecifierSet('== 1.1.*', True):
+                    logger.debug("Interpolating locking beatnote frequencies with piecewise linear functions")
+                    times = self.t0 + np.arange(fplanf.attrs['size']) * fplanf.attrs['dt']
+                    interpolate = lambda x: InterpolatedUnivariateSpline(times, x, k=1, ext='raise')(self.physics_t)
+                    lock_beatnotes = {}
+                    # Go through all MOSAs and pick locking beatnotes
+                    try:
+                        for mosa in self.MOSAS:
+                            if self.lock[mosa] == 'cavity':
+                                # No offset for primary laser
+                                lock_beatnotes[mosa] = 0.0
+                            elif self.lock[mosa] == 'distant':
+                                lock_beatnotes[mosa] = 1E6 * interpolate(fplanf[self.lock_config][f'isi_{mosa}'])
+                            elif self.lock[mosa] == 'adjacent':
+                                # Fplan files only contain the one (left) RFI beatnote
+                                left_mosa = ForEachSC.left_mosa(ForEachMOSA.sc(mosa))
+                                sign = +1 if left_mosa == mosa else -1
+                                lock_beatnotes[mosa] = 1E6 * sign * interpolate(fplanf[self.lock_config][f'rfi_{left_mosa}'])
+                    except ValueError as error:
+                        logger.error("Missing frequency-plan information at \n%s")
+                        raise ValueError("missing frequency-plan information, use longer file or adjust sampling") from error
+                    self.fplan = ForEachMOSA(lock_beatnotes)
+                else:
+                    raise ValueError(f"unsupported frequency-plan file version '{version}'")
+        else:
+            logger.info("Using user-provided locking beatnote frequencies")
+            self.fplan_file = None
+            self.fplan = ForEachMOSA(fplan)
+
     def init_orbits(self, orbits, orbit_dataset):
         """Initialize orbits.
 
@@ -502,9 +550,9 @@ class Instrument:
             logger.info("Using default set of static proper pseudo-ranges")
             self.orbit_file = None
             self.pprs = ForEachMOSA({
-                # Default PPRs based on first samples of Keplerian orbits (v1.0)
-                '12': 8.3324, '23': 8.3028, '31': 8.3324,
-                '13': 8.3315, '32': 8.3044, '21': 8.3315,
+                # Default PPRs based on first samples of Keplerian orbits (v2.0.dev)
+                '12': 8.33242295, '23': 8.30282196, '31': 8.33242298,
+                '13': 8.33159404, '32': 8.30446786, '21': 8.33159402,
             })
             self.d_pprs = ForEachMOSA(0)
             self.tps_proper_time_deviations = ForEachSC(0)
@@ -783,7 +831,7 @@ class Instrument:
         logger.info("Simulating local beams")
         self.simulate_locking()
 
-        ## simulate sidebands
+        ## Simulate sidebands
 
         logger.debug("Computing upper sideband offsets for primary local beam")
         self.local_usb_offsets = self.local_carrier_offsets \
@@ -1418,7 +1466,7 @@ class Instrument:
         self.laser_noises[mosa] = noises.laser(self.physics_fs, self.physics_size, self.laser_asds[mosa])
 
         logger.debug("Computing carrier offsets for primary local beam %s", mosa)
-        self.local_carrier_offsets[mosa] = self.offsets_freqs[mosa]
+        self.local_carrier_offsets[mosa] = 0.0
 
         logger.debug("Computing carrier fluctuations for primary local beam %s", mosa)
         self.local_carrier_fluctuations[mosa] = \
@@ -1438,14 +1486,14 @@ class Instrument:
                      "locked on adjacent beam %s", mosa, adjacent(mosa))
         self.local_carrier_offsets[mosa] = \
             self.local_carrier_offsets[adjacent(mosa)] \
-            + self.offsets_freqs[mosa] * (1 + self.clock_noise_offsets[sc(mosa)])
+            - self.fplan[mosa] * (1 + self.clock_noise_offsets[sc(mosa)])
 
         logger.debug("Computing carrier fluctuations for local beam %s "
                      "locked on adjacent beam %s", mosa, adjacent(mosa))
         adjacent_carrier_fluctuations = self.local_carrier_fluctuations[adjacent(mosa)] \
             + self.central_freq * self.backlink_noises[mosa]
         self.local_carrier_fluctuations[mosa] = adjacent_carrier_fluctuations \
-            + self.offsets_freqs[mosa] * self.clock_noise_fluctuations[sc(mosa)] \
+            - self.fplan[mosa] * self.clock_noise_fluctuations[sc(mosa)] \
             + self.central_freq * self.oms_rfi_carrier_noises[mosa] \
             + self.tdir_tones[mosa]
 
@@ -1466,7 +1514,7 @@ class Instrument:
             -self.d_pprs[mosa] * self.central_freq \
             + (1 - self.d_pprs[mosa]) * self.interpolate(carrier_offsets, -self.pprs[mosa])
         self.local_carrier_offsets[mosa] = distant_carrier_offsets \
-            + self.offsets_freqs[mosa] * (1 + self.clock_noise_offsets[sc(mosa)])
+            - self.fplan[mosa] * (1 + self.clock_noise_offsets[sc(mosa)])
 
         logger.debug("Computing carrier fluctuations for local beam %s "
                      "locked on distant beam %s", mosa, distant(mosa))
@@ -1479,7 +1527,7 @@ class Instrument:
             - (self.central_freq + self.local_carrier_offsets[mosa]) * self.gws[mosa] \
             - (self.central_freq + self.local_carrier_offsets[mosa]) * self.local_ttls[mosa] / c
         self.local_carrier_fluctuations[mosa] = distant_carrier_fluctuations \
-            + self.offsets_freqs[mosa] * self.clock_noise_fluctuations[sc(mosa)] \
+            - self.fplan[mosa] * self.clock_noise_fluctuations[sc(mosa)] \
             + self.central_freq * self.oms_isi_carrier_noises[mosa] \
             + self.tdir_tones[mosa]
 
diff --git a/tests/esa-trailing-fplan-1-1.h5 b/tests/esa-trailing-fplan-1-1.h5
new file mode 100755
index 0000000000000000000000000000000000000000..1ed838af44e8741c0317b94b103b1f416bf1cbad
--- /dev/null
+++ b/tests/esa-trailing-fplan-1-1.h5
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:91cdf77a8b830999d7409d35e2cb5d44870eeaba9a86d516b5c6e8f87e75fa7a
+size 22160440
diff --git a/tests/keplerian-fplan-1-1.h5 b/tests/keplerian-fplan-1-1.h5
new file mode 100755
index 0000000000000000000000000000000000000000..895e915979e8c6268ac53e9ef129c959ac221210
--- /dev/null
+++ b/tests/keplerian-fplan-1-1.h5
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f42b997aef2f2820582aaf35e102c16e59efc0d93f6379fa3355f4821ef810e2
+size 20610648
diff --git a/tests/test_fplan.py b/tests/test_fplan.py
new file mode 100755
index 0000000000000000000000000000000000000000..23d18f4cff1e1c94a29eb31c9402c4b5f2d76c58
--- /dev/null
+++ b/tests/test_fplan.py
@@ -0,0 +1,247 @@
+#! /usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""Test the usage of frequency plans."""
+
+import numpy as np
+import pytest
+
+from h5py import File
+from scipy.interpolate import interp1d
+from lisainstrument import Instrument
+
+
+def _consistent_locking_beatnotes(instru, fplan=None):
+    """Check that TPS locking beatnotes are consistent with ``fplan``.
+
+    We check that locking beatnotes are equal up to machine precision.
+
+    Args:
+        instru (:class:`lisainstrument.Instrument`): instrument object
+        fplan (dict or None): dictionary of locking beatnotes [Hz] or None to read fplan file
+
+    Returns:
+        (bool) Whether the locking beatnotes are valid.
+    """
+    # Read fplan file
+    if fplan is None:
+        with File(instru.fplan_file, 'r') as fplanf:
+            t = np.arange(fplanf.attrs['size']) * fplanf.attrs['dt']
+            if instru.lock_config == 'N1-12':
+                fplan = {
+                    '13': interp1d(t, -fplanf[instru.lock_config]['rfi_12'][:] * 1E6)(instru.physics_t),
+                    '31': interp1d(t, fplanf[instru.lock_config]['isi_31'][:] * 1E6)(instru.physics_t),
+                    '32': interp1d(t, -fplanf[instru.lock_config]['rfi_31'][:] * 1E6)(instru.physics_t),
+                    '21': interp1d(t, fplanf[instru.lock_config]['isi_21'][:] * 1E6)(instru.physics_t),
+                    '23': interp1d(t, fplanf[instru.lock_config]['rfi_23'][:] * 1E6)(instru.physics_t),
+                }
+            elif instru.lock_config == 'N1-21':
+                fplan = {
+                    '23': interp1d(t, fplanf[instru.lock_config]['rfi_23'][:] * 1E6)(instru.physics_t),
+                    '32': interp1d(t, fplanf[instru.lock_config]['isi_32'][:] * 1E6)(instru.physics_t),
+                    '31': interp1d(t, fplanf[instru.lock_config]['rfi_31'][:] * 1E6)(instru.physics_t),
+                    '12': interp1d(t, fplanf[instru.lock_config]['isi_12'][:] * 1E6)(instru.physics_t),
+                    '13': interp1d(t, -fplanf[instru.lock_config]['rfi_12'][:] * 1E6)(instru.physics_t),
+                }
+            elif instru.lock_config == 'N4-12':
+                fplan = {
+                    '13': interp1d(t, -fplanf[instru.lock_config]['rfi_12'][:] * 1E6)(instru.physics_t),
+                    '31': interp1d(t, fplanf[instru.lock_config]['isi_31'][:] * 1E6)(instru.physics_t),
+                    '32': interp1d(t, -fplanf[instru.lock_config]['rfi_31'][:] * 1E6)(instru.physics_t),
+                    '23': interp1d(t, fplanf[instru.lock_config]['isi_23'][:] * 1E6)(instru.physics_t),
+                    '21': interp1d(t, fplanf[instru.lock_config]['isi_21'][:] * 1E6)(instru.physics_t),
+                }
+    # Check locking beatnotes
+    if instru.lock_config == 'N1-12':
+        return np.all([
+            np.allclose(instru.tps_rfi_carrier_offsets['13'], fplan['13']),
+            np.allclose(instru.tps_isi_carrier_offsets['31'], fplan['31']),
+            np.allclose(instru.tps_rfi_carrier_offsets['32'], fplan['32']),
+            np.allclose(instru.tps_isi_carrier_offsets['21'], fplan['21']),
+            np.allclose(instru.tps_rfi_carrier_offsets['23'], fplan['23']),
+        ])
+    if instru.lock_config == 'N1-21':
+        return np.all([
+            np.allclose(instru.tps_rfi_carrier_offsets['23'], fplan['23']),
+            np.allclose(instru.tps_isi_carrier_offsets['32'], fplan['32']),
+            np.allclose(instru.tps_rfi_carrier_offsets['31'], fplan['31']),
+            np.allclose(instru.tps_isi_carrier_offsets['12'], fplan['12']),
+            np.allclose(instru.tps_rfi_carrier_offsets['13'], fplan['13']),
+        ])
+    if instru.lock_config == 'N4-12':
+        return np.all([
+            np.allclose(instru.tps_rfi_carrier_offsets['13'], fplan['13']),
+            np.allclose(instru.tps_isi_carrier_offsets['31'], fplan['31']),
+            np.allclose(instru.tps_rfi_carrier_offsets['32'], fplan['32']),
+            np.allclose(instru.tps_isi_carrier_offsets['23'], fplan['23']),
+            np.allclose(instru.tps_isi_carrier_offsets['21'], fplan['21']),
+        ])
+    raise ValueError(f"unsupported lock configuration '{instru.lock_config}'")
+
+def test_static_fplan():
+    """Test the default static set of locking beatnotes."""
+
+    # Check fplan initialization
+    instru = Instrument(size=100, fplan='static')
+    static = {
+        '12': 8E6, '23': 9E6, '31': 10E6,
+        '13': -8.2E6, '32': -8.5E6, '21': -8.7E6,
+    }
+    for mosa in instru.MOSAS:
+        assert instru.fplan[mosa] == static[mosa]
+
+    # Check locking beatnotes
+    instru = Instrument(size=100, lock='N1-12', fplan='static')
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, static)
+    instru = Instrument(size=100, lock='N1-21', fplan='static')
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, static)
+    instru = Instrument(size=100, lock='N4-12', fplan='static')
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, static)
+
+def test_static_fplan_valid_with_all_lock_configs():
+    """Check that 'static' fplan is valid for all lock configs.
+
+    We check that all beatnotes are between 5 and 25 MHz (and negative)
+    for all locking configurations, using the default static set of locking
+    beatnotes.
+    """
+    for primary in Instrument.MOSAS:
+        for topology in Instrument.LOCK_TOPOLOGIES:
+            instru = Instrument(size=100, lock=f'{topology}-{primary}', fplan='static')
+            instru.disable_all_noises()
+            instru.simulate()
+
+            for mosa in instru.MOSAS:
+                assert np.all(5E6 < np.abs(instru.isi_carriers[mosa]) < 25E6)
+                assert np.all(5E6 < np.abs(instru.isi_usbs[mosa]) < 25E6)
+                assert np.all(5E6 < np.abs(instru.rfi_carriers[mosa]) < 25E6)
+                assert np.all(5E6 < np.abs(instru.rfi_usbs[mosa]) < 25E6)
+
+def test_constant_equal_fplan():
+    """Test a user-defined constant equal fplan."""
+
+    # Check fplan initialization
+    instru = Instrument(size=100, fplan=42.0)
+    for mosa in instru.MOSAS:
+        assert instru.fplan[mosa] == 42.0
+
+def test_constant_unequal_fplan():
+    """Test a user-defined constant unequal fplan."""
+
+    # Check fplan initialization
+    fplan = {
+        '12': 8.1E6, '23': 9.2E6, '31': 10.3E6,
+        '13': 1.4E6, '32': -11.6E6, '21': -9.5E6,
+    }
+    instru = Instrument(size=100, lock='N1-12', fplan=fplan)
+    for mosa in instru.MOSAS:
+        assert instru.fplan[mosa] == fplan[mosa]
+
+    # Check locking beatnotes
+    instru = Instrument(size=100, lock='N1-12', fplan=fplan)
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, fplan)
+    instru = Instrument(size=100, lock='N1-21', fplan=fplan)
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, fplan)
+    instru = Instrument(size=100, lock='N4-12', fplan=fplan)
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, fplan)
+
+def test_varying_equal_fplan():
+    """Test a user-defined time-varying equal fplan."""
+
+    # Check fplan initialization
+    fplan = np.random.uniform(5E6, 25E6, size=400)
+    instru = Instrument(size=100, fplan=fplan)
+    for mosa in instru.MOSAS:
+        assert np.all(instru.fplan[mosa] == fplan)
+
+    # Check locking beatnotes
+    fplan = {mosa: fplan for mosa in Instrument.MOSAS}
+    instru = Instrument(size=100, lock='N1-12', fplan=fplan)
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, fplan)
+    instru = Instrument(size=100, lock='N1-21', fplan=fplan)
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, fplan)
+    instru = Instrument(size=100, lock='N4-12', fplan=fplan)
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, fplan)
+
+def test_varying_unequal_fplan():
+    """Test a user-defined time-varying unequal fplan."""
+
+    # Check fplan initialization
+    fplan = {
+        mosa: np.random.uniform(5E6, 25E6, size=400)
+        for mosa in Instrument.MOSAS
+    }
+    instru = Instrument(size=100, fplan=fplan)
+    for mosa in instru.MOSAS:
+        assert np.all(instru.fplan[mosa] == fplan[mosa])
+
+    # Check locking beatnotes
+    instru = Instrument(size=100, lock='N1-12', fplan=fplan)
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, fplan)
+    instru = Instrument(size=100, lock='N1-21', fplan=fplan)
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, fplan)
+    instru = Instrument(size=100, lock='N4-12', fplan=fplan)
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru, fplan)
+
+def test_keplerian_fplan_1_1():
+    """Test standard Keplerian fplan file v1.1."""
+
+    # Check fplan file with standard lock configs
+    for primary in Instrument.MOSAS:
+        for topology in Instrument.LOCK_TOPOLOGIES:
+            instru = Instrument(size=100, lock=f'{topology}-{primary}', fplan='tests/keplerian-fplan-1-1.h5')
+
+    # Should raise an error for non-standard lock config
+    with pytest.raises(ValueError):
+        Instrument(size=100, lock='six', fplan='tests/keplerian-fplan-1-1.h5')
+    with pytest.raises(ValueError):
+        lock = {'12': 'cavity', '13': 'cavity', '21': 'distant', '31': 'distant', '23': 'adjacent', '32': 'adjacent'}
+        Instrument(size=100, lock=lock, fplan='tests/keplerian-fplan-1-1.h5')
+
+    # Check locking beatnotes
+    instru = Instrument(size=100, lock='N1-12', fplan='tests/keplerian-fplan-1-1.h5')
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru)
+    instru = Instrument(size=100, lock='N1-21', fplan='tests/keplerian-fplan-1-1.h5')
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru)
+    instru = Instrument(size=100, lock='N4-12', fplan='tests/keplerian-fplan-1-1.h5')
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru)
+
+def test_esa_trailing_fplan_1_1():
+    """Test standard ESA trailing fplan file v1.1."""
+
+    # Check fplan file with standard lock configs
+    for primary in Instrument.MOSAS:
+        for topology in Instrument.LOCK_TOPOLOGIES:
+            instru = Instrument(size=100, lock=f'{topology}-{primary}', fplan='tests/esa-trailing-fplan-1-1.h5')
+
+    # Should raise an error for non-standard lock config
+    with pytest.raises(ValueError):
+        Instrument(size=100, lock='six', fplan='tests/esa-trailing-fplan-1-1.h5')
+    with pytest.raises(ValueError):
+        lock = {'12': 'cavity', '13': 'cavity', '21': 'distant', '31': 'distant', '23': 'adjacent', '32': 'adjacent'}
+        Instrument(size=100, lock=lock, fplan='tests/esa-trailing-fplan-1-1.h5')
+
+    # Check locking beatnotes
+    instru = Instrument(size=100, lock='N1-12', fplan='tests/esa-trailing-fplan-1-1.h5')
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru)
+    instru = Instrument(size=100, lock='N1-21', fplan='tests/esa-trailing-fplan-1-1.h5')
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru)
+    instru = Instrument(size=100, lock='N4-12', fplan='tests/esa-trailing-fplan-1-1.h5')
+    instru.simulate()
+    assert _consistent_locking_beatnotes(instru)
diff --git a/tests/test_instrument.py b/tests/test_instrument.py
old mode 100644
new mode 100755