Repository: pyxem/kikuchipy (language: python)

Issue:
Overwrite Signal2D.deepcopy() for EBSD and EBSDMasterPattern to carry over custom properties



Discussion / hints:

Fix patch:
diff --git a/doc/changelog.rst b/doc/changelog.rst
index 2f6825fd..a8cb27a9 100644
--- a/doc/changelog.rst
+++ b/doc/changelog.rst
@@ -36,8 +36,11 @@ Added
 
 Fixed
 -----
+- Deep copying EBSD and EBSDMasterPattern signals carry over, respectively,
+  `xmap` and `detector`, and `phase`, `hemisphere` and `projection` properties
+  (`#356 <https://github.com/pyxem/kikuchipy/pull/356>`_).
 - Scaling of region of interest coordinates used in virtual backscatter electron
-  imaging to physical coordinates
+  imaging to physical coordinates.
   (`#349 <https://github.com/pyxem/kikuchipy/pull/349>`_)
 
 0.3.3 (2021-04-18)
@@ -50,7 +53,7 @@ Contributors
 
 Fixed
 -----
-- Reading of EBSD patterns from Bruker h5ebsd with a region of interest
+- Reading of EBSD patterns from Bruker h5ebsd with a region of interest.
   (`#339 <https://github.com/pyxem/kikuchipy/pull/339>`_)
 - Merging of (typically refined) crystal maps, where either a simulation indices
   array is not present or the array contains more indices per point than scores.
diff --git a/doc/reference.rst b/doc/reference.rst
index da3a45db..10ee3b58 100644
--- a/doc/reference.rst
+++ b/doc/reference.rst
@@ -84,6 +84,11 @@ EBSDDetector
 .. currentmodule:: kikuchipy.detectors.EBSDDetector
 
 .. autosummary::
+    deepcopy
+    pc_bruker
+    pc_emsoft
+    pc_oxford
+    pc_tsl
     plot
 
 .. autoclass:: kikuchipy.detectors.EBSDDetector
@@ -482,6 +487,9 @@ EBSD
 All methods listed here are also available to
 :class:`~kikuchipy.signals.LazyEBSD` objects.
 
+See :class:`hyperspy._signals.signal2d.Signal2D` for methods inherited
+from HyperSpy.
+
 .. currentmodule:: kikuchipy.signals.EBSD
 
 .. autosummary::
@@ -532,6 +540,9 @@ EBSDMasterPattern
 All methods listed here are also available to
 :class:`~kikuchipy.signals.LazyEBSDMasterPattern` objects.
 
+See :class:`hyperspy._signals.signal2d.Signal2D` for methods inherited
+from HyperSpy.
+
 .. currentmodule:: kikuchipy.signals.EBSDMasterPattern
 
 .. autosummary::
@@ -554,6 +565,9 @@ There are no methods exclusive to LazyEBSDMasterPattern objects.
 VirtualBSEImage
 ---------------
 
+See :class:`hyperspy._signals.signal2d.Signal2D` for methods inherited
+from HyperSpy.
+
 .. currentmodule:: kikuchipy.signals.VirtualBSEImage
 
 .. autosummary::
diff --git a/kikuchipy/signals/ebsd.py b/kikuchipy/signals/ebsd.py
index 026f4143..116e7dbb 100644
--- a/kikuchipy/signals/ebsd.py
+++ b/kikuchipy/signals/ebsd.py
@@ -66,6 +66,9 @@
     _rechunk_learning_results,
     _update_learning_results,
 )
