Skip to content

Commit 29575ac

Browse files
committed
Update rsync.py
- Simplified Rsync_aligner code and fixed issue where samples flanked by non-matched sync pulses would be returned as nans when converted between reference frames.
1 parent b7c94df commit 29575ac

1 file changed

Lines changed: 23 additions & 45 deletions

File tree

tools/rsync.py

Lines changed: 23 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# random inter-pulse intervals.
33
# https://pycontrol.readthedocs.io/en/latest/user-guide/synchronisation
44
# Dependencies: Python 3, Numpy, Matplotlib, Scikit-learn.
5-
# (c) Thomas Akam 2018-2023. Released under the GPL-3 open source licence.
5+
# (c) Thomas Akam 2018-2025. Released under the GPL-3 open source licence.
66

77
import numpy as np
88
import pylab as plt
@@ -72,9 +72,7 @@ def __init__(
7272
intervals_B = np.diff(pulse_times_B) * units_B # Inter-pulse intervals for sequence B
7373
intervals_B2 = intervals_B**2
7474
# Find alignments of chunks which minimise sum of squared errors.
75-
chunk_starts_A = np.arange(
76-
0, len(pulse_times_A) - chunk_size, chunk_size
77-
) # Start indices of each chunk of sequence A.
75+
chunk_starts_A = np.arange(0, len(pulse_times_A) - chunk_size, chunk_size) # Start indices of each chunk of A.
7876
chunk_starts_B = np.zeros(chunk_starts_A.shape, int) # Start indicies of corresponding chunks in B.
7977
chunk_min_mse = np.zeros(chunk_starts_A.shape) # Mean squared error for each chunks best alignment.
8078
chunk_2nd_mse = np.zeros(chunk_starts_A.shape) # Mean sqared error for each chunks 2nd best alignment.
@@ -100,37 +98,20 @@ def __init__(
10098
gmm.fit(log_mse)
10199
valid_matches = gmm.predict(log_mse) == np.argmin(gmm.means_) # True for chunks which are valid matches.
102100
# Make arrays of corresponding times.
103-
cor_times_A = np.full(pulse_times_B.shape, np.nan) # A pulse times corresponding to each B pulse.
104101
cor_times_B = np.full(pulse_times_A.shape, np.nan) # B pulse times corresponding to each A pulse.
105102
for csA, csB, valid in zip(chunk_starts_A, chunk_starts_B, valid_matches):
106103
if valid:
107-
cor_times_A[csB : csB + chunk_size] = pulse_times_A[csA : csA + chunk_size]
108104
cor_times_B[csA : csA + chunk_size] = pulse_times_B[csB : csB + chunk_size]
109-
# Store pulse times, their correspondences and units.
110-
self.pulse_times_A = pulse_times_A
111-
self.pulse_times_B = pulse_times_B
112-
self.cor_times_A = cor_times_A
113-
self.cor_times_B = cor_times_B
114-
self.units_A = units_A
115-
self.units_B = units_B
116-
# Compute variables used for extrapolating beyond first/last matching pulse.
117-
diff_cor_times_B = np.diff(cor_times_B)
118-
self.dAdB = np.sum(np.diff(pulse_times_A)[~np.isnan(diff_cor_times_B)]) / np.sum(
119-
diff_cor_times_B[~np.isnan(diff_cor_times_B)]
120-
) # Empirical units_A/units_B from matched inter-pulse intervals.
121-
matched_pulse_times_A = cor_times_A[~np.isnan(cor_times_A)]
122-
matched_pulse_times_B = cor_times_B[~np.isnan(cor_times_B)]
123-
self.first_matched_time_A = matched_pulse_times_A[0]
124-
self.last_matched_time_A = matched_pulse_times_A[-1]
125-
self.first_matched_time_B = matched_pulse_times_B[0]
126-
self.last_matched_time_B = matched_pulse_times_B[-1]
105+
# Store times of matched sync pulses.
106+
self.matched_times_A = pulse_times_A[~np.isnan(cor_times_B)]
107+
self.matched_times_B = cor_times_B[~np.isnan(cor_times_B)]
108+
# Store empirical units_A/units_B from matched inter-pulse intervals.
109+
self.dAdB = np.sum(np.diff(self.matched_times_A)) / np.sum(np.diff(self.matched_times_B))
127110
# Check quality of alignment.
128-
separation_OK = np.abs(gmm.means_[0] - gmm.means_[1])[0] > 3 * np.sum(
129-
np.sqrt(gmm.covariances_)
130-
) # Difference in GMM means > 3 x sum of standard deviations.
131-
order_OK = (np.nanmin(np.diff(cor_times_A)) > 0) and (
132-
np.nanmin(np.diff(cor_times_A)) > 0
133-
) # Corresponding times are monotonically increacing.
111+
# Check Difference in GMM means > 3 x sum of standard deviations.
112+
separation_OK = np.abs(gmm.means_[0] - gmm.means_[1])[0] > 3 * np.sum(np.sqrt(gmm.covariances_))
113+
# Check corresponding times are monotonically increacing.
114+
order_OK = np.all(np.diff(self.matched_times_A) > 0) and np.all(np.diff(self.matched_times_B) > 0)
134115
if not (separation_OK and order_OK):
135116
if raise_exception:
136117
raise RsyncError("No match found between inter-pulse interval sequences.")
@@ -146,7 +127,7 @@ def __init__(
146127
plt.xlabel("Log mean squared error")
147128
plt.ylabel("# chunks")
148129
plt.subplot2grid((3, 3), (0, 2), rowspan=1, colspan=1)
149-
timing_errors = np.diff(cor_times_A) - np.diff(pulse_times_B)
130+
timing_errors = np.diff(cor_times_B) - np.diff(pulse_times_A)
150131
plt.hist(timing_errors[~np.isnan(timing_errors)], 100)
151132
plt.yscale("log", nonpositive="clip")
152133
plt.xlabel("Inter-pulse interval\ndiscrepancy (ms)")
@@ -163,25 +144,25 @@ def A_to_B(self, times_A, extrapolate=True):
163144
before the first matched sync pulse and after the last matched sync pulse will be
164145
extrapolated, if False they will be nans.
165146
"""
166-
times_B = np.interp(times_A, self.pulse_times_A, self.cor_times_B, left=np.nan, right=np.nan)
147+
times_B = np.interp(times_A, self.matched_times_A, self.matched_times_B, left=np.nan, right=np.nan)
167148
if extrapolate:
168-
pf = times_A < self.first_matched_time_A # Mask indicating times pre first matched pulse.
169-
times_B[pf] = (times_A[pf] - self.first_matched_time_A) / self.dAdB + self.first_matched_time_B
170-
pl = times_A > self.last_matched_time_A # Mask indicating times post last matched pulse.
171-
times_B[pl] = (times_A[pl] - self.last_matched_time_A) / self.dAdB + self.last_matched_time_B
149+
pf = times_A < self.matched_times_A[0] # Mask indicating times pre first matched pulse.
150+
times_B[pf] = (times_A[pf] - self.matched_times_A[0]) / self.dAdB + self.matched_times_B[0]
151+
pl = times_A > self.matched_times_A[-1] # Mask indicating times post last matched pulse.
152+
times_B[pl] = (times_A[pl] - self.matched_times_A[-1]) / self.dAdB + self.matched_times_B[-1]
172153
return times_B
173154

174155
def B_to_A(self, times_B, extrapolate=True):
175156
"""Convert times in B reference frame to A reference frame. If extrapolate=True, times
176157
before the first matched sync pulse and after the last matched sync pulse will be
177158
extrapolated, if False they will be nans.
178159
"""
179-
times_A = np.interp(times_B, self.pulse_times_B, self.cor_times_A, left=np.nan, right=np.nan)
160+
times_A = np.interp(times_B, self.matched_times_B, self.matched_times_A, left=np.nan, right=np.nan)
180161
if extrapolate:
181-
pf = times_B < self.first_matched_time_B # Mask indicating times pre first matched pulse.
182-
times_A[pf] = (times_B[pf] - self.first_matched_time_B) * self.dAdB + self.first_matched_time_A
183-
pl = times_B > self.last_matched_time_B # Mask indicating times post last matched pulse.
184-
times_A[pl] = (times_B[pl] - self.last_matched_time_B) * self.dAdB + self.last_matched_time_A
162+
pf = times_B < self.matched_times_B[0] # Mask indicating times pre first matched pulse.
163+
times_A[pf] = (times_B[pf] - self.matched_times_B[0]) * self.dAdB + self.matched_times_A[0]
164+
pl = times_B > self.matched_times_B[-1] # Mask indicating times post last matched pulse.
165+
times_A[pl] = (times_B[pl] - self.matched_times_B[-1]) * self.dAdB + self.matched_times_A[-1]
185166
return times_A
186167

187168

@@ -194,10 +175,7 @@ def simulate_pulses(n_pulse=1000, interval=[100, 1900], units_B=2, noise_SD=2, m
194175
pulse_times_B = units_B * (pulse_times_A + np.cumsum(np.random.normal(scale=noise_SD, size=n_pulse)))
195176
if missing_pulses:
196177
pulse_times_A = np.hstack(
197-
[
198-
pulse_times_A[int(n_pulse * 0.05) : int(n_pulse * 0.21)],
199-
pulse_times_A[int(n_pulse * 0.33) :] + 2e5,
200-
]
178+
[pulse_times_A[int(n_pulse * 0.05) : int(n_pulse * 0.21)], pulse_times_A[int(n_pulse * 0.33) :]]
201179
)
202180
pulse_times_B = np.hstack(
203181
[

0 commit comments

Comments
 (0)