33# Copyright the MNE-Python contributors.
44
55from collections import Counter
6+ from copy import deepcopy
67from functools import partial
78from math import factorial
89from os import path as op
@@ -498,6 +499,30 @@ def _prep_maxwell_filter(
498499 add_channels = (head_pos is not None ) and (not st_only )
499500 head_pos = _check_pos (head_pos , coord_frame , raw , st_fixed )
500501 mc = _MoveComp (head_pos , coord_frame , raw , mc_interp , reconstruct )
502+
503+ # cross_talk=None (or True) means "use built-in ones if in info"
504+ _validate_type (cross_talk , (None , bool , dict , "path-like" ))
505+ _validate_type (calibration , (None , bool , dict , "path-like" ))
506+ if cross_talk is None :
507+ cross_talk = raw .info .get ("cross_talk" )
508+ elif cross_talk is True :
509+ if "cross_talk" not in raw .info :
510+ raise RuntimeError (f"{ cross_talk = } , but info['cross_talk'] is None." )
511+ cross_talk = raw .info ["cross_talk" ]
512+ elif cross_talk is False :
513+ cross_talk = None
514+ # otherwise, it's path-like
515+
516+ if calibration is None :
517+ calibration = raw .info .get ("fine_calibration" )
518+ elif calibration is True :
519+ if "fine_calibration" not in raw .info :
520+ raise RuntimeError (f"{ calibration = } , but info['fine_calibration'] is None." )
521+ calibration = raw .info ["fine_calibration" ]
522+ elif calibration is False :
523+ calibration = None
524+ # otherwise, it's path-like
525+
501526 _check_info (
502527 raw .info ,
503528 sss = not st_only ,
@@ -2174,6 +2199,31 @@ def _overlap_projector(data_int, data_res, corr):
21742199 return V_principal
21752200
21762201
2202+ def _reformat_fine_cal_dict (fine_cal ):
2203+ # reformat a possible dict with "cal_chans", "cal_corrs" keys to more standard one
2204+ # with "ch_names", "locs", "imb_cals" keys
2205+ if "cal_chans" in fine_cal :
2206+ # Someday we might need to refactor this for other systems, but we should do
2207+ # that when we start building in fine cal to them during acq (probably never)
2208+ _GRAD_TYPES = tuple (
2209+ getattr (FIFF , f"FIFFV_COIL_VV_PLANAR_{ t } " )
2210+ for t in ("T1" , "T2" , "T3" , "T4" , "W" )
2211+ )
2212+ ch_names = [f"MEG{ ch_num :04d} " for ch_num in fine_cal ["cal_chans" ][:, 0 ]]
2213+ locs = fine_cal ["cal_corrs" ][:, - 12 :].astype (float )
2214+ coil_types = fine_cal ["cal_chans" ][:, 1 ]
2215+ is_grad = np .isin (coil_types , _GRAD_TYPES )
2216+ starts = np .where (is_grad , 1 , 0 )
2217+ stops = np .where (is_grad , fine_cal ["cal_corrs" ].shape [1 ] - 12 , 1 )
2218+ imb_cals = list (
2219+ fine_cal ["cal_corrs" ][ii , start :stop ].astype (float )
2220+ for ii , (start , stop ) in enumerate (zip (starts , stops ))
2221+ )
2222+ return dict (ch_names = ch_names , locs = locs , imb_cals = imb_cals )
2223+ else :
2224+ return deepcopy (fine_cal )
2225+
2226+
21772227def _prep_fine_cal (info , fine_cal , * , ignore_ref ):
21782228 from ._fine_cal import read_fine_calibration
21792229
@@ -2183,6 +2233,7 @@ def _prep_fine_cal(info, fine_cal, *, ignore_ref):
21832233 fine_cal = read_fine_calibration (fine_cal )
21842234 else :
21852235 extra = "dict"
2236+ fine_cal = _reformat_fine_cal_dict (fine_cal )
21862237 logger .info (f" Using fine calibration { extra } " )
21872238 ch_names = _clean_names (info ["ch_names" ], remove_whitespace = True )
21882239 info_to_cal = dict ()
@@ -2230,12 +2281,12 @@ def _update_sensor_geometry(info, fine_cal, ignore_ref):
22302281 )
22312282
22322283 # Replace sensor locations (and track differences) for fine calibration
2233- ang_shift = list ()
22342284 used = np .zeros (len (info ["chs" ]), bool )
22352285 cal_corrs = list ()
22362286 cal_chans = list ()
22372287 adjust_logged = False
2238- for oi , ci in info_to_cal .items ():
2288+ ang_shift = np .zeros (len (info_to_cal ))
2289+ for ii , (oi , ci ) in enumerate (info_to_cal .items ()):
22392290 assert not used [oi ]
22402291 used [oi ] = True
22412292 info_ch = info ["chs" ][oi ]
@@ -2250,50 +2301,47 @@ def _update_sensor_geometry(info, fine_cal, ignore_ref):
22502301 # EX and EY are orthogonal to EZ. If not, we find the rotation between
22512302 # the original and fine-cal ez, and rotate EX and EY accordingly:
22522303 ch_coil_rot = _loc_to_coil_trans (info_ch ["loc" ])[:3 , :3 ]
2304+ _normalize_vectors (ch_coil_rot .T ) # column-wise
22532305 cal_loc = fine_cal ["locs" ][ci ].copy ()
22542306 cal_coil_rot = _loc_to_coil_trans (cal_loc )[:3 , :3 ]
2255- if (
2256- np .max (
2257- [
2258- np .abs (np .dot (cal_coil_rot [:, ii ], cal_coil_rot [:, 2 ]))
2259- for ii in range (2 )
2260- ]
2261- )
2262- > 1e-6
2263- ): # X or Y not orthogonal
2307+ _normalize_vectors (cal_coil_rot .T )
2308+ # X or Y not orthogonal to Z:
2309+ if np .max (np .abs (cal_coil_rot [:, 2 ] @ cal_coil_rot [:, :2 ])) > 1e-5 :
22642310 if not adjust_logged :
22652311 logger .info (" Adjusting non-orthogonal EX and EY" )
22662312 adjust_logged = True
22672313 # find the rotation matrix that goes from one to the other
2268- this_trans = _find_vector_rotation (ch_coil_rot [:, 2 ], cal_coil_rot [:, 2 ])
2269- cal_loc [3 :] = np .dot (this_trans , ch_coil_rot ).T .ravel ()
2314+ R = _find_vector_rotation (ch_coil_rot [:, 2 ], cal_coil_rot [:, 2 ])
2315+ cal_coil_rot [:] = R @ ch_coil_rot
2316+ _normalize_vectors (cal_coil_rot .T )
2317+ cal_loc [3 :9 ] = cal_coil_rot .T .ravel ()[:6 ] # set just X and Y in output
22702318
22712319 # calculate shift angle
2272- v1 = _loc_to_coil_trans (cal_loc )[:3 , :3 ]
2273- _normalize_vectors (v1 )
2274- v2 = _loc_to_coil_trans (info_ch ["loc" ])[:3 , :3 ]
2275- _normalize_vectors (v2 )
2276- ang_shift .append (np .sum (v1 * v2 , axis = 0 ))
2320+ v2 = _loc_to_coil_trans (info_ch ["loc" ])[:3 , 2 :]
2321+ _normalize_vectors (v2 .T )
2322+ ang_shift [ii ] = cal_coil_rot [:3 , 2 ] @ v2 [:, 0 ]
22772323 if oi in grad_picks :
22782324 extra = [1.0 , fine_cal ["imb_cals" ][ci ][0 ]]
22792325 else :
22802326 extra = [fine_cal ["imb_cals" ][ci ][0 ], 0.0 ]
22812327 cal_corrs .append (np .concatenate ([extra , cal_loc ]))
22822328 # Adjust channel normal orientations with those from fine calibration
22832329 # Channel positions are not changed
2284- info_ch ["loc" ][3 :] = cal_loc [ 3 :]
2330+ info_ch ["loc" ][3 :] = cal_coil_rot . T . ravel ()
22852331 assert info_ch ["coord_frame" ] == FIFF .FIFFV_COORD_DEVICE
22862332 meg_picks = pick_types (info , meg = True , exclude = (), ref_meg = not ignore_ref )
22872333 assert used [meg_picks ].all ()
22882334 assert not used [np .setdiff1d (np .arange (len (used )), meg_picks )].any ()
22892335 # This gets written to the Info struct
2290- sss_cal = dict (cal_corrs = np .array (cal_corrs ), cal_chans = np .array (cal_chans ))
2336+ sss_cal = dict (
2337+ cal_corrs = np .array (cal_corrs , float ),
2338+ cal_chans = np .array (cal_chans , int ),
2339+ )
22912340
22922341 # Log quantification of sensor changes
22932342 # Deal with numerical precision giving absolute vals slightly more than 1.
2294- ang_shift = np .array (ang_shift )
2295- np .clip (ang_shift , - 1.0 , 1.0 , ang_shift )
2296- np .rad2deg (np .arccos (ang_shift ), ang_shift ) # Convert to degrees
2343+ np .clip (ang_shift , - 1.0 , 1.0 , out = ang_shift )
2344+ np .rad2deg (np .arccos (ang_shift ), out = ang_shift ) # Convert to degrees
22972345 logger .info (
22982346 " Adjusted coil orientations by (μ ± σ): "
22992347 f"{ np .mean (ang_shift ):0.1f} ° ± { np .std (ang_shift ):0.1f} ° "
@@ -2899,8 +2947,13 @@ def find_bad_channels_maxwell(
28992947def _read_cross_talk (cross_talk , ch_names ):
29002948 sss_ctc = dict ()
29012949 ctc = None
2902- if cross_talk is not None :
2903- sss_ctc = _read_ctc (cross_talk )
2950+ if cross_talk :
2951+ if not isinstance (cross_talk , dict ):
2952+ sss_ctc = _read_ctc (cross_talk )
2953+ else :
2954+ sss_ctc = deepcopy (cross_talk )
2955+ # the way it is on disk
2956+ sss_ctc ["decoupler" ] = sss_ctc ["decoupler" ].T .tocsc ()
29042957 ctc_chs = sss_ctc ["ch_names" ]
29052958 # checking for extra space ambiguity in channel names
29062959 # between old and new fif files
0 commit comments