Repository: mne-tools/mne-python

Issue:
BUG: possible bug wrt BallTree xhs update
#### Describe the bug
The [xhs](https://github.com/mne-tools/mne-python/blob/master/mne/surface.py#L498) didn't get updated after first initialized as [[[0. 0. 0.]]](https://github.com/mne-tools/mne-python/blob/master/mne/gui/_coreg_gui.py#L487). Therefore, the [nearest_transformed_high_res_mri_idx_hsp](https://github.com/mne-tools/mne-python/blob/master/mne/gui/_coreg_gui.py#L458) is always 0.
![Screen Shot 2020-01-26 at 00 14 35](https://user-images.githubusercontent.com/3050531/73130186-85760900-402e-11ea-8684-cb7ed661887f.png)

After the [snippet](https://github.com/zuxfoucault/mne-python/blob/fix_balltree_xhs_update/mne/gui/_coreg_gui.py#L495) added, the `xhs` is updated with `transformed_high_res_mri_points`, and the `icp_fid_match = nearest`  performed better than `matched` (median distance: 1.280 mm vs 1.642 mm).
![Screen Shot 2020-01-26 at 00 32 50](https://user-images.githubusercontent.com/3050531/73130290-a8a1b800-4030-11ea-8b8c-03fd2968093a.png)

#### Steps to reproduce
https://14813-1301584-gh.circle-artifacts.com/0/dev/auto_examples/forward/plot_auto_alignment.html
Note: the example isn't really ready for public consumption as far as I know



Discussion:
@zuxfoucault did you need to change code in [_coreg_gui.py](https://github.com/zuxfoucault/mne-python/blob/fix_balltree_xhs_update/mne/gui/_coreg_gui.py#L495) to fix things, or just add some lines to the example? If you just needed to change the example code, then I think we can close since that example was never merged / never fully tested / is not yet ready for public consumption.
@larsoner Just add the notification code in [_coreg_gui.py](https://github.com/zuxfoucault/mne-python/blob/fix_balltree_xhs_update/mne/gui/_coreg_gui.py#L495-L498)

Should I do a PR? Not sure if this fix would affect other parts of the package though
Something is weird about https://github.com/zuxfoucault/mne-python/commit/698d3240553e9cad580d9014e60440d57d46a833 because the [existing _update_nearest_calc](https://github.com/mne-tools/mne-python/blob/master/mne/gui/_coreg_gui.py#L490-L493) should already take care of it:
```
    @on_trait_change('processed_high_res_mri_points')
    def _update_nearest_calc(self):
        self.nearest_calc = _DistanceQuery(
            self.processed_high_res_mri_points * self.parameters[6:9])
```
Also, we (should) always do the [nearest calculation in MRI space](https://github.com/mne-tools/mne-python/pull/6732), so your change will break things if display is in head space because it's using the transformed points
> Something is weird about [zuxfoucault@698d324](https://github.com/zuxfoucault/mne-python/commit/698d3240553e9cad580d9014e60440d57d46a833) because the [existing _update_nearest_calc](https://github.com/mne-tools/mne-python/blob/master/mne/gui/_coreg_gui.py#L490-L493) should already take care of it:
> 
> ```
>     @on_trait_change('processed_high_res_mri_points')
>     def _update_nearest_calc(self):
>         self.nearest_calc = _DistanceQuery(
>             self.processed_high_res_mri_points * self.parameters[6:9])
> ```

My rationale is that [self.processed_high_res_mri_points](https://github.com/zuxfoucault/mne-python/blob/fix_balltree_xhs_update/mne/gui/_coreg_gui.py#L682) is generated after
[self.nearest_transformed_high_res_mri_idx_hsp](https://github.com/zuxfoucault/mne-python/blob/fix_balltree_xhs_update/mne/gui/_coreg_gui.py#L683), where xhp is still initialized as [[[0. 0. 0.]]](https://github.com/mne-tools/mne-python/blob/master/mne/gui/_coreg_gui.py#L487) when querying the nearest points.
> My rationale is that self.processed_high_res_mri_points is generated after self.nearest_transformed_high_res_mri_idx_hsp

Where does this happen? I see the def as:
```
    processed_high_res_mri_points = Property(
        depends_on=['mri:bem_high_res:surf', 'grow_hair'])
```
So it does not depend on `nearest_transformed_high_res_mri_idx_hsp`
> > My rationale is that self.processed_high_res_mri_points is generated after self.nearest_transformed_high_res_mri_idx_hsp
> 
> Where does this happen? I see the def as:
> 
> ```
>     processed_high_res_mri_points = Property(
>         depends_on=['mri:bem_high_res:surf', 'grow_hair'])
> ```
> 
> So it does not depend on `nearest_transformed_high_res_mri_idx_hsp`

I think it's [here](https://github.com/zuxfoucault/mne-python/blob/fix_balltree_xhs_update/mne/gui/_coreg_gui.py#L682-L683) 
`nearest_transformed_high_res_mri_idx_hsp` is index for `processed_high_res_mri_points`?
It's used as an index but I don't see why that should affect `nearest_calc`. The code you linked to is inside `_setup_icp`, which should only be called once `processed_high_res_mri_points` is already populated properly, `nearest_calc` is set up properly (automatically), and you've asked to do ICP.

I think the way to solve this is to figure out exactly how/when these things are being triggered, i.e. the flow of updates/changes. It's possible that we are missing a trigger somewhere, but the fix in this PR does not seem right, even if it happens to make things work.
History of some function call printed:
![Screen Shot 2020-01-28 at 09 56 21](https://user-images.githubusercontent.com/3050531/73230057-ea5c6b00-41b6-11ea-8944-89e2bf4075e0.png)

It seems when `_get_processed_high_res_mri_points` is first initialized, handler `_update_nearest_calc: processed_high_res_mri_points` wouldn't be called. The same applies to `_get_transformed_high_res_mri_points`; handler `_update_nearest_calc: transformed_high_res_mri_points` is called on the second `_get_transformed_high_res_mri_points` call. Howeve, I still haven't figured out why `print('_get_processed_high_res_mri_points')` inside `def _get_processed_high_res_mri_points` dosen't get called after first initilization.

But it looks the initialization of `processed_high_res_mri_points` doesn't get `nearest_calc` set up with `_DistanceQuery(self.processed_high_res_mri_points * self.parameters[6:9])` So `xhs` remains ` [[0. 0. 0.]]` while querying. 
> Something is weird about [zuxfoucault@698d324](https://github.com/zuxfoucault/mne-python/commit/698d3240553e9cad580d9014e60440d57d46a833) because the [existing _update_nearest_calc](https://github.com/mne-tools/mne-python/blob/master/mne/gui/_coreg_gui.py#L490-L493) should already take care of it:
> 
> ```
>     @on_trait_change('processed_high_res_mri_points')
>     def _update_nearest_calc(self):
>         self.nearest_calc = _DistanceQuery(
>             self.processed_high_res_mri_points * self.parameters[6:9])
> ```

Out of curiosity, as a workaround, would this look better? `self.nearest_calc = _DistanceQuery(
            self.transformed_high_res_mri_points)` in the original proposal is replaced with `self.nearest_calc = _DistanceQuery( self.processed_high_res_mri_points * self.parameters[6:9])`, hence
```
    @on_trait_change('transformed_high_res_mri_points')
    def _update_nearest_calc(self):
        self.nearest_calc = _DistanceQuery(
             self.processed_high_res_mri_points * self.parameters[6:9])
```

Just adding a [dependence](https://github.com/zuxfoucault/mne-python/blob/fix_balltree_xhs_update/mne/gui/_coreg_gui.py#L243-L244) on `mri_trans` for `processed_high_res_mri_points` would evoke the update, although it's surplus after the 2nd call on `_get_processed_high_res_mri_points`
![Screen Shot 2020-01-28 at 14 52 38](https://user-images.githubusercontent.com/3050531/73242527-32dc4e80-41e0-11ea-9a3a-55598713035d.png)

Alternatively, we can add a  trait attribute, say `xhs_update = Int()`, and make `processed_high_res_mri_points` depend on it:
```
processed_high_res_mri_points = Property(
    depends_on=['mri:bem_high_res:surf', 'grow_hair', 'xhs_update'])
```
So we could just trigger the update once by assign/change `self.xhs_update`'s value each time we need

Resolving patch:
diff --git a/mne/gui/_coreg_gui.py b/mne/gui/_coreg_gui.py
--- a/mne/gui/_coreg_gui.py
+++ b/mne/gui/_coreg_gui.py
@@ -485,12 +485,12 @@ def _get_transformed_low_res_mri_points(self):
         return points
 
     def _nearest_calc_default(self):
-        return _DistanceQuery(np.zeros((1, 3)))
+        return _DistanceQuery(
+            self.processed_high_res_mri_points * self.parameters[6:9])
 
     @on_trait_change('processed_high_res_mri_points')
     def _update_nearest_calc(self):
-        self.nearest_calc = _DistanceQuery(
-            self.processed_high_res_mri_points * self.parameters[6:9])
+        self.nearest_calc = self._nearest_calc_default()
 
     @cached_property
     def _get_transformed_high_res_mri_points(self):
diff --git a/mne/gui/_marker_gui.py b/mne/gui/_marker_gui.py
--- a/mne/gui/_marker_gui.py
+++ b/mne/gui/_marker_gui.py
@@ -13,7 +13,7 @@
 from pyface.api import confirm, error, FileDialog, OK, YES
 from traits.api import (HasTraits, HasPrivateTraits, on_trait_change,
                         cached_property, Instance, Property, Array, Bool,
-                        Button, Enum, File, Float, List, Str)
+                        Button, Enum, File, Float, List, Str, ArrayOrNone)
 from traitsui.api import View, Item, HGroup, VGroup, CheckListEditor
 from traitsui.menu import Action, CancelButton
 
@@ -217,7 +217,7 @@ class MarkerPointDest(MarkerPoints):  # noqa: D401
     name = Property(Str, depends_on='src1.name,src2.name')
     dir = Property(Str, depends_on='src1.dir,src2.dir')
 
-    points = Property(Array(float, (5, 3)),
+    points = Property(ArrayOrNone(float, (5, 3)),
                       depends_on=['method', 'src1.points', 'src1.use',
                                   'src2.points', 'src2.use'])
     enabled = Property(Bool, depends_on=['points'])
diff --git a/mne/gui/_viewer.py b/mne/gui/_viewer.py
--- a/mne/gui/_viewer.py
+++ b/mne/gui/_viewer.py
@@ -14,7 +14,8 @@
 from mayavi.tools.mlab_scene_model import MlabSceneModel
 from traits.api import (HasTraits, HasPrivateTraits, on_trait_change,
                         Instance, Array, Bool, Button, Enum, Float, Int, List,
-                        Range, Str, RGBColor, Property, cached_property)
+                        Range, Str, RGBColor, Property, cached_property,
+                        ArrayOrNone)
 from traitsui.api import (View, Item, HGroup, VGrid, VGroup, Spring,
                           TextEditor)
 from tvtk.api import tvtk
@@ -208,7 +209,7 @@ class PointObject(Object):
     # projection onto a surface
     nearest = Instance(_DistanceQuery)
     check_inside = Instance(_CheckInside)
-    project_to_trans = Array(float, shape=(4, 4))
+    project_to_trans = ArrayOrNone(float, shape=(4, 4))
     project_to_surface = Bool(False, label='Project', desc='project points '
                               'onto the surface')
     orient_to_surface = Bool(False, label='Orient', desc='orient points '


Test patch:
diff --git a/mne/gui/tests/test_coreg_gui.py b/mne/gui/tests/test_coreg_gui.py
--- a/mne/gui/tests/test_coreg_gui.py
+++ b/mne/gui/tests/test_coreg_gui.py
@@ -16,6 +16,7 @@
 import mne
 from mne.datasets import testing
 from mne.io.kit.tests import data_dir as kit_data_dir
+from mne.surface import dig_mri_distances
 from mne.transforms import invert_transform
 from mne.utils import (run_tests_if_main, requires_mayavi, traits_test,
                        modified_env)
@@ -316,4 +317,35 @@ def test_coreg_model_with_fsaverage(tmpdir):
     assert model.hsp.n_omitted == 0
 
 
+@testing.requires_testing_data
+@requires_mayavi
+@traits_test
+def test_coreg_gui_automation():
+    """Test that properties get properly updated."""
+    from mne.gui._file_traits import DigSource
+    from mne.gui._fiducials_gui import MRIHeadWithFiducialsModel
+    from mne.gui._coreg_gui import CoregModel
+    subject = 'sample'
+    hsp = DigSource()
+    hsp.file = raw_path
+    mri = MRIHeadWithFiducialsModel(subjects_dir=subjects_dir, subject=subject)
+    model = CoregModel(mri=mri, hsp=hsp)
+    # gh-7254
+    assert not (model.nearest_transformed_high_res_mri_idx_hsp == 0).all()
+    model.fit_fiducials()
+    model.icp_iterations = 2
+    model.nasion_weight = 2.
+    model.fit_icp()
+    model.omit_hsp_points(distance=5e-3)
+    model.icp_iterations = 2
+    model.fit_icp()
+    errs_icp = np.median(
+        model._get_point_distance())
+    assert 2e-3 < errs_icp < 3e-3
+    info = mne.io.read_info(raw_path)
+    errs_nearest = np.median(
+        dig_mri_distances(info, fname_trans, subject, subjects_dir))
+    assert 1e-3 < errs_nearest < 2e-3
+
+
 run_tests_if_main()