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
77import numpy as np
88import 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\n discrepancy (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