1616import os
1717from scipy .stats import circmean
1818
19+ UNIT_ANGLES = np .deg2rad ([45 , 135 , 0 , 90 ], dtype = 'float64' ) # -45, +45, 0, 90
20+ POL_PREFS = np .deg2rad ([0 , 90 , 180 , 270 , 45 , 135 , 225 , 315 ], dtype = 'float64' )
21+
22+ A = None
23+ A_inv = A
24+
1925
2026def act (s , log = True ):
2127 """
@@ -57,8 +63,7 @@ def synchronise(d, zero=True, unwrap=True):
5763 return d
5864
5965
60- def decode_sky_compass (po , n_sol = 8 , pol_prefs = np .radians ([0 , 90 , 180 , 270 , 45 , 135 , 225 , 315 ]),
61- polarisation = True , intensity = False ):
66+ def decode_sky_compass (po , n_sol = 8 , pol_prefs = POL_PREFS , polarisation = True , intensity = False ):
6267 """
6368 Sky compass decoding routine from Gkanias et al. (2019). This has been
6469 implemeneted from scratch using the equations presented as a demonstration
@@ -87,15 +92,17 @@ def decode_sky_compass(po, n_sol=8, pol_prefs=np.radians([0, 90, 180, 270, 45, 1
8792 for t in range (duration ): # For each timestep
8893
8994 # ensure that the response is in [0, 1]
90- # the highest observed value was 11986 (Thursday 17:20)
91- response = np .clip (po [:, t ] / 12000. , 0. , 1. )
92- if polarisation and not intensity :
93- angle , sigma = pol2sol (response , sol_prefs , pol_prefs , unit_tranform = unit2pol )
95+ # the highest observed value was 32768 (Thursday 17:20)
96+ response = np .clip (po [:, t ] / 33000. , 0. , 1. )
97+ if not polarisation and not intensity :
98+ angle , sigma = pol2eig (response , pol_prefs )
99+ elif polarisation and not intensity :
100+ angle , sigma = pol2sol (response , sol_prefs , pol_prefs , unit_transform = unit2pol )
94101 elif not polarisation and intensity :
95- angle , sigma = pol2sol (response , sol_prefs + np .pi , pol_prefs , unit_tranform = unit2int )
102+ angle , sigma = pol2sol (response , sol_prefs + np .pi , pol_prefs , unit_transform = unit2int )
96103 else :
97- a_pol , c_pol = pol2sol (response , sol_prefs , pol_prefs , unit_tranform = unit2pol )
98- a_int , c_int = pol2sol (response , sol_prefs + np .pi , pol_prefs , unit_tranform = unit2int )
104+ a_pol , c_pol = pol2sol (response , sol_prefs , pol_prefs , unit_transform = unit2pol )
105+ a_int , c_int = pol2sol (response , sol_prefs + np .pi , pol_prefs , unit_transform = unit2int )
99106 s_pol = 1. / c_pol ** 2
100107 s_int = 1. / c_int ** 2
101108
@@ -107,13 +114,53 @@ def decode_sky_compass(po, n_sol=8, pol_prefs=np.radians([0, 90, 180, 270, 45, 1
107114 return angular_outputs , confidence_outputs
108115
109116
110- def pol2sol (po , sol_prefs , pol_prefs , unit_tranform ):
117+ def pol2eig (po , pol_prefs ):
118+
119+ units , diodes = po .shape
120+
121+ # Get photodiode responses for each unit
122+ phi , d = np .array ([unit2eig (po [x ]) for x in range (units )]).T
123+
124+ pe = np .array ([np .cos (phi + pol_prefs ), np .sin (phi + pol_prefs ), np .zeros_like (phi )])
125+
126+ alpha = - pol_prefs
127+ gamma = np .full_like (alpha , np .deg2rad (45 ))
128+
129+ c = np .zeros ([3 , 3 , 8 ], dtype = 'float32' )
130+ e = np .zeros_like (pe )
131+ for i , a , g in zip (range (len (alpha )), alpha , gamma ):
132+ c [..., i ] = np .array ([[np .cos (a ), - np .sin (a ), 0 ], [np .sin (a ), np .cos (a ), 0 ], [0 , 0 , 1 ]])
133+ c [..., i ] = np .dot (c [..., i ], np .array ([[np .cos (g ), 0 , np .sin (g )], [0 , 1 , 0 ], [- np .sin (g ), 0 , np .cos (g )]]))
134+
135+ e [:, i ] = c [..., i ].dot (pe [:, i ])
136+
137+ eig = e .dot (e .T )
138+
139+ eigenvalues , eigenvectors = np .linalg .eigh (eig )
140+
141+ i_star = np .argmin (eigenvalues )
142+ s_1 , s_2 , s_3 = eigenvectors [i_star ]
143+
144+ # print(eigenvalues)
145+ # print(eigenvectors)
146+
147+ # import sys
148+ # sys.exit()
149+
150+ # Compute argument and magnitude of complex conjugate of R (Eq. (4))
151+ angle = np .arctan2 (s_2 , s_1 )
152+ confidence = 1.0
153+
154+ return angle , confidence
155+
156+
157+ def pol2sol (po , sol_prefs , pol_prefs , unit_transform ):
111158 units , diodes = po .shape
112159 n_sol = len (sol_prefs )
113160 n_pol = len (pol_prefs )
114161
115162 # Get photodiode responses for each unit
116- r_pol = np .array ([unit_tranform (po [x ]) for x in range (units )])
163+ r_pol = np .array ([unit_transform (po [x ]) for x in range (units )])
117164
118165 # Init for sum
119166 r_sol = np .zeros (n_sol )
@@ -136,23 +183,41 @@ def pol2sol(po, sol_prefs, pol_prefs, unit_tranform):
136183 return angle , confidence
137184
138185
186+ def unit2eig (unit_response ):
187+ f = unit_response
188+ # q1, q2, q3 = np.linalg.inv(a.T.dot(a)).dot(a.T).dot(f)
189+ q1 , q2 , q3 = A_inv .dot (f )
190+ # q1 = i_0 - i_90
191+ # q2 = i_45 - i_135
192+ # q3 = (i_0 + i_45 + i_90 + i_135) / 2
193+
194+ phi = 0.5 * np .arctan2 (q2 , q1 )
195+ d = np .sqrt (np .square (q1 ) + np .square (q2 )) / q3
196+
197+ # print(f"phi = {np.rad2deg(phi):.2f}, d = {d:.2f}, responses = {unit_response}")
198+
199+ return phi , d
200+
201+
139202def unit2pol (unit_response , log = True ):
140203 r_h , r_v = unit2vh (unit_response , log )
141204 return (r_h - r_v ) / (r_h + r_v )
142205
143206
144207def unit2int (unit_response , log = True ):
145208 r_h , r_v = unit2vh (unit_response , log )
146- r = (r_h + r_v ) / 2
209+ # r = (r_h + r_v) / 2
210+ r = np .sqrt (r_h ** 2 + r_v ** 2 )
147211 return r
148212
149213
150214def unit2vh (unit_response , log = True ):
215+ # -45, +45, 0, 90
151216 _ , _ , s_v , s_h = unit_response
152217 return act (s_h , log ), act (s_v , log )
153218
154219
155- def produce_plot (data_dictionary ):
220+ def produce_plot (data_dictionary , polarisation = True , intensity = False ):
156221
157222 mosaic = [["image" , "skycompass" ],
158223 ["image" , "imu_yaw" ],
@@ -174,6 +239,9 @@ def produce_plot(data_dictionary):
174239 corrected_skycompass_table = []
175240 corrected_imu_table = []
176241
242+ mse = []
243+
244+ print ("MAE:" , end = " " )
177245 for k in data_dictionary .keys ():
178246 data = data_dictionary [k ]
179247
@@ -185,7 +253,8 @@ def produce_plot(data_dictionary):
185253 po .append (data [k ])
186254
187255 po = np .array (po )
188- pol_sensor_angle = synchronise (np .array (decode_sky_compass (po , polarisation = True , intensity = False )[0 ]))
256+ pol_sensor_angle = synchronise (
257+ np .array (decode_sky_compass (po , polarisation = polarisation , intensity = intensity )[0 ]))
189258
190259 if max_length < len (imu ):
191260 max_length = len (imu )
@@ -201,8 +270,12 @@ def produce_plot(data_dictionary):
201270 plot_angles (max_length , data = pol_sensor_angle , ax = ax ["skycompass" ], x_ticks = False )
202271 plot_angles (max_length , data = imu , ax = ax ["imu_yaw" ], x_ticks = False )
203272
273+ mse .append (compare (pol_sensor_angle , imu ))
274+ print (f"{ np .rad2deg (mse [- 1 ]):.2f} " , end = "\t " )
275+
204276 corrected_skycompass_table .append (pol_sensor_angle )
205277 corrected_imu_table .append (imu )
278+ print (f"\n Mean MAE: { np .rad2deg (np .nanmean (mse )):.2f} +/- { np .rad2deg (np .nanstd (mse )):.2f} " )
206279
207280 # Compute mean angle over all reacordings at each timestep.
208281 sc_means = []
@@ -275,3 +348,20 @@ def plot_angles(max_length, data=None, ax=None, x_padding=0, y_padding=np.pi/18,
275348 ax .set_xlim ([- x_padding , max_length + x_padding - 1 ])
276349
277350 return ax
351+
352+
353+ def compare (x , ground_truth ):
354+ ang_distance = (x - ground_truth + np .pi ) % (2 * np .pi ) - np .pi
355+ return np .mean (np .abs (ang_distance ))
356+
357+
358+ def update_globals ():
359+ global A , A_inv
360+
361+ A = np .round (np .array ([np .cos (2 * UNIT_ANGLES ), np .sin (2 * UNIT_ANGLES ), np .ones_like (UNIT_ANGLES )]).T , decimals = 2 )
362+
363+ # A_inv = np.linalg.inv(A.T.dot(A)).dot(A.T)
364+ A_inv = np .linalg .pinv (A )
365+
366+
367+ update_globals ()
0 commit comments