diff --git a/lisainstrument/instrument.py b/lisainstrument/instrument.py old mode 100644 new mode 100755 index 1062db019c1f58802a8099bd4abb1acd737119f0..ee86523f4ae6475e32bbcffb0b6d33aec6c92f45 --- 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) @@ -474,11 +464,12 @@ class Instrument: 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,65 @@ 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({ + # TODO: document this default set + '12': 8.1E6, '23': 9.2E6, '31': 10.3E6, + '13': 1.4E6, '32': -11.6E6, '21': -9.5E6, + }) + 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 orbit 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 + 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] = 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] = interpolate(sign * fplanf[self.lock_config][f'rfi_{left_mosa}']) + 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. @@ -1418,7 +1463,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 +1483,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 +1511,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 +1524,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]