+from kikuchipy.signals.util._crystal_map import (
+    crystal_map_is_compatible_with_signal,
+)
 from kikuchipy.signals.util._map_helper import (
     _get_neighbour_dot_product_matrices,
     _get_average_dot_product_map,
@@ -117,6 +120,8 @@ def __init__(self, *args, **kwargs):
         if not self.metadata.has_item("Sample.Phases"):
             self.set_phase_parameters()
 
+    # ---------------------- Custom properties ----------------------- #
+
     @property
     def detector(self) -> EBSDDetector:
         """An :class:`~kikuchipy.detectors.ebsd_detector.EBSDDetector`
@@ -133,6 +138,15 @@ def xmap(self) -> CrystalMap:
         """
         return self._xmap
 
+    @xmap.setter
+    def xmap(self, value: CrystalMap):
+        if crystal_map_is_compatible_with_signal(
+            value, self.axes_manager, raise_if_false=True,
+        ):
+            self._xmap = value
+
+    # ------------------------ Custom methods ------------------------ #
+
     def set_experimental_parameters(
         self,
         detector=None,
@@ -1524,6 +1538,20 @@ def get_virtual_bse_intensity(
         vbse_sum.set_signal_type("VirtualBSEImage")
         return vbse_sum
 
+    # ------ Methods overwritten from hyperspy.signals.Signal2D ------ #
+
+    def deepcopy(self):
+        new = super().deepcopy()
+        if self.xmap is not None:
+            new._xmap = self.xmap.deepcopy()
+        else:
+            new._xmap = copy.deepcopy(self.xmap)
+        if self.detector is not None:
+            new._detector = self.detector.deepcopy()
+        else:
+            new._detector = copy.deepcopy(self.detector)
+        return new
+
     def save(
         self,
         filename: Optional[str] = None,
@@ -1572,17 +1600,12 @@ def save(
         kikuchipy.io.plugins.nordif.file_writer
         """
         if filename is None:
-            if self.tmp_parameters.has_item(
-                "filename"
-            ) and self.tmp_parameters.has_item("folder"):
-                filename = os.path.join(
-                    self.tmp_parameters.folder, self.tmp_parameters.filename
-                )
-                extension = (
-                    self.tmp_parameters.extension
-                    if not extension
-                    else extension
-                )
+            tmp_params = self.tmp_parameters
+            if tmp_params.has_item("filename") and tmp_params.has_item(
+                "folder"
+            ):
+                filename = os.path.join(tmp_params.folder, tmp_params.filename)
+                extension = tmp_params.extension if not extension else extension
             elif self.metadata.has_item("General.original_filename"):
                 filename = self.metadata.General.original_filename
             else:
diff --git a/kikuchipy/signals/ebsd_master_pattern.py b/kikuchipy/signals/ebsd_master_pattern.py
index 3632047c..50269bb7 100644
--- a/kikuchipy/signals/ebsd_master_pattern.py
+++ b/kikuchipy/signals/ebsd_master_pattern.py
@@ -16,7 +16,7 @@
 # You should have received a copy of the GNU General Public License
 # along with kikuchipy. If not, see <http://www.gnu.org/licenses/>.
 
-
+import copy
 import sys
 from typing import Optional, Union
 
@@ -62,6 +62,8 @@ class EBSDMasterPattern(CommonImage, Signal2D):
     _alias_signal_types = ["ebsd_master_pattern", "master_pattern"]
     _lazy = False
 
+    # ---------------------- Custom properties ----------------------- #
+
     phase = Phase()
     projection = None
     hemisphere = None
@@ -258,6 +260,14 @@ def get_patterns(
 
         return out
 
+    # ------ Methods overwritten from hyperspy.signals.Signal2D ------ #
+    def deepcopy(self):
+        new = super().deepcopy()
+        new.phase = self.phase.deepcopy()
+        new.projection = copy.deepcopy(self.projection)
+        new.hemisphere = copy.deepcopy(self.hemisphere)
+        return new
+
 
 class LazyEBSDMasterPattern(EBSDMasterPattern, LazySignal2D):
     """Lazy implementation of the :class:`EBSDMasterPattern` class.
diff --git a/kikuchipy/signals/util/_crystal_map.py b/kikuchipy/signals/util/_crystal_map.py
new file mode 100644
index 00000000..6d58f057
--- /dev/null
+++ b/kikuchipy/signals/util/_crystal_map.py
@@ -0,0 +1,55 @@
+# -*- coding: utf-8 -*-
+# Copyright 2019-2021 The kikuchipy developers
+#
+# This file is part of kikuchipy.
+#
+# kikuchipy is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# kikuchipy is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with kikuchipy. If not, see <http://www.gnu.org/licenses/>.
+
+"""Utilities for combining a crystallographic map,
+:class:`orix.crystal_map.CrystalMap` with an EBSD signal,
+:class:`kikuchipy.signals.EBSD`.
+"""
+
+import warnings
+
+from hyperspy.axes import AxesManager
+from orix.crystal_map import CrystalMap
+
+
+def crystal_map_is_compatible_with_signal(
+    xmap: CrystalMap, axes_manager: AxesManager, raise_if_false: bool = False
+) -> bool:
+    nav_axes = axes_manager.navigation_axes[::-1]
+    nav_shape = tuple([a.size for a in nav_axes])
+    nav_scale = tuple([a.scale for a in nav_axes])
+    compatible = None
+    try:
+        xmap_scale = tuple([xmap._step_sizes[a.name] for a in nav_axes])
+    except KeyError:
+        warnings.warn(
+            "The signal navigation axes must be named 'x' and/or 'y' in order "
+            "to compare the signal navigation scale to the CrystalMap step "
+            "sizes 'dx' and 'dy' (see `EBSD.axes_manager`)"
+        )
+        xmap_scale = (None,) * len(nav_axes)
+        compatible = False
+    compatible = xmap.shape == nav_shape and xmap_scale == nav_scale
+    if not compatible and raise_if_false:
+        raise AttributeError(
+            f"The crystal map shape {xmap.shape} and step sizes {xmap_scale} "
+            f"aren't compatible with the signal navigation shape {nav_shape} "
+            f"and step sizes {nav_scale} (see `EBSD.axes_manager`)"
+        )
+    else:
+        return compatible


Test patch:
diff --git a/kikuchipy/conftest.py b/kikuchipy/conftest.py
index 8be32d83..8b5a59c4 100644
--- a/kikuchipy/conftest.py
+++ b/kikuchipy/conftest.py
@@ -370,8 +370,9 @@ def _get_single_phase_xmap(
         prop_names=["scores", "simulation_indices"],
         name="a",
         phase_id=0,
+        step_sizes=(1.5, 1),
     ):
-        d, map_size = _get_spatial_array_dicts(nav_shape)
+        d, map_size = _get_spatial_array_dicts(nav_shape, step_sizes)
         rot_idx = np.random.choice(
             np.arange(rotations.size), map_size * rotations_per_point
         )
diff --git a/kikuchipy/signals/tests/test_ebsd.py b/kikuchipy/signals/tests/test_ebsd.py
index 822f452e..3472a816 100644
--- a/kikuchipy/signals/tests/test_ebsd.py
+++ b/kikuchipy/signals/tests/test_ebsd.py
@@ -29,13 +29,8 @@
 from scipy.ndimage import correlate
 from skimage.exposure import rescale_intensity
 
-from kikuchipy import load
-from kikuchipy.data import nickel_ebsd_small, nickel_ebsd_large
+import kikuchipy as kp
 from kikuchipy.conftest import assert_dictionary
-from kikuchipy.filters.window import Window
-from kikuchipy.pattern._pattern import fft_spectrum
-from kikuchipy.signals.ebsd import EBSD, LazyEBSD
-from kikuchipy.signals.util._metadata import ebsd_metadata, metadata_nodes
 
 matplotlib.use("Agg")  # For plotting
 
@@ -49,21 +44,21 @@ def test_init(self):
 
         # Signal shape
         array0 = np.zeros(shape=(10, 10, 10, 10))
-        s0 = EBSD(array0)
+        s0 = kp.signals.EBSD(array0)
         assert array0.shape == s0.axes_manager.shape
 
         # Cannot initialise signal with one signal dimension
         with pytest.raises(ValueError):
-            _ = EBSD(np.zeros(10))
+            _ = kp.signals.EBSD(np.zeros(10))
 
         # Shape of one-image signal
         array1 = np.zeros(shape=(10, 10))
-        s1 = EBSD(array1)
+        s1 = kp.signals.EBSD(array1)
         assert array1.shape == s1.axes_manager.shape
 
         # SEM metadata
-        kp_md = ebsd_metadata()
-        sem_node = metadata_nodes("sem")
+        kp_md = kp.signals.util.ebsd_metadata()
+        sem_node = kp.signals.util.metadata_nodes("sem")
         assert_dictionary(
             kp_md.get_item(sem_node), s1.metadata.get_item(sem_node)
         )
@@ -77,7 +72,7 @@ def test_as_lazy(self, dummy_signal):
         # Assert that lazy attribute and class changed, while metadata was not
         # changed
         assert lazy_signal._lazy is True
-        assert lazy_signal.__class__ == LazyEBSD
+        assert lazy_signal.__class__ == kp.signals.LazyEBSD
         assert_dictionary(
             dummy_signal.metadata.as_dictionary(),
             lazy_signal.metadata.as_dictionary(),
@@ -108,7 +103,7 @@ def test_set_experimental_parameters(self, dummy_signal):
             "magnification": 500,
         }
         dummy_signal.set_experimental_parameters(**p)
-        ebsd_node = metadata_nodes("ebsd")
+        ebsd_node = kp.signals.util.metadata_nodes("ebsd")
         md_dict = dummy_signal.metadata.get_item(ebsd_node).as_dictionary()
         assert_dictionary(p, md_dict)
 
@@ -267,7 +262,7 @@ def test_incorrect_static_background_pattern(
         static background pattern to `remove_static_background().`
         """
 
-        ebsd_node = metadata_nodes("ebsd")
+        ebsd_node = kp.signals.util.metadata_nodes("ebsd")
         dummy_signal.metadata.set_item(
             ebsd_node + ".static_background", static_bg
         )
@@ -311,7 +306,7 @@ def test_remove_static_background_relative_and_scalebg_raises(
             )
 
     def test_non_square_patterns(self):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         s = s.isig[:, :-5]  # Remove bottom five rows
         static_bg = s.mean(axis=(0, 1))
         static_bg.change_dtype(np.uint8)
@@ -597,7 +592,7 @@ def test_adaptive_histogram_equalization(self):
         actual equalization are found elsewhere.
         """
 
-        s = load(KIKUCHIPY_FILE)
+        s = kp.load(KIKUCHIPY_FILE)
 
         # These window sizes should work without issue
         for kernel_size in [None, 10]:
@@ -610,7 +605,7 @@ def test_adaptive_histogram_equalization(self):
             s.adaptive_histogram_equalization(kernel_size=(10, 10, 10))
 
     def test_lazy_adaptive_histogram_equalization(self):
-        s = load(KIKUCHIPY_FILE, lazy=True)
+        s = kp.load(KIKUCHIPY_FILE, lazy=True)
         s.adaptive_histogram_equalization()
         assert isinstance(s.data, da.Array)
 
@@ -741,7 +736,7 @@ def test_average_neighbour_patterns_window_1d(self, dummy_signal):
         assert dummy_signal.data.dtype == answer.dtype
 
     def test_average_neighbour_patterns_pass_window(self, dummy_signal):
-        w = Window()
+        w = kp.filters.Window()
         dummy_signal.average_neighbour_patterns(w)
         # fmt: off
         answer = np.array(
@@ -767,14 +762,16 @@ def test_average_neighbour_patterns_lazy(self):
             (6,),
             (6,),
         )
-        s = LazyEBSD(da.zeros((55, 75, 6, 6), chunks=chunks, dtype=np.uint8))
+        s = kp.signals.LazyEBSD(
+            da.zeros((55, 75, 6, 6), chunks=chunks, dtype=np.uint8)
+        )
         s.average_neighbour_patterns()
         s.compute()
 
 
 class TestRebin:
     def test_rebin(self, dummy_signal):
-        ebsd_node = metadata_nodes("ebsd")
+        ebsd_node = kp.signals.util.metadata_nodes("ebsd")
 
         # Passing new_shape, only scaling in signal space
         new_shape = (3, 3, 2, 1)
@@ -800,7 +797,7 @@ def test_rebin(self, dummy_signal):
         ]
         s2 = dummy_signal.copy().as_lazy()
         s3 = dummy_signal.rebin(scale=scale, out=s2)
-        assert isinstance(s2, LazyEBSD)
+        assert isinstance(s2, kp.signals.LazyEBSD)
         assert s2.axes_manager.shape == tuple(expected_new_shape)
         assert s2.metadata.get_item(ebsd_node + ".binning") == float(scale[3])
         assert s3 is None
@@ -841,13 +838,13 @@ class TestDecomposition:
     def test_decomposition(self, dummy_signal):
         dummy_signal.change_dtype(np.float32)
         dummy_signal.decomposition()
-        assert isinstance(dummy_signal, EBSD)
+        assert isinstance(dummy_signal, kp.signals.EBSD)
 
     def test_lazy_decomposition(self, dummy_signal):
         lazy_signal = dummy_signal.as_lazy()
         lazy_signal.change_dtype(np.float32)
         lazy_signal.decomposition()
-        assert isinstance(lazy_signal, LazyEBSD)
+        assert isinstance(lazy_signal, kp.signals.LazyEBSD)
 
     @pytest.mark.parametrize(
         "components, dtype_out, mean_intensity",
@@ -874,7 +871,7 @@ def test_get_decomposition_model(
         # Check data shape, signal class and image intensities in model
         # signal
         assert model_signal.data.shape == dummy_signal.data.shape
-        assert isinstance(model_signal, EBSD)
+        assert isinstance(model_signal, kp.signals.EBSD)
         assert np.allclose(model_signal.data.mean(), mean_intensity, atol=1e-3)
 
     @pytest.mark.parametrize(
@@ -890,7 +887,7 @@ def test_get_decomposition_model_lazy(
         lazy_signal.decomposition(algorithm="PCA", output_dimension=9)
 
         # Signal type
-        assert isinstance(lazy_signal, LazyEBSD)
+        assert isinstance(lazy_signal, kp.signals.LazyEBSD)
 
         # Turn factors and loadings into dask arrays
         lazy_signal.learning_results.factors = da.from_array(
@@ -908,7 +905,7 @@ def test_get_decomposition_model_lazy(
         # Check data shape, signal class and image intensities in model
         # signal after rescaling to 8 bit unsigned integer
         assert model_signal.data.shape == lazy_signal.data.shape
-        assert isinstance(model_signal, LazyEBSD)
+        assert isinstance(model_signal, kp.signals.LazyEBSD)
         model_signal.rescale_intensity(relative=True, dtype_out=np.uint8)
         model_mean = model_signal.data.mean().compute()
         assert np.allclose(model_mean, mean_intensity, atol=0.1)
@@ -938,7 +935,7 @@ def test_get_decomposition_model_write(
         lazy_signal.get_decomposition_model_write(
             components=components, dir_out=tmp_path, fname_out=fname_out
         )
-        s_reload = load(os.path.join(tmp_path, fname_out))
+        s_reload = kp.load(os.path.join(tmp_path, fname_out))
 
         # ... data type, data shape and mean intensity
         assert s_reload.data.dtype == lazy_signal.data.dtype
@@ -951,15 +948,15 @@ def test_compute(self, dummy_signal):
         lazy_signal = dummy_signal.as_lazy()
 
         lazy_signal.compute()
-        assert isinstance(lazy_signal, EBSD)
+        assert isinstance(lazy_signal, kp.signals.EBSD)
         assert lazy_signal._lazy is False
 
     def test_change_dtype(self, dummy_signal):
         lazy_signal = dummy_signal.as_lazy()
 
-        assert isinstance(lazy_signal, LazyEBSD)
+        assert isinstance(lazy_signal, kp.signals.LazyEBSD)
         lazy_signal.change_dtype("uint16")
-        assert isinstance(lazy_signal, LazyEBSD)
+        assert isinstance(lazy_signal, kp.signals.LazyEBSD)
 
 
 class TestGetDynamicBackgroundEBSD:
@@ -970,7 +967,7 @@ def test_get_dynamic_background_spatial(self, dummy_signal):
         )
 
         assert bg.data.dtype == dtype_out
-        assert isinstance(bg, EBSD)
+        assert isinstance(bg, kp.signals.EBSD)
 
     def test_get_dynamic_background_frequency(self, dummy_signal):
         dtype_out = np.float32
@@ -979,7 +976,7 @@ def test_get_dynamic_background_frequency(self, dummy_signal):
         )
 
         assert bg.data.dtype == dtype_out
-        assert isinstance(bg, EBSD)
+        assert isinstance(bg, kp.signals.EBSD)
 
     def test_get_dynamic_background_raises(self, dummy_signal):
         filter_domain = "Vasselheim"
@@ -991,11 +988,11 @@ def test_get_dynamic_background_lazy(self, dummy_signal):
 
         bg = lazy_signal.get_dynamic_background()
 
-        assert isinstance(bg, LazyEBSD)
+        assert isinstance(bg, kp.signals.LazyEBSD)
 
         bg.compute()
 
-        assert isinstance(bg, EBSD)
+        assert isinstance(bg, kp.signals.EBSD)
 
 
 class TestGetImageQualityEBSD:
@@ -1076,16 +1073,16 @@ def test_fft_filter_frequency(
         dummy_signal.data = dummy_signal.data.astype(dtype_out)
 
         shape = dummy_signal.axes_manager.signal_shape
-        w = Window(transfer_function, shape=shape, **kwargs)
+        w = kp.filters.Window(transfer_function, shape=shape, **kwargs)
 
         dummy_signal.fft_filter(
             transfer_function=w, function_domain="frequency", shift=shift,
         )
 
-        assert isinstance(dummy_signal, EBSD)
+        assert isinstance(dummy_signal, kp.signals.EBSD)
         assert dummy_signal.data.dtype == dtype_out
         assert np.allclose(
-            np.sum(fft_spectrum(dummy_signal.inav[0, 0].data)),
+            np.sum(kp.pattern.fft_spectrum(dummy_signal.inav[0, 0].data)),
             expected_spectrum_sum,
             atol=1e-4,
         )
@@ -1129,7 +1126,7 @@ def test_fft_filter_lazy(self, dummy_signal):
             transfer_function=w, function_domain="frequency", shift=False
         )
 
-        assert isinstance(lazy_signal, LazyEBSD)
+        assert isinstance(lazy_signal, kp.signals.LazyEBSD)
         assert lazy_signal.data.dtype == dummy_signal.data.dtype
 
 
@@ -1194,7 +1191,7 @@ def test_normalize_intensity(
         else:
             assert np.allclose(np.mean(dummy_signal.data), 0, atol=1e-6)
 
-        assert isinstance(dummy_signal, EBSD)
+        assert isinstance(dummy_signal, kp.signals.EBSD)
         assert dummy_signal.data.dtype == dtype_out
         assert np.allclose(dummy_signal.inav[0, 0].data, answer, atol=1e-4)
 
@@ -1204,22 +1201,22 @@ def test_normalize_intensity_lazy(self, dummy_signal):
 
         lazy_signal.normalize_intensity()
 
-        assert isinstance(lazy_signal, LazyEBSD)
+        assert isinstance(lazy_signal, kp.signals.LazyEBSD)
         assert np.allclose(np.mean(lazy_signal.data.compute()), 0, atol=1e-6)
 
 
-class TestEBSDxmapProperty:
+class TestEBSDXmapProperty:
     def test_init_xmap(self, dummy_signal):
         """The attribute is set correctly."""
         assert dummy_signal.xmap is None
 
-        ssim = load(EMSOFT_FILE)
+        ssim = kp.load(EMSOFT_FILE)
         xmap = ssim.xmap
         assert isinstance(xmap, CrystalMap)
         assert xmap.phases[0].name == "ni"
 
-    def test_attribute_carry_over_from_lazy(self, dummy_signal):
-        ssim = load(EMSOFT_FILE, lazy=True)
+    def test_attribute_carry_over_from_lazy(self):
+        ssim = kp.load(EMSOFT_FILE, lazy=True)
         xmap_lazy = ssim.xmap.deepcopy()
         assert isinstance(xmap_lazy, CrystalMap)
         assert xmap_lazy.phases[0].name == "ni"
@@ -1230,13 +1227,63 @@ def test_attribute_carry_over_from_lazy(self, dummy_signal):
         assert xmap.phases[0].name == "ni"
         assert np.allclose(xmap.rotations.data, xmap_lazy.rotations.data)
 
+    def test_set_xmap(self, get_single_phase_xmap):
+        s = kp.data.nickel_ebsd_large(lazy=True)
+        nav_shape = s.axes_manager.navigation_shape[::-1]
+        step_sizes = (1.5, 1.5)
+
+        # Should succeed
+        xmap_good = get_single_phase_xmap(
+            nav_shape=nav_shape, step_sizes=step_sizes
+        )
+        s.xmap = xmap_good
+
+        # Should fail
+        xmap_bad = get_single_phase_xmap(
+            nav_shape=nav_shape[::-1], step_sizes=step_sizes,
+        )
+        with pytest.raises(AttributeError, match="The crystal map shape"):
+            s.xmap = xmap_bad
+        with pytest.raises(AttributeError, match="The crystal map shape"):
+            s.axes_manager["x"].scale = 2
+            s.xmap = xmap_good
+        with pytest.raises(AttributeError, match="The crystal map shape"):
+            s.axes_manager["x"].scale = 1
+            s.axes_manager["x"].name = "x2"
+            with pytest.warns(UserWarning, match="The signal navigation axes"):
+                s.xmap = xmap_good
+
+    def test_attribute_carry_over_from_deepcopy(self, get_single_phase_xmap):
+        s = kp.data.nickel_ebsd_small(lazy=True)
+        nav_axes = s.axes_manager.navigation_axes[::-1]
+        nav_shape = (a.size for a in nav_axes)
+        nav_scales = (a.scale for a in nav_axes)
+
+        xmap = get_single_phase_xmap(nav_shape=nav_shape, step_sizes=nav_scales)
+        s.xmap = xmap
+
+        s2 = s.deepcopy()
+        assert isinstance(s2.xmap, CrystalMap)
+        r1 = s.xmap.rotations.data
+        r2 = s2.xmap.rotations.data
+        assert not np.may_share_memory(r1, r2)
+        assert np.allclose(r1, r2)
+        assert not np.may_share_memory(s.detector.pc, s2.detector.pc)
+
+
+class TestEBSDDetectorProperty:
+    def test_attribute_carry_over_from_deepcopy(self):
+        s = kp.data.nickel_ebsd_small(lazy=True)
+        s._detector = None
+        s2 = s.deepcopy()
+
 
 class TestPatternMatching:
     def test_match_patterns(self, dummy_signal):
         """Scores are all 1.0 for a dictionary containing all patterns
         from dummy_signal().
         """
-        s_dict = EBSD(dummy_signal.data.reshape(-1, 3, 3))
+        s_dict = kp.signals.EBSD(dummy_signal.data.reshape(-1, 3, 3))
         s_dict._xmap = CrystalMap(Rotation(np.zeros((9, 4))))
         xmap = dummy_signal.match_patterns(s_dict)
 
@@ -1245,17 +1292,17 @@ def test_match_patterns(self, dummy_signal):
 
 class TestAverageNeighbourDotProductMap:
     def test_adp_0d(self):
-        s = nickel_ebsd_small().inav[0, 0]
+        s = kp.data.nickel_ebsd_small().inav[0, 0]
         with pytest.raises(ValueError, match="Signal must have at least one"):
             _ = s.get_average_neighbour_dot_product_map()
 
     def test_adp_1d(self):
-        s = nickel_ebsd_small().inav[0]
+        s = kp.data.nickel_ebsd_small().inav[0]
         adp = s.get_average_neighbour_dot_product_map()
         assert np.allclose(adp, [0.997470, 0.997457, 0.99744], atol=1e-5)
 
     def test_adp_2d(self):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         adp = s.get_average_neighbour_dot_product_map()
         assert np.allclose(
             adp,
@@ -1290,8 +1337,8 @@ def test_adp_2d(self):
         ],
     )
     def test_adp_window(self, window, desired_adp_map):
-        s = nickel_ebsd_small()
-        w = Window(window=window)
+        s = kp.data.nickel_ebsd_small()
+        w = kp.filters.Window(window=window)
         adp = s.get_average_neighbour_dot_product_map(window=w)
         assert np.allclose(adp, desired_adp_map, atol=1e-5)
 
@@ -1317,7 +1364,7 @@ def test_adp_window(self, window, desired_adp_map):
         ],
     )
     def test_adp_zero_mean(self, zero_mean, desired_adp_map):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         adp = s.get_average_neighbour_dot_product_map(zero_mean=zero_mean)
         assert np.allclose(adp, desired_adp_map, atol=1e-5)
 
@@ -1343,12 +1390,12 @@ def test_adp_zero_mean(self, zero_mean, desired_adp_map):
         ],
     )
     def test_adp_normalize(self, normalize, desired_adp_map):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         adp = s.get_average_neighbour_dot_product_map(normalize=normalize)
         assert np.allclose(adp, desired_adp_map, atol=1e-5)
 
     def test_adp_dtype_out(self):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         dtype1 = np.float32
         adp1 = s.get_average_neighbour_dot_product_map(normalize=False)
         assert adp1.dtype == dtype1
@@ -1359,7 +1406,7 @@ def test_adp_dtype_out(self):
         assert adp2.dtype == dtype2
 
     def test_adp_lazy(self):
-        s = nickel_ebsd_small(lazy=True)
+        s = kp.data.nickel_ebsd_small(lazy=True)
         adp = s.get_average_neighbour_dot_product_map()
 
         assert np.allclose(
@@ -1374,7 +1421,7 @@ def test_adp_lazy(self):
         assert adp.dtype == np.float32
 
     def test_adp_lazy2(self):
-        s = nickel_ebsd_large()
+        s = kp.data.nickel_ebsd_large()
         s_lazy = s.as_lazy()
         adp = s.get_average_neighbour_dot_product_map()
         adp_lazy = s_lazy.get_average_neighbour_dot_product_map()
@@ -1386,13 +1433,13 @@ def test_adp_lazy2(self):
     @pytest.mark.parametrize(
         "window",
         [
-            Window(window="circular", shape=(3, 3)),
-            Window(window="rectangular", shape=(3, 2)),
-            Window(window="rectangular", shape=(2, 3)),
+            kp.filters.Window(window="circular", shape=(3, 3)),
+            kp.filters.Window(window="rectangular", shape=(3, 2)),
+            kp.filters.Window(window="rectangular", shape=(2, 3)),
         ],
     )
     def test_adp_dp_matrices(self, window):
-        s = nickel_ebsd_large()
+        s = kp.data.nickel_ebsd_large()
         dp_matrices = s.get_neighbour_dot_product_matrices(window=window)
         adp1 = s.get_average_neighbour_dot_product_map(window=window)
         adp2 = s.get_average_neighbour_dot_product_map(dp_matrices=dp_matrices)
@@ -1401,7 +1448,7 @@ def test_adp_dp_matrices(self, window):
 
     @pytest.mark.parametrize("slices", [(0,), (slice(0, 1), slice(None))])
     def test_adp_dp_matrices_shapes(self, slices):
-        s = nickel_ebsd_small().inav[slices]
+        s = kp.data.nickel_ebsd_small().inav[slices]
         dp_matrices = s.get_neighbour_dot_product_matrices()
         adp1 = s.get_average_neighbour_dot_product_map()
         adp2 = s.get_average_neighbour_dot_product_map(dp_matrices=dp_matrices)
@@ -1411,12 +1458,12 @@ def test_adp_dp_matrices_shapes(self, slices):
 
 class TestNeighbourDotProductMatrices:
     def test_dp_matrices_0d(self):
-        s = nickel_ebsd_small().inav[0, 0]
+        s = kp.data.nickel_ebsd_small().inav[0, 0]
         with pytest.raises(ValueError, match="Signal must have at least one"):
             _ = s.get_neighbour_dot_product_matrices()
 
     def test_dp_matrices_1d(self):
-        s = nickel_ebsd_small().inav[0]
+        s = kp.data.nickel_ebsd_small().inav[0]
         dp_matrices = s.get_neighbour_dot_product_matrices()
 
         assert dp_matrices.shape == s.axes_manager.navigation_shape + (3,)
@@ -1433,7 +1480,7 @@ def test_dp_matrices_1d(self):
         )
 
     def test_dp_matrices_2d(self):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         dp_matrices = s.get_neighbour_dot_product_matrices()
 
         assert dp_matrices.shape == s.axes_manager.navigation_shape + (3, 3)
@@ -1450,7 +1497,7 @@ def test_dp_matrices_2d(self):
         )
 
     def test_dp_matrices_lazy(self):
-        s = nickel_ebsd_large()
+        s = kp.data.nickel_ebsd_large()
         s_lazy = s.as_lazy()
         dp_matrices = s.get_neighbour_dot_product_matrices()
         dp_matrices_lazy = s_lazy.get_neighbour_dot_product_matrices()
@@ -1463,7 +1510,7 @@ def test_dp_matrices_lazy(self):
         "window, desired_dp_matrices_11",
         [
             (
-                Window(window="circular", shape=(3, 3)),
+                kp.filters.Window(window="circular", shape=(3, 3)),
                 [
                     [np.nan, 0.997347, np.nan],
                     [0.994177, 1, 0.997358],
@@ -1471,7 +1518,7 @@ def test_dp_matrices_lazy(self):
                 ],
             ),
             (
-                Window(window="rectangular", shape=(3, 3)),
+                kp.filters.Window(window="rectangular", shape=(3, 3)),
                 [
                     [0.994048, 0.997347, 0.996990],
                     [0.994177, 1, 0.997358],
@@ -1479,17 +1526,17 @@ def test_dp_matrices_lazy(self):
                 ],
             ),
             (
-                Window(window="rectangular", shape=(3, 2)),
+                kp.filters.Window(window="rectangular", shape=(3, 2)),
                 [[0.994048, 0.997347], [0.994177, 1], [0.994017, 0.997360]],
             ),
             (
-                Window(window="rectangular", shape=(2, 3)),
+                kp.filters.Window(window="rectangular", shape=(2, 3)),
                 [[0.994048, 0.997347, 0.996990], [0.994177, 1, 0.997358]],
             ),
         ],
     )
     def test_dp_matrices_window(self, window, desired_dp_matrices_11):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         dp_matrices = s.get_neighbour_dot_product_matrices(window=window)
 
         assert np.allclose(
@@ -1501,7 +1548,7 @@ def test_dp_matrices_window(self, window, desired_dp_matrices_11):
 
     @pytest.mark.parametrize("dtype_out", [np.float16, np.float32, np.float64])
     def test_dp_matrices_dtype_out(self, dtype_out):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         dp_matrices = s.get_neighbour_dot_product_matrices(dtype_out=dtype_out)
 
         assert dp_matrices.dtype == dtype_out
@@ -1528,7 +1575,7 @@ def test_dp_matrices_dtype_out(self, dtype_out):
         ],
     )
     def test_dp_matrices_zero_mean(self, zero_mean, desired_dp_matrices11):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         dp_matrices = s.get_neighbour_dot_product_matrices(zero_mean=zero_mean)
 
         assert np.allclose(
@@ -1557,7 +1604,7 @@ def test_dp_matrices_zero_mean(self, zero_mean, desired_dp_matrices11):
         ],
     )
     def test_dp_matrices_normalize(self, normalize, desired_dp_matrices11):
-        s = nickel_ebsd_small()
+        s = kp.data.nickel_ebsd_small()
         dp_matrices = s.get_neighbour_dot_product_matrices(normalize=normalize)
 
         assert np.allclose(
@@ -1566,6 +1613,6 @@ def test_dp_matrices_normalize(self, normalize, desired_dp_matrices11):
 
     def test_dp_matrices_large(self):
         nav_shape = (250, 137)
-        s = LazyEBSD(da.ones(nav_shape + (96, 96), dtype=np.uint8))
+        s = kp.signals.LazyEBSD(da.ones(nav_shape + (96, 96), dtype=np.uint8))
         dp_matrices = s.get_neighbour_dot_product_matrices()
         assert dp_matrices.shape == nav_shape + (1, 1)
diff --git a/kikuchipy/signals/tests/test_ebsd_masterpattern.py b/kikuchipy/signals/tests/test_ebsd_masterpattern.py
index 58ce2b8a..11ea485c 100644
--- a/kikuchipy/signals/tests/test_ebsd_masterpattern.py
+++ b/kikuchipy/signals/tests/test_ebsd_masterpattern.py
@@ -27,23 +27,20 @@
 from orix.quaternion import Rotation
 import pytest
 
+import kikuchipy as kp
 from kikuchipy import load
 from kikuchipy.conftest import assert_dictionary
 from kikuchipy.data import nickel_ebsd_master_pattern_small
-from kikuchipy.detectors import EBSDDetector
 from kikuchipy.io.plugins.tests.test_emsoft_ebsd_masterpattern import (
     setup_axes_manager,
     METADATA,
 )
 from kikuchipy.signals.tests.test_ebsd import assert_dictionary
 from kikuchipy.signals.ebsd_master_pattern import (
-    EBSDMasterPattern,
-    LazyEBSDMasterPattern,
     _get_direction_cosines,
     _get_lambert_interpolation_parameters,
     _get_patterns_chunk,
 )
-from kikuchipy.signals.ebsd import LazyEBSD, EBSD
 from kikuchipy.indexing.similarity_metrics import ncc, ndp
 
 
@@ -55,7 +52,7 @@
 
 class TestEBSDMasterPatternInit:
     def test_init_no_metadata(self):
-        s = EBSDMasterPattern(
+        s = kp.signals.EBSDMasterPattern(
             np.zeros((2, 10, 11, 11)),
             projection="lambert",
             hemisphere="both",
@@ -68,15 +65,15 @@ def test_init_no_metadata(self):
         assert s.hemisphere == "both"
 
     def test_ebsd_masterpattern_lazy_data_init(self):
-        s = EBSDMasterPattern(da.zeros((2, 10, 11, 11)))
+        s = kp.signals.EBSDMasterPattern(da.zeros((2, 10, 11, 11)))
 
-        assert isinstance(s, EBSDMasterPattern)
+        assert isinstance(s, kp.signals.EBSDMasterPattern)
         assert isinstance(s.data, da.Array)
 
     def test_ebsd_masterpattern_lazy_init(self):
-        s = LazyEBSDMasterPattern(da.zeros((2, 10, 11, 11)))
+        s = kp.signals.LazyEBSDMasterPattern(da.zeros((2, 10, 11, 11)))
 
-        assert isinstance(s, LazyEBSDMasterPattern)
+        assert isinstance(s, kp.signals.LazyEBSDMasterPattern)
         assert isinstance(s.data, da.Array)
 
 
@@ -89,21 +86,21 @@ def test_save_load_hspy(self, save_path_hdf5):
 
         axes_manager = setup_axes_manager(["energy", "height", "width"])
 
-        assert isinstance(s, EBSDMasterPattern)
+        assert isinstance(s, kp.signals.EBSDMasterPattern)
         assert_dictionary(s.axes_manager.as_dictionary(), axes_manager)
         assert_dictionary(s.metadata.as_dictionary(), METADATA)
 
         s.save(save_path_hdf5)
 
         s2 = hs_load(save_path_hdf5, signal_type="EBSDMasterPattern")
-        assert isinstance(s2, EBSDMasterPattern)
+        assert isinstance(s2, kp.signals.EBSDMasterPattern)
         assert_dictionary(s2.axes_manager.as_dictionary(), axes_manager)
         assert_dictionary(s2.metadata.as_dictionary(), METADATA)
 
         s3 = hs_load(save_path_hdf5)
         assert isinstance(s3, Signal2D)
         s3.set_signal_type("EBSDMasterPattern")
-        assert isinstance(s3, EBSDMasterPattern)
+        assert isinstance(s3, kp.signals.EBSDMasterPattern)
         assert_dictionary(s3.axes_manager.as_dictionary(), axes_manager)
         assert_dictionary(s.metadata.as_dictionary(), METADATA)
 
@@ -128,7 +125,7 @@ def test_original_metadata_save_load_cycle(self, save_path_hdf5):
 
         s.save(save_path_hdf5)
         s2 = hs_load(save_path_hdf5, signal_type="EBSDMasterPattern")
-        assert isinstance(s2, EBSDMasterPattern)
+        assert isinstance(s2, kp.signals.EBSDMasterPattern)
 
         omd_dict_keys2 = s2.original_metadata.as_dictionary().keys()
         assert [k in omd_dict_keys2 for k in desired_keys]
@@ -141,16 +138,30 @@ class TestProperties:
     )
     def test_properties(self, projection, hemisphere):
         mp = nickel_ebsd_master_pattern_small(
-            projection=projection, hemisphere=hemisphere
+            projection=projection, hemisphere=hemisphere, lazy=True
         )
-
         assert mp.projection == projection
         assert mp.hemisphere == hemisphere
 
+        # Deepcopy
+        mp2 = mp.deepcopy()
+
+        assert mp2.projection == projection
+        mp2.projection = "gnomonic"
+        assert mp2.projection != projection
+
+        assert mp2.hemisphere == hemisphere
+        mp2.hemisphere = "west"
+        assert mp2.hemisphere != hemisphere
+
+        assert mp2.phase.point_group.name == mp.phase.point_group.name
+        mp2.phase.space_group = 220
+        assert mp2.phase.point_group.name != mp.phase.point_group.name
+
 
 class TestSimulatedPatternDictionary:
     # Create detector model
-    detector = EBSDDetector(
+    detector = kp.detectors.EBSDDetector(
         shape=(480, 640),
         px_size=50,
         pc=(20, 20, 15000),
@@ -168,18 +179,9 @@ def test_get_lambert_interpolation_parameters(self):
         npx = 1001
         npy = 1001
         scale = 500
-        (
-            nii,
-            nij,
-            niip,
-            nijp,
-            di,
-            dj,
-            dim,
-            djm,
-        ) = _get_lambert_interpolation_parameters(
+        nii, nij, niip, nijp = _get_lambert_interpolation_parameters(
             rotated_direction_cosines=dc, npx=npx, npy=npy, scale=scale,
-        )
+        )[:4]
 
         assert (nii <= niip).all()
         assert (nij <= nijp).all()
@@ -220,7 +222,7 @@ def test_get_patterns(self):
 
         detector_shape = self.detector.shape
         r2 = Rotation.from_euler(((0, 0, 0), (1, 1, 1), (2, 2, 2)))
-        mp_a = EBSDMasterPattern(np.zeros((2, 10, 11, 11)))
+        mp_a = kp.signals.EBSDMasterPattern(np.zeros((2, 10, 11, 11)))
         print(mp_a.axes_manager)
         mp_a.axes_manager[0].name = "hemisphere"
         mp_a.axes_manager[1].name = "energy"
@@ -228,40 +230,40 @@ def test_get_patterns(self):
         mp_a.phase = Phase("Ni", 225)
         out_a = mp_a.get_patterns(r2, self.detector, 5)
 
-        assert isinstance(out_a, LazyEBSD)
+        assert isinstance(out_a, kp.signals.LazyEBSD)
         desired_data_shape = (3,) + detector_shape[::-1]
         assert out_a.axes_manager.shape == desired_data_shape
 
-        mp_b = EBSDMasterPattern(np.zeros((10, 11, 11)))
+        mp_b = kp.signals.EBSDMasterPattern(np.zeros((10, 11, 11)))
         mp_b.axes_manager[0].name = "energy"
         mp_b.projection = "lambert"
         mp_b.phase = Phase("Ni", 225)
         out_b = mp_b.get_patterns(r2, self.detector, 5)
 
-        assert isinstance(out_b, LazyEBSD)
+        assert isinstance(out_b, kp.signals.LazyEBSD)
         assert out_b.axes_manager.shape == desired_data_shape
 
-        mp_c = EBSDMasterPattern(np.zeros((11, 11)))
+        mp_c = kp.signals.EBSDMasterPattern(np.zeros((11, 11)))
         mp_c.projection = "lambert"
         mp_c.phase = Phase("Ni", 225)
         out_c = mp_c.get_patterns(r2, self.detector, 5)
         out_c_2 = mp_c.get_patterns(r2, self.detector, 5, compute=True)
 
-        assert isinstance(out_c, LazyEBSD)
-        assert isinstance(out_c_2, EBSD)
+        assert isinstance(out_c, kp.signals.LazyEBSD)
+        assert isinstance(out_c_2, kp.signals.EBSD)
         assert out_c.axes_manager.shape == desired_data_shape
 
-        mp_c2 = EBSDMasterPattern(np.zeros((11, 11)))
+        mp_c2 = kp.signals.EBSDMasterPattern(np.zeros((11, 11)))
         mp_c2.projection = "lambert"
         mp_c2.phase = Phase("!Ni", 220)
         with pytest.raises(AttributeError):
             mp_c2.get_patterns(r2, self.detector, 5)
 
-        mp_d = EBSDMasterPattern(np.zeros((2, 11, 11)))
+        mp_d = kp.signals.EBSDMasterPattern(np.zeros((2, 11, 11)))
         with pytest.raises(NotImplementedError):
             mp_d.get_patterns(r2, self.detector, 5)
 
-        mp_e = EBSDMasterPattern(np.zeros((10, 11, 11)))
+        mp_e = kp.signals.EBSDMasterPattern(np.zeros((10, 11, 11)))
         mp_e.axes_manager[0].name = "energy"
         mp_e.projection = "lambert"
         mp_e.phase = Phase("!Ni", 220)
@@ -270,7 +272,7 @@ def test_get_patterns(self):
 
         # More than one Projection center is currently not supported so
         # it should fail
-        d2 = EBSDDetector(
+        d2 = kp.detectors.EBSDDetector(
             shape=(10, 10),
             px_size=50,
             pc=((0, 0, 15000), (0, 0, 15000)),
@@ -307,7 +309,7 @@ def test_get_patterns_chunk(self):
     def test_simulated_patterns_xmap_detector(self):
         mp = nickel_ebsd_master_pattern_small(projection="lambert")
         r = Rotation.from_euler([[0, 0, 0], [0, np.pi / 2, 0]])
-        detector = EBSDDetector(
+        detector = kp.detectors.EBSDDetector(
             shape=(60, 60),
             pc=[0.5, 0.5, 0.5],
             sample_tilt=70,
@@ -327,13 +329,13 @@ def test_simulated_patterns_xmap_detector(self):
     def test_get_patterns_navigation_shape(self, nav_shape):
         mp = nickel_ebsd_master_pattern_small(projection="lambert")
         r = Rotation(np.random.uniform(low=0, high=1, size=nav_shape + (4,)))
-        detector = EBSDDetector(shape=(60, 60))
+        detector = kp.detectors.EBSDDetector(shape=(60, 60))
         sim = mp.get_patterns(rotations=r, detector=detector, energy=20)
         assert sim.axes_manager.navigation_shape[::-1] == nav_shape
 
     def test_get_patterns_navigation_shape_raises(self):
         mp = nickel_ebsd_master_pattern_small(projection="lambert")
         r = Rotation(np.random.uniform(low=0, high=1, size=(1, 2, 3, 4)))
-        detector = EBSDDetector(shape=(60, 60))
+        detector = kp.detectors.EBSDDetector(shape=(60, 60))
         with pytest.raises(ValueError, match="The rotations object can only"):
             _ = mp.get_patterns(rotations=r, detector=detector, energy=20)