1717
1818from __future__ import annotations
1919from copy import deepcopy
20+ from datetime import datetime
2021import logging
22+ from pathlib import Path
23+ import re
2124from typing import List , Optional , Tuple , Union
2225
2326from matplotlib .figure import Figure
3336from skimage .transform import ProjectiveTransform
3437from sklearn .linear_model import LinearRegression , RANSACRegressor
3538
39+ from kikuchipy import __version__
3640from kikuchipy .indexing ._hough_indexing import _get_indexer_from_detector
3741
3842
@@ -63,35 +67,33 @@ class EBSDDetector:
6367 ----------
6468 shape
6569 Number of detector rows and columns in pixels. Default is
66- `` (1, 1)`` .
70+ (1, 1).
6771 px_size
6872 Size of unbinned detector pixel in um, assuming a square
69- pixel shape. Default is ``1`` .
73+ pixel shape. Default is 1 .
7074 binning
7175 Detector binning, i.e. how many pixels are binned into one.
72- Default is ``1`` , i.e. no binning.
76+ Default is 1 , i.e. no binning.
7377 tilt
74- Detector tilt from horizontal in degrees. Default is ``0`` .
78+ Detector tilt from horizontal in degrees. Default is 0 .
7579 azimuthal
7680 Sample tilt about the sample RD (downwards) axis. A positive
7781 angle means the sample normal moves towards the right
78- looking from the sample to the detector. Default is ``0`` .
82+ looking from the sample to the detector. Default is 0 .
7983 sample_tilt
80- Sample tilt from horizontal in degrees. Default is ``70`` .
84+ Sample tilt from horizontal in degrees. Default is 70 .
8185 pc
8286 X, Y and Z coordinates of the projection/pattern centers
8387 (PCs), describing the location of the beam on the sample
8488 measured relative to the detection screen. See *Notes* for
8589 the definition and conversions between conventions. If
8690 multiple PCs are passed, they are assumed to be on the form
87- ``[[x0, y0, z0], [x1, y1, z1], ...]``. Default is
88- ``[0.5, 0.5, 0.5]``.
91+ [[x0, y0, z0], [x1, y1, z1], ...]. Default is [0.5, 0.5, 0.5].
8992 convention
9093 PC convention. If not given, Bruker's convention is assumed.
91- Options are ``"tsl"``/``"edax"``/``"amatek"``,
92- ``"oxford"``/``"aztec"``, ``"bruker"``, ``"emsoft"``,
93- ``"emsoft4"``, and ``"emsoft5"``. ``"emsoft"`` and ``"emsoft5"``
94- is the same convention. See *Notes* for conversions between
94+ Options are "tsl"/"edax"/"amatek", "oxford"/"aztec", "bruker",
95+ "emsoft", "emsoft4", and "emsoft5". "emsoft" and "emsoft5" is
96+ the same convention. See *Notes* for conversions between
9597 conventions.
9698
9799 Notes
@@ -194,7 +196,9 @@ def __init__(
194196 self .azimuthal = azimuthal
195197 self .sample_tilt = sample_tilt
196198 self .pc = pc
197- self ._set_pc_from_convention (convention )
199+ if convention is None :
200+ convention = "bruker"
201+ self ._set_pc_in_bruker_convention (convention )
198202
199203 def __repr__ (self ) -> str :
200204 pc_average = tuple (self .pc_average .round (3 ))
@@ -454,6 +458,72 @@ def r_max(self) -> np.ndarray:
454458 corners [..., 3 ] = self .x_min ** 2 + self .y_min ** 2 # Lo. left
455459 return np .atleast_2d (np .sqrt (np .max (corners , axis = - 1 )))
456460
461+ @classmethod
462+ def load (cls , fname : Union [Path , str ]) -> EBSDDetector :
463+ """Return an EBSD detector loaded from a text file saved with
464+ :meth:`save`.
465+
466+ Parameters
467+ ----------
468+ fname
469+ Full path to file.
470+
471+ Returns
472+ -------
473+ detector
474+ Loaded EBSD detector.
475+ """
476+ pc = np .loadtxt (fname )
477+
478+ keys = [
479+ "shape" ,
480+ "px_size" ,
481+ "binning" ,
482+ "tilt" ,
483+ "azimuthal" ,
484+ "sample_tilt" ,
485+ "convention" ,
486+ "navigation_shape" ,
487+ ]
488+
489+ detector_kw = dict (zip (keys , [None ] * len (keys )))
490+ with open (fname , mode = "r" ) as f :
491+ header = []
492+ for line in f .readlines ():
493+ if line [0 ] == "#" :
494+ line = line [2 :- 1 ].lstrip (" " )
495+ if len (line ) > 0 :
496+ header .append (line )
497+ match = re .match (r"^(\w+|\w+\s\w+): (.*)" , line )
498+ if match :
499+ groups = match .groups ()
500+ if groups [0 ] in detector_kw and len (groups ) > 1 :
501+ detector_kw [groups [0 ]] = groups [1 ]
502+ else :
503+ break
504+
505+ for k in ["shape" , "navigation_shape" ]:
506+ shape = detector_kw [k ]
507+ try :
508+ detector_kw [k ] = tuple (int (i ) for i in shape [1 :- 1 ].split ("," ))
509+ except ValueError : # pragma: no cover
510+ detector_kw [k ] = None
511+ for k , dtype in zip (
512+ ["px_size" , "binning" , "tilt" , "azimuthal" , "sample_tilt" ],
513+ [float , int , float , float , float ],
514+ ):
515+ value = detector_kw [k ].rstrip (" deg" )
516+ try :
517+ detector_kw [k ] = dtype (value )
518+ except (): # pragma: no cover
519+ detector_kw [k ] = None
520+
521+ nav_shape = detector_kw .pop ("navigation_shape" )
522+ if isinstance (nav_shape , tuple ):
523+ pc = pc .reshape (nav_shape + (3 ,))
524+
525+ return cls (pc = pc , ** detector_kw )
526+
457527 def crop (self , extent : Union [Tuple [int , int , int , int ], List [int ]]) -> EBSDDetector :
458528 """Return a new detector with its :attr:`shape` cropped and
459529 :attr:`pc` values updated accordingly.
@@ -750,8 +820,8 @@ def extrapolate_pc(
750820 binning : int = None ,
751821 is_outlier : Optional [Union [tuple , list , np .ndarray ]] = None ,
752822 ):
753- r"""Return a new detector with projection centers (PCs)
754- extrapolated from an average PC.
823+ r"""Return a new detector with projection centers (PCs) in a 2D
824+ map extrapolated from an average PC.
755825
756826 The average PC :math:`\bar{PC}` is calculated from :attr:`pc`,
757827 possibly excluding some PCs based on the ``is_outlier`` mask.
@@ -1505,14 +1575,71 @@ def plot_pc(
15051575 if return_figure :
15061576 return fig
15071577
1578+ def save (self , filename : str , convention : str = "Bruker" , ** kwargs ) -> None :
1579+ """Save detector in a text file with projection centers (PCs) in
1580+ the given convention.
1581+
1582+ Parameters
1583+ ----------
1584+ filename
1585+ Name of text file to write to. See :func:`~numpy.savetxt`
1586+ for supported file formats.
1587+ convention
1588+ PC convention. Default is Bruker's convention. Options are
1589+ "tsl"/"edax", "oxford", "bruker", "emsoft", "emsoft4", and
1590+ "emsoft5". "emsoft" and "emsoft5" is the same convention.
1591+ See *Notes* in :class:`EBSDDetector` for conversions between
1592+ conventions.
1593+ **kwargs
1594+ Keyword arguments passed to :func:`~numpy.savetxt`, e.g.
1595+ ``fmt="%.4f"`` to reduce the number of PC decimals from the
1596+ default 7 to 4.
1597+ """
1598+ pc = self ._get_pc_in_convention (convention )
1599+ pc = pc .reshape (- 1 , 3 )
1600+
1601+ time_now = datetime .now ().strftime ("%Y-%m-%d %H:%M:%S" )
1602+
1603+ kwargs .setdefault (
1604+ "header" ,
1605+ (
1606+ f"EBSDDetector\n "
1607+ f" shape: { self .shape } \n "
1608+ f" px_size: { self .px_size } \n "
1609+ f" binning: { self .binning } \n "
1610+ f" tilt: { self .tilt } deg\n "
1611+ f" azimuthal: { self .azimuthal } deg\n "
1612+ f" sample_tilt: { self .sample_tilt } deg\n "
1613+ f" convention: { convention } \n "
1614+ f" navigation_shape: { self .navigation_shape } \n \n "
1615+ f"kikuchipy version: { __version__ } \n "
1616+ f"Time: { time_now } \n \n "
1617+ "Column names: PCx, PCy, PCz"
1618+ ),
1619+ )
1620+ kwargs .setdefault ("fmt" , "%.7f" )
1621+ np .savetxt (fname = filename , X = pc , ** kwargs )
1622+
15081623 # ------------------------ Private methods ----------------------- #
15091624
1510- def _get_pc_from_convention (self , convention : Optional [str ] = None ) -> np .ndarray :
1511- if convention is None or convention .lower () in CONVENTION_ALIAS ["bruker" ]:
1512- return self .pc
1625+ def _get_pc_in_bruker_convention (self , convention : str = "bruker" ) -> np .ndarray :
1626+ """Convert current :attr:`pc` to Bruker's convention from
1627+ another convention.
1628+
1629+ Parameters
1630+ ----------
1631+ convention
1632+ Convention of the current PCs. Default is "bruker".
15131633
1634+ Returns
1635+ -------
1636+ pc
1637+ PC array in Bruker's convention.
1638+ """
15141639 conv = convention .lower ()
1515- if conv in CONVENTION_ALIAS ["tsl" ] + CONVENTION_ALIAS ["oxford" ]:
1640+ if conv in CONVENTION_ALIAS ["bruker" ]:
1641+ return self .pc
1642+ elif conv in CONVENTION_ALIAS ["tsl" ] + CONVENTION_ALIAS ["oxford" ]:
15161643 return self ._pc_tsl2bruker ()
15171644 elif conv in CONVENTION_ALIAS ["emsoft" ]:
15181645 try :
@@ -1526,8 +1653,40 @@ def _get_pc_from_convention(self, convention: Optional[str] = None) -> np.ndarra
15261653 f"recognised conventions { CONVENTION_ALIAS_ALL } "
15271654 )
15281655
1529- def _set_pc_from_convention (self , convention : Optional [str ] = None ):
1530- self .pc = self ._get_pc_from_convention (convention )
1656+ def _set_pc_in_bruker_convention (self , convention : str = "bruker" ):
1657+ self .pc = self ._get_pc_in_bruker_convention (convention )
1658+
1659+ def _get_pc_in_convention (self , convention : str = "bruker" ) -> np .ndarray :
1660+ """Convert current :attr:`pc` from Bruker's convention to
1661+ another convention.
1662+
1663+ Parameters
1664+ ----------
1665+ convention
1666+ Convention of the output PCs. Default is "bruker", which
1667+ means the PCs are returned without conversion.
1668+
1669+ Returns
1670+ -------
1671+ pc
1672+ PC array in specified convention.
1673+ """
1674+ conv = convention .lower ()
1675+ if conv in CONVENTION_ALIAS ["bruker" ]:
1676+ return self .pc
1677+ elif conv in CONVENTION_ALIAS ["tsl" ] + CONVENTION_ALIAS ["oxford" ]:
1678+ return self ._pc_bruker2tsl ()
1679+ elif conv in CONVENTION_ALIAS ["emsoft" ]:
1680+ try :
1681+ version = int (convention [- 1 ])
1682+ except ValueError :
1683+ version = 5
1684+ return self ._pc_bruker2emsoft (version )
1685+ else :
1686+ raise ValueError (
1687+ f"Projection center convention '{ convention } ' not among the "
1688+ f"recognised conventions { CONVENTION_ALIAS_ALL } "
1689+ )
15311690
15321691 def _pc_emsoft2bruker (self , version : int = 5 ) -> np .ndarray :
15331692 new_pc = np .zeros_like (self .pc , dtype = float )
0 commit comments