#!/usr/bin/env python
# coding: utf-8
# This notebook is part of the `kikuchipy` documentation https://kikuchipy.org.
# Links to the documentation won't work from the notebook.
# # Pattern matching
#
# Crystal orientations can be determined from experimental EBSD patterns by
# matching them to a dictionary of simulated patterns of known orientations, see
# e.g. Chen et al. (2015),
# Nolze et al. (2016),
# Foden et al. (2019).
#
# Here, we will demonstrate *dictionary indexing* (DI) using a Ni data set of 4125
# EBSD patterns and a dynamically simulated master pattern from EMsoft, both of
# low resolution and found in the [kikuchipy.data](../reference.rst#data) module.
# The pattern dictionary is generated from a discrete grid of orientations with a
# fixed set of projection center (PC) parameters. The true orientation is likely
# to fall in between multiple grid points, which means there is always a lower
# angular accuracy associated with each DI run. We can improve upon each
# orientation by letting it deviate from the discrete grid points. We do this by
# maximizing the similarity between experimental and simulated patterns using
# numerical optimization algorithms from the
# [SciPy library](https://docs.scipy.org/doc/scipy/reference/optimize.html). This
# is here called *orientation refinement*. We could instead keep the orientations
# fixed and let the PC parameters deviate from their fixed value used in the
# dictionary, here called *projection center refinement*. Finally, we can also
# refine both at the same time, here called *orientation and projection center
# refinement*. The need for orientation or orientation and PC refinement is
# discussed in e.g.
# Singh et al. (2017),
# Winkelmann et al. (2020), and
# Pang et al. (2020).
#
# The term *pattern matching* is here used for the combined approach of DI
# followed by refinement.
#
# Before we can generate a dictionary of
# simulated patterns, we need a master pattern containing all possible scattering
# vectors for a candidate phase. This can be simulated using EMsoft
# Callahan and De Graef (2013)
# Jackson et al. (2014), and then read
# into kikuchipy.
# First, we import libraries and load the small experimental Nickel test data
# In[1]:
# Exchange inline for notebook or qt5 (from pyqt) for interactive plotting
get_ipython().run_line_magic('matplotlib', 'inline')
import tempfile
import matplotlib.pyplot as plt
import hyperspy.api as hs
import numpy as np
from orix import sampling, plot, io
import kikuchipy as kp
plt.rcParams.update({"figure.facecolor": "w", "font.size": 15})
# Use kp.load("data.h5") to load your own data
s = kp.data.nickel_ebsd_large(allow_download=True) # External download
s
# To obtain a good match, we must increase the signal-to-noise ratio. In this
# pattern matching analysis, the Kikuchi bands are considered the signal, and the
# angle-dependent backscatter intensity, along with unwanted detector effects,
# are considered to be noise. See the
# [pattern processing guide](pattern_processing.rst) for further details.
# In[2]:
s.remove_static_background()
s.remove_dynamic_background()
#
#
# Note
#
# DI is computationally intensive and takes in general a long time due to all the
# comparisons being done. To maintain an acceptable memory usage and be done
# within reasonable time, it is recommended to write processed patterns to an HDF5
# file for quick reading during DI.
#
#
# In[3]:
#s.save("pattern_static_dynamic.h5")
# ## Dictionary indexing
#
# ### Load a master pattern
#
# Next, we load a dynamically simulated Nickel master pattern generated with
# EMsoft, in the northern hemisphere projection of the square Lambert projection
# for an accelerating voltage of 20 keV
# In[4]:
energy = 20
mp = kp.data.nickel_ebsd_master_pattern_small(projection="lambert", energy=energy)
mp
# In[5]:
mp.plot()
# The Nickel phase information, specifically the crystal symmetry, asymmetric atom
# positions, and crystal lattice, is conveniently stored in an
# [orix.crystal_map.Phase](https://orix.readthedocs.io/en/stable/reference.html#orix.crystal_map.phase_list.Phase)
# In[6]:
ni = mp.phase
ni
# In[7]:
ni.structure # Element, x, y, z, site occupation
# In[8]:
ni.structure.lattice # nm and degrees
# ### Sample orientation space
#
# If we don't know anything about the possible crystal (unit cell) orientations in
# our sample, the safest thing to do is to generate a dictionary of orientations
# uniformly distributed in a candidate phase's orientation space. To achieve this,
# we sample the Rodrigues Fundamental Zone of the proper point group *432* with a
# 4$^{\circ}$ characteristic distance between orientations (we can either pass
# in the proper point group, or the space group, which is a subgroup of the proper
# point group) using
# [orix.sampling.get_sample_fundamental()](https://orix.readthedocs.io/en/stable/reference.html#orix.sampling.sample_generators.get_sample_fundamental)
# In[9]:
rotations = sampling.get_sample_fundamental(
resolution=4, space_group=ni.space_group.number
)
rotations
# This sampling resulted in 14423 crystal orientations.
#
#
#
# Note
#
# A characteristic distance of 4$^{\circ}$ results in a course sampling of
# orientation space; a shorter distance should be used for experimental work.
#
#
# ### Define the detector-sample-geometry
# Now that we have our master pattern and crystal orientations, we need to
# describe the EBSD detector's position with respect to the sample (interaction
# volume). This ensures that projecting parts of the master pattern onto our
# detector yields dynamically simulated patterns resembling our experimental ones.
# See the [reference frames](reference_frames.rst) user guide and the
# [EBSDDetector](../reference.rst#kikuchipy.detectors.EBSDDetector)
# class for further details.
# In[10]:
signal_shape = s.axes_manager.signal_shape[::-1]
detector = kp.detectors.EBSDDetector(
shape=signal_shape,
pc=[0.421, 0.7794, 0.5049],
sample_tilt=70,
convention="tsl",
)
detector
# Let's double check the projection/pattern center (PC) position on the detector
# using
# [plot()](../reference.rst#kikuchipy.detectors.EBSDDetector.plot)
# In[11]:
detector.plot(coordinates="gnomonic", pattern=s.inav[0, 0].data)
# ### Generate dictionary
#
# Now we're ready to generate our dictionary of simulated patterns by projecting
# parts of the master pattern onto our detector for all sampled orientations,
# using the
# [get_patterns()](../reference.rst#kikuchipy.signals.ebsdmasterpattern.get_patterns)
# method. The method assumes the crystal orientations are represented with respect
# to the EDAX TSL sample reference frame RD-TD-ND.
#
#
#
# Note
#
# It is in general adviced to not compute the dictionary immediately, but let the
# dictionary indexing method handle this, by passing `compute=False`. This will
# return a `LazyEBSD` signal, with the dictionary patterns as a
# [dask array](https://docs.dask.org/en/latest/array.html).
#
#
# In[12]:
sim = mp.get_patterns(
rotations=rotations,
detector=detector,
energy=energy,
dtype_out=np.float32,
compute=True
)
sim
# Let's inspect the three first of the 14423 simulated patterns
# In[13]:
#sim.plot() # Plot the patterns with a navigator for easy inspection
fig, ax = plt.subplots(ncols=3, figsize=(18, 6))
for i in range(3):
ax[i].imshow(sim.inav[i].data, cmap="gray")
euler = np.rad2deg(sim.xmap[i].rotations.to_euler())[0]
ax[i].set_title(
f"($\phi_1, \Phi, \phi_2)$ = {np.array_str(euler, precision=1)}"
)
ax[i].axis("off")
fig.tight_layout()
# ### Perform indexing
#
# *Added in version: 0.5.*
#
# The Kikuchi pattern signal is usually weak towards the corners of the detector,
# so we can pass a signal mask to only match the pixels where the mask values are
# ``False``, i.e. mask out the values that are ``True``. This convention is in
# line with how NumPy, Dask, scikit-image etc. defines a mask. We can pass
# whatever mask we want, as long as it is a boolean array of the detector shape.
# In[14]:
signal_mask = ~kp.filters.Window("circular", signal_shape).astype(bool)
p = s.inav[0, 0].data
fig, ax = plt.subplots(ncols=2, figsize=(10, 5))
ax[0].imshow(p * signal_mask, cmap="gray")
ax[0].set_title("Not used in matching")
ax[1].imshow(p * ~signal_mask, cmap="gray")
ax[1].set_title("Used in matching");
# Finally, let's use the
# [dictionary_indexing()](../reference.rst#kikuchipy.signals.EBSD.dictionary_indexing)
# method to match the simulated patterns to our experimental patterns, using
# the [zero-mean normalized cross correlation (NCC)](../reference.rst#kikuchipy.indexing.similarity_metrics.NormalizedCrossCorrelationMetric)
# coefficient $r$
# Gonzalez & Woods (2017), which is
# the default similarity metric. Let's keep the 10 best matching orientations. A
# number of 4125 * 14423 comparisons is quite small, which we can do in memory all
# at once. However, in cases where the number of comparisons are too big for our
# memory to handle, we should iterate over the dictionary of simulated patterns
# by passing the number of patterns per iteration. To demonstrate this, we do at
# least 10 iterations here. The results are returned as a
# [orix.crystal_map.CrystalMap](https://orix.readthedocs.io/en/latest/reference.html#crystalmap).
# In[15]:
xmap = s.dictionary_indexing(
sim,
metric="ncc",
keep_n=10,
n_per_iteration=sim.axes_manager.navigation_size // 10,
signal_mask=signal_mask
)
xmap
#
#
# Note
#
# *Added in version: 0.5.*
#
# Dictionary indexing of real world data sets takes a long time because the
# matching is computationally intensive. The
# [dictionary_indexing()](../reference.rst#kikuchipy.signals.EBSD.dictionary_indexing)
# method accepts parameters *n_per_iteration*, *rechunk* and *dtype* that lets us
# control this behaviour to a certain extent, so be sure to take a look at the
# method's docstring.
#
#
# The [normalized dot product](../reference.rst#kikuchipy.indexing.similarity_metrics.NormalizedDotProductMetric)
# can be used instead of the NCC by passing `metric="ndp"`. A custom metric can
# be used instead, by creating a class which inherits from the abstract class
# [SimilarityMetric](../reference.rst#kikuchipy.indexing.similarity_metrics.SimilarityMetric).
# The results can be exported to an HDF5 file re-readable by orix
# In[16]:
temp_dir = tempfile.mkdtemp() + "/"
xmap_file = temp_dir + "ni.h5"
io.save(xmap_file, xmap)
# ### Inspect indexing results
#
# So far, the [orix library](https://orix.readthedocs.io) cannot produce any
# orientation color maps, pole figures etc. This has to be done in other software,
# like MTEX.
#
# Here, we'll inspect the map of best matching scores and a so-called orientation
# similarity (OS) map, which compares the best matching orientations for each
# pattern to it’s nearest neighbours.
#
# Let's get the NCC map in the correct shape from the CrystalMap’s scores property
# and the OS map with
# [orientation_similarity_map()](../reference.rst#kikuchipy.indexing.orientation_similarity_map)
# using the full list of 10 best matches per point
# In[17]:
ncc_map = xmap.scores[:, 0].reshape(*xmap.shape)
os_map = kp.indexing.orientation_similarity_map(xmap)
# And plot the maps
# In[18]:
fig, ax = plt.subplots(ncols=2, figsize=(9, 3))
im0 = ax[0].imshow(ncc_map)
ax[0].axis("off")
fig.colorbar(im0, ax=ax[0], label="NCC")
im1 = ax[1].imshow(os_map)
ax[1].axis("off")
fig.colorbar(im1, ax=ax[1], label="Orientation similarity")
fig.tight_layout();
# We can see that some of the grains have orientations which doesn't match that
# well to any orientation (pattern) in the dictionary. Note that refinment will
# not be able to improve much upon those orientations that didn't get a good match
# from DI. The simplest way to improve the results for the darker grains is to
# reduce the orientation sampling characteristic distance. This will increase our
# dictionary size and thus our indexing time, but will lead to better results.
#
# We can use the crystal map property `simulation_indices` to get the best
# matching simulated patterns from the dictionary of simulated patterns
# In[19]:
best_patterns = sim.data[xmap.simulation_indices[:, 0]].reshape(s.data.shape)
s_best = kp.signals.EBSD(best_patterns)
s_best
# The simplest way to visually compare the experimental and best matching
# simulated patterns are to
# [plot them in the same navigator](visualizing_patterns.ipynb#plot-multiple-signals).
# Here, we use the highest $r$ as a navigator. When using an interactive backend
# like `Qt5Agg`, we can then move the red square around to look at the patterns in
# each point.
# In[20]:
ncc_navigator = hs.signals.Signal2D(xmap.get_map_data(xmap.scores[:, 0]))
hs.plot.plot_signals([s, s_best], navigator=ncc_navigator)
# Let's also plot the best matches for patterns from two grains
# In[22]:
grain1 = (0, 0)
grain2 = (30, 10)
fig, ax = plt.subplots(ncols=2, nrows=2, figsize=(10, 10))
ax[0, 0].imshow(s.inav[grain1].data, cmap="gray")
ax[0, 0].axis("off")
ax[0, 1].imshow(s_best.inav[grain1].data, cmap="gray")
ax[0, 1].axis("off")
ax[1, 0].imshow(s.inav[grain2].data, cmap="gray")
ax[1, 0].axis("off")
ax[1, 1].imshow(s_best.inav[grain2].data, cmap="gray")
ax[1, 1].axis("off")
fig.tight_layout(h_pad=0.5, w_pad=1)
# ## Refinement
#
# Let's look at the effect of three refinement routes, all implemented as `EBSD`
# methods:
#
# 1. Refine orientations while keeping the PCs fixed: [refine_orientation()](../reference.rst#kikuchipy.signals.EBSD.refine_orientation)
# 2. Refine PCs while keeping the orientations fixed: [refine_projection_center()](../reference.rst#kikuchipy.signals.EBSD.refine_projection_center)
# 3. Refine orientations and PCs at the same time: [refine_orientation_projection_center()](../reference.rst#kikuchipy.signals.EBSD.refine_orientation_projection_center)
#
# For each run we will compare the maps and histograms of NCC scores before and
# after refinement, and also the PC parameters if appropriate.
#
# As stated at the top, we use the numerical optimization routines from the
# [SciPy library](https://docs.scipy.org/doc/scipy/reference/optimize.html). For
# every orientation and/or PC, we want to iteratively increase the similarity (NCC
# score) between our experimental pattern and a new simulated pattern projected
# onto our EBSD detector for every iteration until the score increase from one
# iteration to the next is below a certain threshold, by default 0.0001 in most
# cases. The orientation and/or PC is updated slightly for every iteration. We
# have access to both local and global optimization algorithms. Consult the
# kikuchipy docstring methods linked below and the SciPy documentation for all
# available options.
#
# Note that while we here refine orientations obtained from DI, any orientation
# results, e.g. from Hough indexing, can be refined with this approach, as long as
# an appropriate master pattern and
# [EBSDDetector](../reference.rst#kikuchipy.detectors.EBSDDetector) is provided,
# and the orientation results are passed as a
# [CrystalMap](https://orix.readthedocs.io/en/stable/reference.html#orix.crystal_map.CrystalMap).
#
# ### Refine orientations
#
# We use
# [refine_orientation()](../reference.rst#kikuchipy.signals.EBSD.refine_orientation)
# to refine orientations while keeping the PCs fixed, using the default local
# Nelder-Mead simplex method
# In[23]:
xmap_refined = s.refine_orientation(
xmap=xmap,
detector=detector,
master_pattern=mp,
energy=energy,
method="minimize", # Default
method_kwargs=dict(method="Nelder-Mead", options=dict(fatol=1e-4)), # Default
compute=True,
)
# Compare the NCC score maps. We use the same colorbar bounds for both maps
# In[24]:
ncc_after_orientation_refinement = xmap_refined.get_map_data("scores")
ncc_di_min = np.min(ncc_map)
ncc_di_max = np.max(ncc_map)
ncc_ori_ref_min = np.min(ncc_after_orientation_refinement)
ncc_ori_ref_max = np.max(ncc_after_orientation_refinement)
vmin = min([ncc_di_min, ncc_ori_ref_min])
vmax = max([ncc_di_max, ncc_ori_ref_max])
# In[25]:
fig, ax = plt.subplots(ncols=2, figsize=(9, 3))
im0 = ax[0].imshow(ncc_map, vmin=vmin, vmax=vmax)
ax[0].axis("off")
fig.colorbar(im0, ax=ax[0], label="NCC from DI")
im1 = ax[1].imshow(ncc_after_orientation_refinement, vmin=vmin, vmax=vmax)
ax[1].axis("off")
ncc_after_label = "NCC after ori. ref."
fig.colorbar(im1, ax=ax[1], label=ncc_after_label)
fig.tight_layout();
# Compare the histograms
# In[26]:
bins = np.linspace(vmin, vmax, 100)
fig, ax = plt.subplots(figsize=(9, 5))
_ = ax.hist(ncc_map.ravel(), bins, alpha=0.5, label="NCC from DI")
_ = ax.hist(
ncc_after_orientation_refinement.ravel(),
bins,
alpha=0.5,
label=ncc_after_label,
)
ax.set_xlabel("Normalized cross correlation (NCC) scores")
ax.set_ylabel("Frequency")
ax.legend()
fig.tight_layout();
# We can see that orientation refinement found the best orientation
# (with a fixed PC) for most of the grains, but not for the ones which didn't find
# the correct best match in the dictionary. Clearly, the dictionary is too sparse.
# ### Refine projection centers
#
# We use
# [refine_projection_center()](../reference.rst#kikuchipy.signals.EBSD.refine_projection_center)
# to refine PCs while keeping the orientations fixed, using the local modified
# Powell method. This method is also known as Bound Optimization BY Quadratic
# Approximation (BOBYQA), and is used in EMsoft and discussed by
# Singh et al. (2017). We will pass
# a `trust_region` of +/- 5% for the PC parameters to use as upper and lower
# bounds during refinement.
#
# We can also pass `compute=False`, to do the
# refinement later. The results will then be a list of
# [dask.delayed.Delayed](https://docs.dask.org/en/latest/delayed-api.html#dask.delayed.Delayed)
# instances. We can pass this list to
# [kikuchipy.indexing.compute_refine_projection_center_results()](../reference.rst#kikuchipy.indexing.compute_refine_projection_center_results)
# and perform the refinement to retrieve the results
# In[27]:
delayed_results = s.refine_projection_center(
xmap=xmap,
detector=detector,
master_pattern=mp,
energy=energy,
method="minimize",
method_kwargs=dict(method="Powell", options=dict(ftol=1e-3)),
trust_region=[0.05, 0.05, 0.05],
compute=False,
)
# In[28]:
ncc_after_pc_refinement, detector_refined = kp.indexing.compute_refine_projection_center_results(
results=delayed_results, detector=detector, xmap=xmap
)
# Note that `refine_orientation()` and `refine_orientation_projection_center()`
# also takes the `compute` parameter, and there similar functions to compute the
# refinement results:
# [kikuchipy.indexing.compute_refine_orientation_results()](../reference.rst#kikuchipy.indexing.compute_refine_orientation_results)
# and
# [kikuchipy.indexing.compute_refine_orientation_projection_center_results()](../reference.rst#kikuchipy.indexing.compute_refine_orientation_projection_center_results).
#
# Let's plot the refined scores and PCs
# In[29]:
ncc_pc_ref_min = np.min(ncc_after_pc_refinement)
ncc_pc_ref_max = np.max(ncc_after_pc_refinement)
vmin2 = min([ncc_di_min, ncc_pc_ref_min])
vmax2 = max([ncc_di_max, ncc_pc_ref_max])
# In[30]:
fig, ax = plt.subplots(ncols=2, figsize=(9, 3))
im0 = ax[0].imshow(ncc_map, vmin=vmin2, vmax=vmax2)
ax[0].axis("off")
fig.colorbar(im0, ax=ax[0], label="NCC from DI")
im1 = ax[1].imshow(ncc_after_pc_refinement, vmin=vmin2, vmax=vmax2)
ax[1].axis("off")
ncc_after_pc_label = "NCC after PC refinement"
fig.colorbar(im1, ax=ax[1], label=ncc_after_pc_label)
fig.tight_layout();
# Compare the histograms
# In[31]:
bins = np.linspace(vmin2, vmax2, 100)
fig, ax = plt.subplots(figsize=(9, 5))
_ = ax.hist(ncc_map.ravel(), bins, alpha=0.5, label="NCC from DI")
_ = ax.hist(
ncc_after_pc_refinement.ravel(),
bins,
alpha=0.5,
label=ncc_after_pc_label,
)
ax.set_xlabel("Normalized cross correlation (NCC) scores")
ax.set_ylabel("Frequency")
ax.legend()
fig.tight_layout();
# In[32]:
print(
f"PC used in DI:\t\t{detector.pc_average}\n"
f"PC after PC refinement:\t{detector_refined.pc_average}"
)
fig, ax = plt.subplots(ncols=3, figsize=(15, 3))
im0 = ax[0].imshow(detector_refined.pcx)
fig.colorbar(im0, ax=ax[0], label="Projection center x")
ax[0].axis("off")
im1 = ax[1].imshow(detector_refined.pcy)
fig.colorbar(im1, ax=ax[1], label="Projection center y")
ax[1].axis("off")
im2 = ax[2].imshow(detector_refined.pcz)
fig.colorbar(im2, ax=ax[2], label="Projection center z")
ax[2].axis("off")
fig.tight_layout();
# ### Refine orientations and projection centers
#
# We use
# [refine_orientation_projection_center()](../reference.rst#kikuchipy.signals.EBSD.refine_orientation_projection_center)
# to refine orientations and PCs at the same time. For the purpose of saving time
# and computation resources here, we'll just do this for the upper left quarter of
# the data set
# In[33]:
nav_shape = s.axes_manager.navigation_shape[::-1]
y1, x1 = np.array(nav_shape) // 2
slices2d = (slice(0, y1), slice(0, x1))
# In[34]:
s2 = s.inav[slices2d[::-1]] # HyperSpy flips the usual NumPy (row, column) to (column, row)
s2
# In[35]:
xmap2 = xmap[slices2d]
xmap2
# In[36]:
xmap_refined2, detector_refined2 = s2.refine_orientation_projection_center(
xmap=xmap2,
detector=detector,
master_pattern=mp,
energy=energy,
method="minimize",
method_kwargs=dict(options=dict(fatol=1e-3)),
compute=True,
)
# In[37]:
xmap_refined2
# Compare the NCC score maps. We use the same colorbar bounds for both maps
# In[38]:
ncc_after_orientation_pc_refinement = xmap_refined2.get_map_data("scores")
ncc_map2 = ncc_map[slices2d]
ncc2_di_min = np.min(ncc_map2)
ncc2_di_max = np.max(ncc_map2)
ncc_ori_pc_ref_min = np.min(ncc_after_orientation_pc_refinement)
ncc_ori_pc_ref_max = np.max(ncc_after_orientation_pc_refinement)
vmin3 = min([ncc2_di_min, ncc_ori_pc_ref_min])
vmax3 = max([ncc2_di_max, ncc_ori_pc_ref_max])
# In[39]:
fig, ax = plt.subplots(ncols=2, figsize=(9, 3))
im0 = ax[0].imshow(ncc_map2, vmin=vmin, vmax=vmax)
ax[0].axis("off")
fig.colorbar(im0, ax=ax[0], label="NCC from DI")
im1 = ax[1].imshow(ncc_after_orientation_pc_refinement, vmin=vmin3, vmax=vmax3)
ax[1].axis("off")
ncc_after_ori_pc_label = "NCC after ori./PC ref."
fig.colorbar(im1, ax=ax[1], label=ncc_after_ori_pc_label)
fig.tight_layout();
# Compare the histograms
# In[40]:
bins = np.linspace(vmin3, vmax3, 100)
fig, ax = plt.subplots(figsize=(9, 5))
_ = ax.hist(ncc_map2.ravel(), bins, alpha=0.5, label="NCC from DI")
_ = ax.hist(
ncc_after_orientation_pc_refinement.ravel(),
bins,
alpha=0.5,
label=ncc_after_ori_pc_label,
)
ax.set_xlabel("Normalized cross correlation (NCC) scores")
ax.set_ylabel("Frequency")
ax.legend()
fig.tight_layout();
# Let's also inspect the refined PC parameters
# In[41]:
print(
f"PC used in DI:\t\t{detector.pc_average}\n"
f"PC after PC refinement:\t{detector_refined2.pc_average}"
)
fig, ax = plt.subplots(ncols=3, figsize=(15, 3))
im0 = ax[0].imshow(detector_refined2.pcx)
fig.colorbar(im0, ax=ax[0], label="Projection center x")
ax[0].axis("off")
im1 = ax[1].imshow(detector_refined2.pcy)
fig.colorbar(im1, ax=ax[1], label="Projection center y")
ax[1].axis("off")
im2 = ax[2].imshow(detector_refined2.pcz)
fig.colorbar(im2, ax=ax[2], label="Projection center z")
ax[2].axis("off")
fig.tight_layout();
# In[42]:
# Remove files written to disk in this user guide
import os
os.remove(xmap_file)
os.rmdir(temp_dir)