diff --git a/.buildinfo b/.buildinfo new file mode 100644 index 00000000..9f6ae4c6 --- /dev/null +++ b/.buildinfo @@ -0,0 +1,4 @@ +# Sphinx build info version 1 +# This file hashes the configuration used when building these files. When it is not found, a full rebuild will be done. +config: 868fb72c95ded13c8d8a087307de155c +tags: 645f666f9bcd5a90fca523b33c5a78b7 diff --git a/.doctrees/_autosummary/pyTEMlib.animation.InteractiveAberration.doctree b/.doctrees/_autosummary/pyTEMlib.animation.InteractiveAberration.doctree new file mode 100644 index 00000000..644ff57d Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.animation.InteractiveAberration.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.animation.InteractiveRonchigramMagnification.doctree b/.doctrees/_autosummary/pyTEMlib.animation.InteractiveRonchigramMagnification.doctree new file mode 100644 index 00000000..02d064c0 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.animation.InteractiveRonchigramMagnification.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.animation.add_aperture.doctree b/.doctrees/_autosummary/pyTEMlib.animation.add_aperture.doctree new file mode 100644 index 00000000..cb949d3e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.animation.add_aperture.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.animation.add_lens.doctree b/.doctrees/_autosummary/pyTEMlib.animation.add_lens.doctree new file mode 100644 index 00000000..92ab6650 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.animation.add_lens.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.animation.deficient_holz_line.doctree b/.doctrees/_autosummary/pyTEMlib.animation.deficient_holz_line.doctree new file mode 100644 index 00000000..9d6ec786 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.animation.deficient_holz_line.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.animation.deficient_kikuchi_line.doctree b/.doctrees/_autosummary/pyTEMlib.animation.deficient_kikuchi_line.doctree new file mode 100644 index 00000000..227fdc71 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.animation.deficient_kikuchi_line.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.animation.doctree b/.doctrees/_autosummary/pyTEMlib.animation.doctree new file mode 100644 index 00000000..6856f32c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.animation.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.animation.geometric_ray_diagram.doctree b/.doctrees/_autosummary/pyTEMlib.animation.geometric_ray_diagram.doctree new file mode 100644 index 00000000..6041342a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.animation.geometric_ray_diagram.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.animation.propagate_beam.doctree b/.doctrees/_autosummary/pyTEMlib.animation.propagate_beam.doctree new file mode 100644 index 00000000..a62698ac Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.animation.propagate_beam.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.atom_tools.atom_refine.doctree b/.doctrees/_autosummary/pyTEMlib.atom_tools.atom_refine.doctree new file mode 100644 index 00000000..934366b2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.atom_tools.atom_refine.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.atom_tools.atoms_clustering.doctree b/.doctrees/_autosummary/pyTEMlib.atom_tools.atoms_clustering.doctree new file mode 100644 index 00000000..54fcb0f7 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.atom_tools.atoms_clustering.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.atom_tools.doctree b/.doctrees/_autosummary/pyTEMlib.atom_tools.doctree new file mode 100644 index 00000000..3d259f0f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.atom_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.atom_tools.find_atoms.doctree b/.doctrees/_autosummary/pyTEMlib.atom_tools.find_atoms.doctree new file mode 100644 index 00000000..5ae021d3 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.atom_tools.find_atoms.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.atom_tools.gauss_difference.doctree b/.doctrees/_autosummary/pyTEMlib.atom_tools.gauss_difference.doctree new file mode 100644 index 00000000..3c5f8714 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.atom_tools.gauss_difference.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.atom_tools.intensity_area.doctree b/.doctrees/_autosummary/pyTEMlib.atom_tools.intensity_area.doctree new file mode 100644 index 00000000..fa64d916 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.atom_tools.intensity_area.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.config_dir.doctree b/.doctrees/_autosummary/pyTEMlib.config_dir.doctree new file mode 100644 index 00000000..34e47d0d Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.config_dir.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.atoms_from_dictionary.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.atoms_from_dictionary.doctree new file mode 100644 index 00000000..81ad3f84 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.atoms_from_dictionary.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.ball_and_stick.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.ball_and_stick.doctree new file mode 100644 index 00000000..b5deb95e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.ball_and_stick.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.doctree new file mode 100644 index 00000000..3bdda619 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.get_dictionary.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.get_dictionary.doctree new file mode 100644 index 00000000..16164035 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.get_dictionary.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.get_projection.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.get_projection.doctree new file mode 100644 index 00000000..59f699db Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.get_projection.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.get_symmetry.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.get_symmetry.doctree new file mode 100644 index 00000000..e6c9b49f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.get_symmetry.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.jmol_viewer.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.jmol_viewer.doctree new file mode 100644 index 00000000..80c32dfe Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.jmol_viewer.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.plot_super_cell.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.plot_super_cell.doctree new file mode 100644 index 00000000..9be91b3f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.plot_super_cell.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.plot_unit_cell.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.plot_unit_cell.doctree new file mode 100644 index 00000000..3d955d70 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.plot_unit_cell.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.set_bond_radii.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.set_bond_radii.doctree new file mode 100644 index 00000000..0890cdad Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.set_bond_radii.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.crystal_tools.structure_by_name.doctree b/.doctrees/_autosummary/pyTEMlib.crystal_tools.structure_by_name.doctree new file mode 100644 index 00000000..0564eaa5 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.crystal_tools.structure_by_name.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.cartesian2polar.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.cartesian2polar.doctree new file mode 100644 index 00000000..efac893b Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.cartesian2polar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.circles.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.circles.doctree new file mode 100644 index 00000000..28856cbd Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.circles.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.doctree new file mode 100644 index 00000000..5c5f591d Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotCBED_parameter.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotCBED_parameter.doctree new file mode 100644 index 00000000..1e79879a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotCBED_parameter.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotHOLZ_parameter.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotHOLZ_parameter.doctree new file mode 100644 index 00000000..f892a722 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotHOLZ_parameter.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotKikuchi.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotKikuchi.doctree new file mode 100644 index 00000000..21c0833e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotKikuchi.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotSAED_parameter.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotSAED_parameter.doctree new file mode 100644 index 00000000..779c6593 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plotSAED_parameter.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plot_diffraction_pattern.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plot_diffraction_pattern.doctree new file mode 100644 index 00000000..81382d58 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plot_diffraction_pattern.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plot_reciprocal_unit_cell_2D.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plot_reciprocal_unit_cell_2D.doctree new file mode 100644 index 00000000..42d5ea5c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plot_reciprocal_unit_cell_2D.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plot_ring_pattern.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plot_ring_pattern.doctree new file mode 100644 index 00000000..f5bfcb32 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.plot_ring_pattern.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.topolar.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.topolar.doctree new file mode 100644 index 00000000..246fbe8d Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.topolar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.diffraction_plot.warp.doctree b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.warp.doctree new file mode 100644 index 00000000..ed218c2a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.diffraction_plot.warp.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.doctree b/.doctrees/_autosummary/pyTEMlib.doctree new file mode 100644 index 00000000..c4a92931 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.doctree b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.doctree new file mode 100644 index 00000000..d930821f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.get_propagator.doctree b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.get_propagator.doctree new file mode 100644 index 00000000..0a95de60 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.get_propagator.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.get_transmission.doctree b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.get_transmission.doctree new file mode 100644 index 00000000..5f7d4973 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.get_transmission.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.interaction_parameter.doctree b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.interaction_parameter.doctree new file mode 100644 index 00000000..2392b82d Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.interaction_parameter.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.make_chi.doctree b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.make_chi.doctree new file mode 100644 index 00000000..d0449232 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.make_chi.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.multi_slice.doctree b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.multi_slice.doctree new file mode 100644 index 00000000..575b8cfb Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.multi_slice.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.objective_lens_function.doctree b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.objective_lens_function.doctree new file mode 100644 index 00000000..9503bdfe Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.objective_lens_function.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.potential_1dim.doctree b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.potential_1dim.doctree new file mode 100644 index 00000000..aacdb6b0 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.potential_1dim.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.potential_2dim.doctree b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.potential_2dim.doctree new file mode 100644 index 00000000..797cbb9a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.dynamic_scattering.potential_2dim.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.detect_peaks.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.detect_peaks.doctree new file mode 100644 index 00000000..768f9f27 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.detect_peaks.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.detector_response.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.detector_response.doctree new file mode 100644 index 00000000..cfb8c4d2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.detector_response.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.doctree new file mode 100644 index 00000000..ff2095bb Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.find_elements.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.find_elements.doctree new file mode 100644 index 00000000..b95540ab Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.find_elements.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.fit_model.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.fit_model.doctree new file mode 100644 index 00000000..0372d2e9 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.fit_model.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.gaussian.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.gaussian.doctree new file mode 100644 index 00000000..91d034ea Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.gaussian.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.getFWHM.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.getFWHM.doctree new file mode 100644 index 00000000..18ebcb81 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.getFWHM.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.get_eds_cross_sections.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_eds_cross_sections.doctree new file mode 100644 index 00000000..9c9d66e7 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_eds_cross_sections.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.get_eds_xsection.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_eds_xsection.doctree new file mode 100644 index 00000000..acef400f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_eds_xsection.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.get_model.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_model.doctree new file mode 100644 index 00000000..8cb27473 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_model.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.get_peak.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_peak.doctree new file mode 100644 index 00000000..318363bb Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_peak.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.get_x_ray_lines.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_x_ray_lines.doctree new file mode 100644 index 00000000..78da9f64 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.get_x_ray_lines.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eds_tools.update_fit_values.doctree b/.doctrees/_autosummary/pyTEMlib.eds_tools.update_fit_values.doctree new file mode 100644 index 00000000..ff55ae8e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eds_tools.update_fit_values.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog.CompositionWidget.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog.CompositionWidget.doctree new file mode 100644 index 00000000..c2cce5dc Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog.CompositionWidget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog.doctree new file mode 100644 index 00000000..cfa9cbd8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog.get_sidebar.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog.get_sidebar.doctree new file mode 100644 index 00000000..fa461537 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog.get_sidebar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.EdgesAtCursor.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.EdgesAtCursor.doctree new file mode 100644 index 00000000..bf5420f1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.EdgesAtCursor.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.ElementalEdges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.ElementalEdges.doctree new file mode 100644 index 00000000..c9f3131c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.ElementalEdges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.InteractiveSpectrumImage.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.InteractiveSpectrumImage.doctree new file mode 100644 index 00000000..a8e3c781 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.InteractiveSpectrumImage.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.PeriodicTableWidget.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.PeriodicTableWidget.doctree new file mode 100644 index 00000000..b4bf174c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.PeriodicTableWidget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.RangeSelector.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.RangeSelector.doctree new file mode 100644 index 00000000..5d503033 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.RangeSelector.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.RegionSelector.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.RegionSelector.doctree new file mode 100644 index 00000000..6c4c3e21 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.RegionSelector.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.SIPlot.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.SIPlot.doctree new file mode 100644 index 00000000..351ca0a7 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.SIPlot.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.SpectrumPlot.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.SpectrumPlot.doctree new file mode 100644 index 00000000..7522a705 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.SpectrumPlot.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.doctree new file mode 100644 index 00000000..9cf61cdf Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.get_likely_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.get_likely_edges.doctree new file mode 100644 index 00000000..a85db278 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.get_likely_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.get_periodic_table_info.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.get_periodic_table_info.doctree new file mode 100644 index 00000000..17d1d8c8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.get_periodic_table_info.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.get_periodic_table_widget.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.get_periodic_table_widget.doctree new file mode 100644 index 00000000..c480beb0 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.get_periodic_table_widget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.make_box_layout.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.make_box_layout.doctree new file mode 100644 index 00000000..166ead4a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.make_box_layout.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.plot_EELS.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.plot_EELS.doctree new file mode 100644 index 00000000..3d3edb57 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dialog_utilities.plot_EELS.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_dlg.doctree b/.doctrees/_autosummary/pyTEMlib.eels_dlg.doctree new file mode 100644 index 00000000..83da9f2c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_dlg.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.add_element_to_dataset.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.add_element_to_dataset.doctree new file mode 100644 index 00000000..44994a33 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.add_element_to_dataset.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.add_peaks.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.add_peaks.doctree new file mode 100644 index 00000000..8542aaba Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.add_peaks.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.assign_likely_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.assign_likely_edges.doctree new file mode 100644 index 00000000..85af3a47 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.assign_likely_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.auto_chemical_composition.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.auto_chemical_composition.doctree new file mode 100644 index 00000000..8cfcba94 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.auto_chemical_composition.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.auto_id_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.auto_id_edges.doctree new file mode 100644 index 00000000..0d6cd2e6 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.auto_id_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.cl_model.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.cl_model.doctree new file mode 100644 index 00000000..b4337166 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.cl_model.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.doctree new file mode 100644 index 00000000..5a2053da Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.drude.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.drude.doctree new file mode 100644 index 00000000..a35283ed Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.drude.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.drude2.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.drude2.doctree new file mode 100644 index 00000000..28ceb30f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.drude2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.drude_lorentz.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.drude_lorentz.doctree new file mode 100644 index 00000000..6f9d1ea9 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.drude_lorentz.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.drude_simulation.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.drude_simulation.doctree new file mode 100644 index 00000000..b72b8091 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.drude_simulation.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.effective_collection_angle.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.effective_collection_angle.doctree new file mode 100644 index 00000000..72527ef5 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.effective_collection_angle.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.find_all_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_all_edges.doctree new file mode 100644 index 00000000..41cf7b91 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_all_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.find_associated_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_associated_edges.doctree new file mode 100644 index 00000000..02d615c8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_associated_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.find_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_edges.doctree new file mode 100644 index 00000000..73182d36 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.find_major_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_major_edges.doctree new file mode 100644 index 00000000..4f753530 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_major_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.find_maxima.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_maxima.doctree new file mode 100644 index 00000000..18142861 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_maxima.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.find_peaks.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_peaks.doctree new file mode 100644 index 00000000..87c6da8f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_peaks.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.find_white_lines.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_white_lines.doctree new file mode 100644 index 00000000..c26d347f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.find_white_lines.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_dataset.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_dataset.doctree new file mode 100644 index 00000000..69f1b75c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_dataset.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_edges.doctree new file mode 100644 index 00000000..dd9fbb41 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_edges2.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_edges2.doctree new file mode 100644 index 00000000..e2606d81 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_edges2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_model.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_model.doctree new file mode 100644 index 00000000..5059b7a9 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_model.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_peaks.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_peaks.doctree new file mode 100644 index 00000000..bb8674db Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.fit_peaks.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.fix_energy_scale.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.fix_energy_scale.doctree new file mode 100644 index 00000000..ecb1db57 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.fix_energy_scale.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.gauss.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.gauss.doctree new file mode 100644 index 00000000..805c7eb4 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.gauss.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.get_energy_shifts.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_energy_shifts.doctree new file mode 100644 index 00000000..168162c3 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_energy_shifts.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.get_resolution_functions.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_resolution_functions.doctree new file mode 100644 index 00000000..b5d1ebb3 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_resolution_functions.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.get_spectrum_eels_db.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_spectrum_eels_db.doctree new file mode 100644 index 00000000..942d3843 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_spectrum_eels_db.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.get_wave_length.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_wave_length.doctree new file mode 100644 index 00000000..0626cb1a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_wave_length.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.get_x_sections.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_x_sections.doctree new file mode 100644 index 00000000..0b77c8b2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_x_sections.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.get_z.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_z.doctree new file mode 100644 index 00000000..007b05ae Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.get_z.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.identify_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.identify_edges.doctree new file mode 100644 index 00000000..528a3079 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.identify_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.kroeger_core.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.kroeger_core.doctree new file mode 100644 index 00000000..a50e7eaa Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.kroeger_core.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.kroeger_core2.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.kroeger_core2.doctree new file mode 100644 index 00000000..e2c3ea36 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.kroeger_core2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.list_all_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.list_all_edges.doctree new file mode 100644 index 00000000..3cea71fb Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.list_all_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.lorentz.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.lorentz.doctree new file mode 100644 index 00000000..ab774366 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.lorentz.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.make_cross_sections.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.make_cross_sections.doctree new file mode 100644 index 00000000..8b976102 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.make_cross_sections.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.make_edges.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.make_edges.doctree new file mode 100644 index 00000000..3256d853 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.make_edges.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.model3.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.model3.doctree new file mode 100644 index 00000000..9656fdf2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.model3.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.model_ll.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.model_ll.doctree new file mode 100644 index 00000000..28f0076a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.model_ll.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.model_smooth.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.model_smooth.doctree new file mode 100644 index 00000000..e910f131 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.model_smooth.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.plot_dispersion.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.plot_dispersion.doctree new file mode 100644 index 00000000..5be0d7cd Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.plot_dispersion.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.power_law.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.power_law.doctree new file mode 100644 index 00000000..3bbc7f63 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.power_law.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.power_law_background.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.power_law_background.doctree new file mode 100644 index 00000000..3f105904 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.power_law_background.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.read_msa.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.read_msa.doctree new file mode 100644 index 00000000..e7753df2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.read_msa.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.residuals_ll.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.residuals_ll.doctree new file mode 100644 index 00000000..19f3c34e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.residuals_ll.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.residuals_ll2.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.residuals_ll2.doctree new file mode 100644 index 00000000..b051124c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.residuals_ll2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.residuals_smooth.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.residuals_smooth.doctree new file mode 100644 index 00000000..64d3769f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.residuals_smooth.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.resolution_function.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.resolution_function.doctree new file mode 100644 index 00000000..ffc43ccf Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.resolution_function.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.resolution_function2.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.resolution_function2.doctree new file mode 100644 index 00000000..c1d68cde Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.resolution_function2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.second_derivative.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.second_derivative.doctree new file mode 100644 index 00000000..53414d05 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.second_derivative.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.set_previous_quantification.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.set_previous_quantification.doctree new file mode 100644 index 00000000..e806ac04 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.set_previous_quantification.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.shift_on_same_scale.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.shift_on_same_scale.doctree new file mode 100644 index 00000000..10b1cb13 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.shift_on_same_scale.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.sort_peaks.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.sort_peaks.doctree new file mode 100644 index 00000000..597391f4 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.sort_peaks.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.xsec_xrpa.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.xsec_xrpa.doctree new file mode 100644 index 00000000..effe12d1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.xsec_xrpa.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.zl.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.zl.doctree new file mode 100644 index 00000000..490b054d Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.zl.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.eels_tools.zl_func.doctree b/.doctrees/_autosummary/pyTEMlib.eels_tools.zl_func.doctree new file mode 100644 index 00000000..50c9c112 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.eels_tools.zl_func.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.ChooseDataset.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.ChooseDataset.doctree new file mode 100644 index 00000000..0f36888a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.ChooseDataset.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.FileWidget.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.FileWidget.doctree new file mode 100644 index 00000000..5392afd1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.FileWidget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.add_dataset_from_file.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.add_dataset_from_file.doctree new file mode 100644 index 00000000..10500391 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.add_dataset_from_file.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.add_to_dict.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.add_to_dict.doctree new file mode 100644 index 00000000..f116638e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.add_to_dict.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.doctree new file mode 100644 index 00000000..90c08772 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.get_h5_filename.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.get_h5_filename.doctree new file mode 100644 index 00000000..fd7b1cbc Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.get_h5_filename.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.get_last_path.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.get_last_path.doctree new file mode 100644 index 00000000..6a800ab8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.get_last_path.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.get_main_channel.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.get_main_channel.doctree new file mode 100644 index 00000000..9ccdadef Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.get_main_channel.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.get_start_channel.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.get_start_channel.doctree new file mode 100644 index 00000000..4b4a6fc9 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.get_start_channel.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.h5_add_crystal_structure.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_add_crystal_structure.doctree new file mode 100644 index 00000000..1fe489db Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_add_crystal_structure.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.h5_add_to_structure.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_add_to_structure.doctree new file mode 100644 index 00000000..08216abb Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_add_to_structure.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.h5_get_crystal_structure.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_get_crystal_structure.doctree new file mode 100644 index 00000000..0fb23a8c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_get_crystal_structure.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.h5_group_to_dict.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_group_to_dict.doctree new file mode 100644 index 00000000..5fe6b9b1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_group_to_dict.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.h5_tree.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_tree.doctree new file mode 100644 index 00000000..5d1dfee0 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.h5_tree.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.log_results.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.log_results.doctree new file mode 100644 index 00000000..c48374b0 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.log_results.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.open_file.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.open_file.doctree new file mode 100644 index 00000000..7480296b Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.open_file.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.open_file_dialog_qt.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.open_file_dialog_qt.doctree new file mode 100644 index 00000000..19c9121a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.open_file_dialog_qt.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.read_cif.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.read_cif.doctree new file mode 100644 index 00000000..d57349cf Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.read_cif.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.read_dm3_info.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.read_dm3_info.doctree new file mode 100644 index 00000000..1c2881f9 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.read_dm3_info.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.read_essential_metadata.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.read_essential_metadata.doctree new file mode 100644 index 00000000..468bf5e1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.read_essential_metadata.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.read_nion_image_info.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.read_nion_image_info.doctree new file mode 100644 index 00000000..3fc9c4ae Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.read_nion_image_info.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.read_old_h5group.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.read_old_h5group.doctree new file mode 100644 index 00000000..4518cc0e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.read_old_h5group.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.read_poscar.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.read_poscar.doctree new file mode 100644 index 00000000..5bfb2f8c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.read_poscar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.save_dataset.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.save_dataset.doctree new file mode 100644 index 00000000..36f09635 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.save_dataset.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.save_dataset_dictionary.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.save_dataset_dictionary.doctree new file mode 100644 index 00000000..655101bb Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.save_dataset_dictionary.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.save_file_dialog_qt.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.save_file_dialog_qt.doctree new file mode 100644 index 00000000..a9a09f85 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.save_file_dialog_qt.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.save_path.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.save_path.doctree new file mode 100644 index 00000000..c1d3bccc Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.save_path.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.save_single_dataset.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.save_single_dataset.doctree new file mode 100644 index 00000000..9131c964 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.save_single_dataset.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.set_dimensions.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.set_dimensions.doctree new file mode 100644 index 00000000..57de13a8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.set_dimensions.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools.update_directory_list.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools.update_directory_list.doctree new file mode 100644 index 00000000..1ebbad39 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools.update_directory_list.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.file_tools_qt.doctree b/.doctrees/_autosummary/pyTEMlib.file_tools_qt.doctree new file mode 100644 index 00000000..3b331019 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.file_tools_qt.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.breadth_first_search.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.breadth_first_search.doctree new file mode 100644 index 00000000..3f3869ff Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.breadth_first_search.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.circum_center.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.circum_center.doctree new file mode 100644 index 00000000..9b5f72f2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.circum_center.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.doctree new file mode 100644 index 00000000..18cf5d4e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.find_interstitial_clusters.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.find_interstitial_clusters.doctree new file mode 100644 index 00000000..abb48015 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.find_interstitial_clusters.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.find_overlapping_spheres.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.find_overlapping_spheres.doctree new file mode 100644 index 00000000..12945b8f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.find_overlapping_spheres.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.find_polyhedra.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.find_polyhedra.doctree new file mode 100644 index 00000000..467c3eaf Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.find_polyhedra.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.get_bond_radii.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_bond_radii.doctree new file mode 100644 index 00000000..5cc94467 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_bond_radii.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.get_bonds.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_bonds.doctree new file mode 100644 index 00000000..dd07a978 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_bonds.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.get_connectivity_matrix.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_connectivity_matrix.doctree new file mode 100644 index 00000000..485ff5fc Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_connectivity_matrix.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.get_distortion_matrix.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_distortion_matrix.doctree new file mode 100644 index 00000000..87b08d68 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_distortion_matrix.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.get_maximum_view.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_maximum_view.doctree new file mode 100644 index 00000000..67a03137 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_maximum_view.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.get_non_periodic_supercell.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_non_periodic_supercell.doctree new file mode 100644 index 00000000..06f01056 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_non_periodic_supercell.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.get_polygons.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_polygons.doctree new file mode 100644 index 00000000..e6d16cb0 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_polygons.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.get_significant_vertices.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_significant_vertices.doctree new file mode 100644 index 00000000..01df77ba Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_significant_vertices.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.get_voronoi.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_voronoi.doctree new file mode 100644 index 00000000..0f2e6250 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.get_voronoi.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.interstitial_sphere_center.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.interstitial_sphere_center.doctree new file mode 100644 index 00000000..6cdcacf2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.interstitial_sphere_center.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.make_polygons.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.make_polygons.doctree new file mode 100644 index 00000000..62481ef1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.make_polygons.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.make_polyhedrons.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.make_polyhedrons.doctree new file mode 100644 index 00000000..57bd9199 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.make_polyhedrons.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.plot_atoms.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.plot_atoms.doctree new file mode 100644 index 00000000..273c9ee1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.plot_atoms.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.polygon_sort.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.polygon_sort.doctree new file mode 100644 index 00000000..7b4762e6 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.polygon_sort.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.set_bond_radii.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.set_bond_radii.doctree new file mode 100644 index 00000000..b7e2556e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.set_bond_radii.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.sort_polyhedra_by_vertices.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.sort_polyhedra_by_vertices.doctree new file mode 100644 index 00000000..a362e176 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.sort_polyhedra_by_vertices.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.transform_voronoi.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.transform_voronoi.doctree new file mode 100644 index 00000000..4f3d5c12 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.transform_voronoi.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort.doctree new file mode 100644 index 00000000..2b9d13f8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort_sitk.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort_sitk.doctree new file mode 100644 index 00000000..6aff9234 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort_sitk.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort_stack.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort_stack.doctree new file mode 100644 index 00000000..6bb1312b Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort_stack.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort_stack_sitk.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort_stack_sitk.doctree new file mode 100644 index 00000000..39d3cae0 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.undistort_stack_sitk.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_tools.voronoi_volumes.doctree b/.doctrees/_autosummary/pyTEMlib.graph_tools.voronoi_volumes.doctree new file mode 100644 index 00000000..5a5fcacb Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_tools.voronoi_volumes.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.doctree new file mode 100644 index 00000000..0596c5cb Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.get_boundary_polyhedra.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.get_boundary_polyhedra.doctree new file mode 100644 index 00000000..da2d1782 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.get_boundary_polyhedra.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_bonds.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_bonds.doctree new file mode 100644 index 00000000..a63cda15 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_bonds.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_polyhedron.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_polyhedron.doctree new file mode 100644 index 00000000..ed3e4c54 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_polyhedron.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_super_cell.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_super_cell.doctree new file mode 100644 index 00000000..f4a56699 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_super_cell.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_supercell.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_supercell.doctree new file mode 100644 index 00000000..9481b8ec Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_supercell.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_supercell_bonds.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_supercell_bonds.doctree new file mode 100644 index 00000000..579a1977 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_supercell_bonds.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_supercell_polyhedra.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_supercell_polyhedra.doctree new file mode 100644 index 00000000..a389a68f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_supercell_polyhedra.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_with_polyhedra.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_with_polyhedra.doctree new file mode 100644 index 00000000..23efa68b Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.plot_with_polyhedra.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.graph_viz.show_polyhedra.doctree b/.doctrees/_autosummary/pyTEMlib.graph_viz.show_polyhedra.doctree new file mode 100644 index 00000000..c31f9f9c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.graph_viz.show_polyhedra.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_dialog.doctree b/.doctrees/_autosummary/pyTEMlib.image_dialog.doctree new file mode 100644 index 00000000..00b35de4 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_dialog.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_dlg.doctree b/.doctrees/_autosummary/pyTEMlib.image_dlg.doctree new file mode 100644 index 00000000..edb80cbc Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_dlg.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.ImageWithLineProfile.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.ImageWithLineProfile.doctree new file mode 100644 index 00000000..f6a4ede2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.ImageWithLineProfile.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.adaptive_fourier_filter.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.adaptive_fourier_filter.doctree new file mode 100644 index 00000000..1f8c8177 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.adaptive_fourier_filter.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.align_crystal_reflections.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.align_crystal_reflections.doctree new file mode 100644 index 00000000..d3641de4 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.align_crystal_reflections.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.calculate_ctf.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.calculate_ctf.doctree new file mode 100644 index 00000000..cbbd1e6e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.calculate_ctf.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.calculate_scherzer.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.calculate_scherzer.doctree new file mode 100644 index 00000000..5bfbf4ec Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.calculate_scherzer.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.calibrate_image_scale.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.calibrate_image_scale.doctree new file mode 100644 index 00000000..93c11d5b Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.calibrate_image_scale.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.cart2pol.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.cart2pol.doctree new file mode 100644 index 00000000..7804dc2c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.cart2pol.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.cartesian2polar.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.cartesian2polar.doctree new file mode 100644 index 00000000..a68aefa3 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.cartesian2polar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.clean_svd.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.clean_svd.doctree new file mode 100644 index 00000000..c6290a0e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.clean_svd.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.complete_registration.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.complete_registration.doctree new file mode 100644 index 00000000..fb0d0752 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.complete_registration.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.crop_image_stack.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.crop_image_stack.doctree new file mode 100644 index 00000000..663959aa Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.crop_image_stack.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.decon_lr.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.decon_lr.doctree new file mode 100644 index 00000000..f2b63dc4 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.decon_lr.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.demon_registration.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.demon_registration.doctree new file mode 100644 index 00000000..249a9440 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.demon_registration.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.diffractogram_spots.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.diffractogram_spots.doctree new file mode 100644 index 00000000..9fd06c6a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.diffractogram_spots.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.doctree new file mode 100644 index 00000000..ae946dac Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.fourier_transform.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.fourier_transform.doctree new file mode 100644 index 00000000..7b0c4e1d Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.fourier_transform.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.get_rotation.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.get_rotation.doctree new file mode 100644 index 00000000..334eefe1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.get_rotation.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.get_wavelength.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.get_wavelength.doctree new file mode 100644 index 00000000..e0d48fa5 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.get_wavelength.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.histogram_plot.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.histogram_plot.doctree new file mode 100644 index 00000000..94cee16b Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.histogram_plot.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.pol2cart.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.pol2cart.doctree new file mode 100644 index 00000000..c0c96a3a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.pol2cart.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.power_spectrum.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.power_spectrum.doctree new file mode 100644 index 00000000..9cec8195 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.power_spectrum.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.rebin.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.rebin.doctree new file mode 100644 index 00000000..277f3b3e Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.rebin.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.rig_reg_drift.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.rig_reg_drift.doctree new file mode 100644 index 00000000..c0114bd0 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.rig_reg_drift.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.rigid_registration.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.rigid_registration.doctree new file mode 100644 index 00000000..e519062a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.rigid_registration.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.rotational_symmetry_diffractogram.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.rotational_symmetry_diffractogram.doctree new file mode 100644 index 00000000..3b37f901 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.rotational_symmetry_diffractogram.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.warp.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.warp.doctree new file mode 100644 index 00000000..0703d7a8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.warp.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.image_tools.xy2polar.doctree b/.doctrees/_autosummary/pyTEMlib.image_tools.xy2polar.doctree new file mode 100644 index 00000000..1fc54407 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.image_tools.xy2polar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_dialog.InfoWidget.doctree b/.doctrees/_autosummary/pyTEMlib.info_dialog.InfoWidget.doctree new file mode 100644 index 00000000..34174f72 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_dialog.InfoWidget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_dialog.doctree b/.doctrees/_autosummary/pyTEMlib.info_dialog.doctree new file mode 100644 index 00000000..764e832b Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_dialog.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_dialog.get_sidebar.doctree b/.doctrees/_autosummary/pyTEMlib.info_dialog.get_sidebar.doctree new file mode 100644 index 00000000..62302d99 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_dialog.get_sidebar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_dlg.doctree b/.doctrees/_autosummary/pyTEMlib.info_dlg.doctree new file mode 100644 index 00000000..39764bee Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_dlg.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_widget.EELSWidget.doctree b/.doctrees/_autosummary/pyTEMlib.info_widget.EELSWidget.doctree new file mode 100644 index 00000000..0c673cae Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_widget.EELSWidget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_widget.InfoWidget.doctree b/.doctrees/_autosummary/pyTEMlib.info_widget.InfoWidget.doctree new file mode 100644 index 00000000..b7081063 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_widget.InfoWidget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_widget.LowLossWidget.doctree b/.doctrees/_autosummary/pyTEMlib.info_widget.LowLossWidget.doctree new file mode 100644 index 00000000..11c1c631 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_widget.LowLossWidget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_widget.doctree b/.doctrees/_autosummary/pyTEMlib.info_widget.doctree new file mode 100644 index 00000000..b67127ca Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_widget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_widget.get_info_sidebar.doctree b/.doctrees/_autosummary/pyTEMlib.info_widget.get_info_sidebar.doctree new file mode 100644 index 00000000..7a10a7f5 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_widget.get_info_sidebar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.info_widget.get_low_loss_sidebar.doctree b/.doctrees/_autosummary/pyTEMlib.info_widget.get_low_loss_sidebar.doctree new file mode 100644 index 00000000..d2f4c8c3 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.info_widget.get_low_loss_sidebar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.interactive_eels.doctree b/.doctrees/_autosummary/pyTEMlib.interactive_eels.doctree new file mode 100644 index 00000000..d260eed2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.interactive_eels.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.interactive_image.doctree b/.doctrees/_autosummary/pyTEMlib.interactive_image.doctree new file mode 100644 index 00000000..bd33e9c6 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.interactive_image.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.Zuo_fig_3_18.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.Zuo_fig_3_18.doctree new file mode 100644 index 00000000..45857006 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.Zuo_fig_3_18.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.check_sanity.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.check_sanity.doctree new file mode 100644 index 00000000..e261afc3 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.check_sanity.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.doctree new file mode 100644 index 00000000..1cbe947a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.example.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.example.doctree new file mode 100644 index 00000000..9fdb1666 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.example.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.feq.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.feq.doctree new file mode 100644 index 00000000..ad58fb97 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.feq.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.find_angles.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.find_angles.doctree new file mode 100644 index 00000000..62dd5358 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.find_angles.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.find_nearest_zone_axis.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.find_nearest_zone_axis.doctree new file mode 100644 index 00000000..a114ee7c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.find_nearest_zone_axis.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_dynamically_allowed.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_dynamically_allowed.doctree new file mode 100644 index 00000000..0fa1dae2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_dynamically_allowed.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_metric_tensor.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_metric_tensor.doctree new file mode 100644 index 00000000..5a2a3029 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_metric_tensor.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_rotation_matrix.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_rotation_matrix.doctree new file mode 100644 index 00000000..4ff67e0c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_rotation_matrix.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_wavelength.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_wavelength.doctree new file mode 100644 index 00000000..6466c33a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.get_wavelength.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.kinematic_scattering.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.kinematic_scattering.doctree new file mode 100644 index 00000000..e100d303 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.kinematic_scattering.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.kinematic_scattering2.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.kinematic_scattering2.doctree new file mode 100644 index 00000000..4020d89c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.kinematic_scattering2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.make_pretty_labels.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.make_pretty_labels.doctree new file mode 100644 index 00000000..ed41dba3 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.make_pretty_labels.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.read_poscar.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.read_poscar.doctree new file mode 100644 index 00000000..15be214a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.read_poscar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.ring_pattern_calculation.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.ring_pattern_calculation.doctree new file mode 100644 index 00000000..c29d32c7 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.ring_pattern_calculation.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.scattering_matrix.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.scattering_matrix.doctree new file mode 100644 index 00000000..5fbd96ed Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.scattering_matrix.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.stage_rotation_matrix.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.stage_rotation_matrix.doctree new file mode 100644 index 00000000..6a892662 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.stage_rotation_matrix.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.vector_norm.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.vector_norm.doctree new file mode 100644 index 00000000..367ff00d Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.vector_norm.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.zone_mistilt.doctree b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.zone_mistilt.doctree new file mode 100644 index 00000000..ae009fd8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.kinematic_scattering.zone_mistilt.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.microscope.Microscope.doctree b/.doctrees/_autosummary/pyTEMlib.microscope.Microscope.doctree new file mode 100644 index 00000000..f6f9dbf4 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.microscope.Microscope.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.microscope.doctree b/.doctrees/_autosummary/pyTEMlib.microscope.doctree new file mode 100644 index 00000000..62298f1a Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.microscope.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.peak_dialog.PeakFitWidget.doctree b/.doctrees/_autosummary/pyTEMlib.peak_dialog.PeakFitWidget.doctree new file mode 100644 index 00000000..591b77a2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.peak_dialog.PeakFitWidget.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.peak_dialog.doctree b/.doctrees/_autosummary/pyTEMlib.peak_dialog.doctree new file mode 100644 index 00000000..1ee6bbaf Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.peak_dialog.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.peak_dialog.get_sidebar.doctree b/.doctrees/_autosummary/pyTEMlib.peak_dialog.get_sidebar.doctree new file mode 100644 index 00000000..17a57093 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.peak_dialog.get_sidebar.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.peak_dialog.smooth.doctree b/.doctrees/_autosummary/pyTEMlib.peak_dialog.smooth.doctree new file mode 100644 index 00000000..5e64c8e0 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.peak_dialog.smooth.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.peak_dlg.doctree b/.doctrees/_autosummary/pyTEMlib.peak_dlg.doctree new file mode 100644 index 00000000..ba215e61 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.peak_dlg.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.doctree new file mode 100644 index 00000000..ff83de43 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.get_chi.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_chi.doctree new file mode 100644 index 00000000..063965e1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_chi.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.get_chi_2.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_chi_2.doctree new file mode 100644 index 00000000..1157e793 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_chi_2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.get_d2chidu2.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_d2chidu2.doctree new file mode 100644 index 00000000..01303ba9 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_d2chidu2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.get_d2chidudv.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_d2chidudv.doctree new file mode 100644 index 00000000..7f58bcfb Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_d2chidudv.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.get_d2chidv2.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_d2chidv2.doctree new file mode 100644 index 00000000..3c3e6ea4 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_d2chidv2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.get_ronchigram.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_ronchigram.doctree new file mode 100644 index 00000000..c4f8e775 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_ronchigram.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.get_ronchigram_2.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_ronchigram_2.doctree new file mode 100644 index 00000000..44e78263 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_ronchigram_2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.get_source_energy_spread.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_source_energy_spread.doctree new file mode 100644 index 00000000..538e4de2 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_source_energy_spread.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.get_target_aberrations.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_target_aberrations.doctree new file mode 100644 index 00000000..c050b5b3 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.get_target_aberrations.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.make_chi.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.make_chi.doctree new file mode 100644 index 00000000..7ba04bd8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.make_chi.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.make_chi1.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.make_chi1.doctree new file mode 100644 index 00000000..af840516 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.make_chi1.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.make_gauss.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.make_gauss.doctree new file mode 100644 index 00000000..e0b8f246 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.make_gauss.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.make_lorentz.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.make_lorentz.doctree new file mode 100644 index 00000000..d26eca06 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.make_lorentz.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.print_aberrations.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.print_aberrations.doctree new file mode 100644 index 00000000..3e44c8c1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.print_aberrations.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.probe2.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.probe2.doctree new file mode 100644 index 00000000..fd5936bd Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.probe2.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.probe_tools.zero_loss_peak_weight.doctree b/.doctrees/_autosummary/pyTEMlib.probe_tools.zero_loss_peak_weight.doctree new file mode 100644 index 00000000..c244bbd1 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.probe_tools.zero_loss_peak_weight.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.sidpy_tools.ChooseDataset.doctree b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.ChooseDataset.doctree new file mode 100644 index 00000000..f4a866c7 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.ChooseDataset.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.sidpy_tools.doctree b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.doctree new file mode 100644 index 00000000..01e85845 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_dimensions_by_order.doctree b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_dimensions_by_order.doctree new file mode 100644 index 00000000..568611ed Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_dimensions_by_order.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_dimensions_by_type.doctree b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_dimensions_by_type.doctree new file mode 100644 index 00000000..59c0d01f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_dimensions_by_type.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_extent.doctree b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_extent.doctree new file mode 100644 index 00000000..922a6e5d Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_extent.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_image_dims.doctree b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_image_dims.doctree new file mode 100644 index 00000000..564756b9 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.get_image_dims.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.sidpy_tools.make_dummy_dataset.doctree b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.make_dummy_dataset.doctree new file mode 100644 index 00000000..df4e9908 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.make_dummy_dataset.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.sidpy_tools.plot.doctree b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.plot.doctree new file mode 100644 index 00000000..556e8174 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.sidpy_tools.plot.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.simulation_tools.doctree b/.doctrees/_autosummary/pyTEMlib.simulation_tools.doctree new file mode 100644 index 00000000..438a34aa Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.simulation_tools.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.simulation_tools.exciting_get_spectra.doctree b/.doctrees/_autosummary/pyTEMlib.simulation_tools.exciting_get_spectra.doctree new file mode 100644 index 00000000..2d7798c7 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.simulation_tools.exciting_get_spectra.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.simulation_tools.final_state_broadening.doctree b/.doctrees/_autosummary/pyTEMlib.simulation_tools.final_state_broadening.doctree new file mode 100644 index 00000000..0d1ed23f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.simulation_tools.final_state_broadening.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.version.doctree b/.doctrees/_autosummary/pyTEMlib.version.doctree new file mode 100644 index 00000000..f77ad0b7 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.version.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.CurveVisualizer.doctree b/.doctrees/_autosummary/pyTEMlib.viz.CurveVisualizer.doctree new file mode 100644 index 00000000..7f120bb8 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.CurveVisualizer.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.SpectrumView.doctree b/.doctrees/_autosummary/pyTEMlib.viz.SpectrumView.doctree new file mode 100644 index 00000000..7df64c4f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.SpectrumView.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.doctree b/.doctrees/_autosummary/pyTEMlib.viz.doctree new file mode 100644 index 00000000..0e08b07c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.find_edge_names.doctree b/.doctrees/_autosummary/pyTEMlib.viz.find_edge_names.doctree new file mode 100644 index 00000000..d37d1417 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.find_edge_names.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.plot.doctree b/.doctrees/_autosummary/pyTEMlib.viz.plot.doctree new file mode 100644 index 00000000..266a3f39 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.plot.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.plot_image.doctree b/.doctrees/_autosummary/pyTEMlib.viz.plot_image.doctree new file mode 100644 index 00000000..474a549c Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.plot_image.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.plot_spectrum.doctree b/.doctrees/_autosummary/pyTEMlib.viz.plot_spectrum.doctree new file mode 100644 index 00000000..7668f83b Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.plot_spectrum.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.plot_stack.doctree b/.doctrees/_autosummary/pyTEMlib.viz.plot_stack.doctree new file mode 100644 index 00000000..1f318e51 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.plot_stack.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.spectrum_view_plotly.doctree b/.doctrees/_autosummary/pyTEMlib.viz.spectrum_view_plotly.doctree new file mode 100644 index 00000000..117be2fc Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.spectrum_view_plotly.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.viz.verify_spectrum_dataset.doctree b/.doctrees/_autosummary/pyTEMlib.viz.verify_spectrum_dataset.doctree new file mode 100644 index 00000000..26169790 Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.viz.verify_spectrum_dataset.doctree differ diff --git a/.doctrees/_autosummary/pyTEMlib.xrpa_x_sections.doctree b/.doctrees/_autosummary/pyTEMlib.xrpa_x_sections.doctree new file mode 100644 index 00000000..e51ec23f Binary files /dev/null and b/.doctrees/_autosummary/pyTEMlib.xrpa_x_sections.doctree differ diff --git a/.doctrees/about.doctree b/.doctrees/about.doctree new file mode 100644 index 00000000..ffc3ee00 Binary files /dev/null and b/.doctrees/about.doctree differ diff --git a/.doctrees/contact.doctree b/.doctrees/contact.doctree new file mode 100644 index 00000000..61647eed Binary files /dev/null and b/.doctrees/contact.doctree differ diff --git a/.doctrees/credits.doctree b/.doctrees/credits.doctree new file mode 100644 index 00000000..52431803 Binary files /dev/null and b/.doctrees/credits.doctree differ diff --git a/.doctrees/environment.pickle b/.doctrees/environment.pickle new file mode 100644 index 00000000..b2a8ddc6 Binary files /dev/null and b/.doctrees/environment.pickle differ diff --git a/.doctrees/index.doctree b/.doctrees/index.doctree new file mode 100644 index 00000000..3b859ba9 Binary files /dev/null and b/.doctrees/index.doctree differ diff --git a/.doctrees/install.doctree b/.doctrees/install.doctree new file mode 100644 index 00000000..f5d0267d Binary files /dev/null and b/.doctrees/install.doctree differ diff --git a/.doctrees/notebooks/EELS/index.doctree b/.doctrees/notebooks/EELS/index.doctree new file mode 100644 index 00000000..ab844dbc Binary files /dev/null and b/.doctrees/notebooks/EELS/index.doctree differ diff --git a/.doctrees/notebooks/Imaging/index.doctree b/.doctrees/notebooks/Imaging/index.doctree new file mode 100644 index 00000000..1be4ac86 Binary files /dev/null and b/.doctrees/notebooks/Imaging/index.doctree differ diff --git a/.doctrees/revisions.doctree b/.doctrees/revisions.doctree new file mode 100644 index 00000000..5ad1f49e Binary files /dev/null and b/.doctrees/revisions.doctree differ diff --git a/.nojekyll b/.nojekyll new file mode 100644 index 00000000..e69de29b diff --git a/_autosummary/pyTEMlib.animation.InteractiveAberration.html b/_autosummary/pyTEMlib.animation.InteractiveAberration.html new file mode 100644 index 00000000..c692e257 --- /dev/null +++ b/_autosummary/pyTEMlib.animation.InteractiveAberration.html @@ -0,0 +1,136 @@ + + +
+ + + +Ewald sphere construction to explain Laue Circle and deficient HOLZ lines
+Parameters: +exact_bragg: boolean
+++whether to tilt into exact Bragg condition or along zone axis
+
whether to shift exact Bragg-condition onto zone axis origin
+first or second Laue zone only
+color of wave vectors and Ewald sphere
+Figures and Animations for TEM in jupyter notebooks +part of MSE 672 course at UTK
+Author: Gerd Duscher +revision: 01/11/2021 +03/17/2021 added Aberration Animation
+Functions
++ | add aperture to propagate beam plot |
+
+ | add lens to propagate beam plot |
+
+ | Ewald sphere construction to explain Laue Circle and deficient HOLZ lines |
+
+ | + |
+ | Sketch of geometric ray diagram od one lens |
+
+ | geometrical propagation of light rays from given source |
+
Classes
++ | ### Interactive explanation of aberrations |
+
+ | ### Interactive explanation of magnification |
+
geometrical propagation of light rays from given source
+source_position (list) – location of the source (z0, x0) along and off axis (in mm)
numerical_aperture (float) – numerical aperture of the beam (in degrees)
number_of_rays (int) – number of rays to trace
lens_positions (numpy array) – array with the location of the lenses
focal_lengths (numpy array) – array with the focal length of lenses
lens_labels (list of string) – label for the nature of lenses
color (str) – color of the rays on plot
Fits a Gaussian in a blob of an image
+image (np.array or sidpy Dataset) –
atoms (list or np.array) – positions of atoms
radius (float) – radius of circular mask to define fitting of Gaussian
max_int (float) – optional - maximum intensity to be considered for fitting (to exclude contaminated areas for example)
min_int (float) – optional - minimum intensity to be considered for fitting (to exclude contaminated holes for example)
max_dist (float) – optional - maximum distance of movement of Gaussian during fitting
sym – dictionary containing new atom positions and other output such as intensity of the fitted Gaussian
+Difference between part of an image and a Gaussian +This function is used int he atom refine function of pyTEMlib
+params (list) – list of Gaussian parameters [width, position_x, position_y, intensity]
area (numpy array) – 2D matrix = part of an image
numpy array
+flattened array of difference
+Atom detection
+All atom detection is done here +Everything is in unit of pixel!!
+Author: Gerd Duscher
+part of pyTEMlib
+a pycroscopy package
+Functions
++ | Fits a Gaussian in a blob of an image |
+
+ | A wrapper for sklearn.cluster kmeans clustering of atoms. |
+
+ | Find atoms is a simple wrapper for blob_log in skimage.feature |
+
+ | Difference between part of an image and a Gaussian This function is used int he atom refine function of pyTEMlib |
+
+ | integrated intensity of atoms in an image with a mask around each atom of radius radius |
+
config_dir: setup of directory ~/.pyTEMlib for custom sources and database
+Calculates the data to plot a ball and stick model
+atoms (ase.Atoms object) – object containing the structural information like ‘cell’, ‘positions’, and ‘symbols’ .
extend (integer or list f 3 integers) – The extend argument scales the effective cell in which atoms +will be included. It must either be a list of three integers or a single +integer scaling all 3 directions. By setting this value to one, +all corner and edge atoms will be included in the returned cell. +This will of cause make the returned cell non-repeatable, but this is +very useful for visualisation.
max_bond_length (1 float) – The max_bond_length argument defines the distance for which a bond will be shown. +If max_bond_length is zero, the tabulated atom radii will be used.
super_cell – structure with additional information in info dictionary
+ase.Atoms object
+crystal_tools
+part of pyTEMlib
+Author: Gerd Duscher
+Provides convenient functions to make most regular crystal structures
+Contains also a dictionary of crystal structures and atomic form factors
+everything is in SI units, except length is given in nm. +angles are assumed to be in degree but will be internally converted to rad
+See the notebooks for examples of these routines
+Functions
++ | + |
+ | Calculates the data to plot a ball and stick model |
+
+ | structure dictionary from ase.Atoms object |
+
+ | + |
+ | Symmetry analysis with spglib |
+
+ | jmol viewer of ase .Atoms object requires jupyter-jsmol to be installed (available through conda or pip) |
+
+ | make a super_cell to plot with extra atoms at periodic boundaries |
+
+ | Simple plot of unit cell |
+
+ | + |
+ | Provides crystal structure in ase.Atoms format. |
+
jmol viewer of ase .Atoms object +requires jupyter-jsmol to be installed (available through conda or pip)
+structure info
+size of unit_cell; maximum = 8
+view
+JsmolView object
+Example
+from jupyter_jsmol import JsmolView +import ase +import ase.build +import itertools +import numpy as np +atoms = ase.build.bulk(‘Cu’, ‘fcc’, a=5.76911, cubic=True) +for pos in list(itertools.product([0.25, .75], repeat=3)):
+++atoms += ase.Atom(‘Al’, al2cu.cell.lengths()*pos)
+
view = plot_ase(atoms, size = 8) +display(view)
+Provides crystal structure in ase.Atoms format. +Additional information is stored in the info attribute as a dictionary
+Please note that the chemical expressions are not case-sensitive.
+atoms – structure
+ase.Atoms
+Example
+>> # for a list of pre-defined crystal structures +>> import pyTEMlib.crystal_tools +>> print(pyTEMlib.crystal_tools.crystal_data_base.keys()) +>> +>> atoms = pyTEMlib.crystal_tools.structure_by_name(‘Silicon’) +>> print(atoms) +>> print(atoms.info)
+Make a scatter plot of circles. +Similar to plt.scatter, but the size of circles are in data scale. +:param x: Input data +:type x: scalar or array_like, shape (n, ) +:param y: Input data +:type y: scalar or array_like, shape (n, ) +:param s: Radius of circles. +:type s: scalar or array_like, shape (n, ) +:param c: c can be a single color format string, or a sequence of color
+++specifications of length N, or a sequence of N numbers to be +mapped to colors using the cmap and norm specified via kwargs. +Note that c should not be a single numeric RGB or RGBA sequence +because that is indistinguishable from an array of values +to be colormapped. (If you insist, use color instead.) +c can be a 2-D array in which the rows are RGB or RGBA, however.
+
vmin (scalar, optional, default: None) – vmin and vmax are used in conjunction with norm to normalize +luminance data. If either are None, the min and max of the +color array is used.
vmax (scalar, optional, default: None) – vmin and vmax are used in conjunction with norm to normalize +luminance data. If either are None, the min and max of the +color array is used.
kwargs (~matplotlib.collections.Collection properties) – Eg. alpha, edgecolor(ec), facecolor(fc), linewidth(lw), linestyle(ls), +norm, cmap, transform, etc.
paths
+~matplotlib.collections.PathCollection
+Examples
+a = np.arange(11) +circles(a, a, s=a*0.2, c=a, alpha=0.5, ec=’none’) +plt.colorbar() +License +——– +This code is under [The BSD 3-Clause License] +(http://opensource.org/licenses/BSD-3-Clause)
+Functions
++ | + |
+ | Make a scatter plot of circles. Similar to plt.scatter, but the size of circles are in data scale. :param x: Input data :type x: scalar or array_like, shape (n, ) :param y: Input data :type y: scalar or array_like, shape (n, ) :param s: Radius of circles. :type s: scalar or array_like, shape (n, ) :param c: c can be a single color format string, or a sequence of color specifications of length N, or a sequence of N numbers to be mapped to colors using the cmap and norm specified via kwargs. Note that c should not be a single numeric RGB or RGBA sequence because that is indistinguishable from an array of values to be colormapped. (If you insist, use color instead.) c can be a 2-D array in which the rows are RGB or RGBA, however. :type c: color or sequence of color, optional, default : 'b' :param vmin: vmin and vmax are used in conjunction with norm to normalize luminance data. If either are None, the min and max of the color array is used. :type vmin: scalar, optional, default: None :param vmax: vmin and vmax are used in conjunction with norm to normalize luminance data. If either are None, the min and max of the color array is used. :type vmax: scalar, optional, default: None :param kwargs: Eg. alpha, edgecolor(ec), facecolor(fc), linewidth(lw), linestyle(ls), norm, cmap, transform, etc. :type kwargs: ~matplotlib.collections.Collection properties. |
+
+ | + |
+ | + |
+ | + |
+ | + |
+ | Plot of spot diffraction pattern with matplotlib Plot of spot diffraction pattern with matplotlib |
+
+ | Plot # unit cell in reciprocal space in 2D |
+
+ | Plot of ring diffraction pattern with matplotlib |
+
+ | Transform img to its polar coordinate representation. |
+
+ | Define original polar grid |
+
Plot of spot diffraction pattern with matplotlib +Plot of spot diffraction pattern with matplotlib
+atoms (dictionary or ase.Atoms object) – information stored as dictionary either directly or in info attribute of ase.Atoms object
diffraction_pattern (None or sidpy.Dataset) – diffraction pattern in background
grey (bool) – plotting in greyscale if True
fig – reference to matplotlib figure
+matplotlib figure
+Plot of ring diffraction pattern with matplotlib
+atoms (dictionary or sidpy.Dataset) – information stored as dictionary either directly or in metadata attribute of sidpy.Dataset
grey (bool) – plotting in greyscale if True
fig – reference to matplotlib figure
+matplotlib figure
+Define original polar grid
+diffraction pattern
+coordinates of center in pixel
+numpy array of diffraction pattern in polar coordinates
+Get propagator function
+has to be convoluted with wave function after transmission
+number of pixels of one axis in square image
+distance between layers
+number of layers to make a propagator
+wavelength of incident electrons
+field of view of image
+relative bandwidth to avoid anti-aliasing
+propagator
+complex numpy array (layers x size_in_pixel x size_in_pixel)
+Get transmission function
+has to be multiplied in real space with wave function
+potential of a layer
+acceleration voltage in V
+complex numpy array (nxn)
+Dynamic Scattering Library for Multi-Slice Calculations
+author: Gerd Duscher
+Functions
++ | Get propagator function |
+
+ | Get transmission function |
+
+ | Calculates interaction parameter sigma |
+
+ | ### + |
+
+ | Multi-Slice Calculation |
+
+ | Objective len function to be convoluted with exit wave to derive image function |
+
+ | Calculates the projected potential of an atom of element |
+
+ | Make a super-cell with potentials |
+
Calculates interaction parameter sigma
+acceleration voltage in volt
+interaction parameter – interaction parameter (dimensionless)
+float
+### +# Aberration function chi +### +phi and theta are meshgrids of the angles in polar coordinates. +aberrations is a dictionary with the aberrations coefficients +Attention: an empty aberration dictionary will give you a perfect aberration
+Multi-Slice Calculation
+The wave function will be changed iteratively
+wave (complex numpy array (nxn)) – starting wave function
number_of_unit_cell_z (int) – this gives the thickness in multiples of c lattice parameter
number_layers (int) – number of layers per unit cell
transmission (complex numpy array) – transmission function
propagator (complex numpy array) – propagator function
complex numpy array
+Objective len function to be convoluted with exit wave to derive image function
+aberrations in nm should at least contain defocus (C10), and spherical aberration (C30)
+number of pixel in x direction
+number of pixel in y direction
+field of view of potential
+wavelength in nm
+aperture size in 1/nm
+object function: numpy array (nx x ny) +extent: list
+Calculates the projected potential of an atom of element
+The projected potential will be in units of V nm^2, +however, internally we will use Angstrom instead of nm! +The basis for these calculations are the atomic form factors of Kirkland 2𝑛𝑑 edition +following the equation in Appendix C page 252.
+name of ‘element
+impact parameters (distances from atom position) in nm
+projected potential in units of V nm^2
+numpy array (nxn)
+Example
+tags = {}
+tags[‘acceleration_voltage_V’] = 30000
+tags[‘detector’] ={} +tags[‘detector’][‘layers’] ={}
+## layer thicknesses of commen materials in EDS detectors in m +tags[‘detector’][‘layers’][‘alLayer’] = {} +tags[‘detector’][‘layers’][‘alLayer’][‘thickness’] = 30 *1e-9 # in m +tags[‘detector’][‘layers’][‘alLayer’][‘Z’] = 13
+tags[‘detector’][‘layers’][‘deadLayer’] = {} +tags[‘detector’][‘layers’][‘deadLayer’][‘thickness’] = 100 *1e-9 # in m +tags[‘detector’][‘layers’][‘deadLayer’][‘Z’] = 14
+tags[‘detector’][‘layers’][‘window’] = {} +tags[‘detector’][‘layers’][‘window’][‘thickness’] = 100 *1e-9 # in m +tags[‘detector’][‘layers’][‘window’][‘Z’] = 6
+tags[‘detector’][‘detector’] = {} +tags[‘detector’][‘detector’][‘thickness’] = 45 * 1e-3 # in m +tags[‘detector’][‘detector’][‘Z’] = 14 +tags[‘detector’][‘detector’][‘area’] = 30 * 1e-6 #in m2
+energy_scale = np.linspace(.1,60,1199)*1000 i eV +detector_response(tags, energy_scale)
+eds_tools +Model based quantification of energy-dispersive X-ray spectroscopy data +Copyright by Gerd Duscher
+The University of Tennessee, Knoxville +Department of Materials Science & Engineering
+Sources:
+everything is in SI units, except length is given in nm and angles in mrad.
+See the notebooks for examples of these routines
+All the input and output is done through a dictionary which is to be found in the meta_data +attribute of the sidpy.Dataset
+Functions
++ | + |
+ | Example + |
+
+ | + |
+ | + |
+ | + |
+ | + |
+ | + |
+ | + |
+ | + |
+ | + |
+ | + |
+ | + |
Bases: object
Methods
+
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
QT dialog window for EELS compositional analysis
+Author: Gerd Duscher
+Functions
++ | + |
Classes
++ | + |
Bases: object
Adds a Cursor to a plot, which plots all major (possible) ionization edges at +the cursor location if left (right) mouse button is clicked.
+matplotlib axis
+energy_scale of spectrum
+numpy array
+intensities of spectrum
+numpy array
+optional parameter maximum_chemical_shift which allows to change the energy range in which the edges +are searched.
+Example
+fig, ax = plt.subplots() +ax.plot(energy_scale, spectrum) +cursor = EdgesAtCursor(ax, energy_scale, spectrum)
+see Chapter4 ‘CH4-Working_with_X-Sections’ notebook
+Methods
+
|
++ |
|
++ |
|
++ |
|
++ |
Bases: object
Adds ionization edges of element z to plot with axis ax
+There is an optional parameter maximum_chemical_shift which allows to change +the energy range in which the edges are searched.
+available functions: +- update(): updates the drawing of ionization edges +- set_edge(Z) : changes atomic number and updates everything accordingly +- disconnect: makes everything invisible and stops drawing +- reconnect: undo of disconnect
+usage: +>> fig, ax = plt.subplots() +>> ax.plot(energy_scale, spectrum) +>> Z= 42 +>> cursor = ElementalEdges(ax, Z)
+see Chapter4 ‘CH4-Working_with_X-Sections’ notebook
+Methods
+
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
Bases: object
Interactive spectrum imaging plot
+[‘image’]: displayed image +[‘data’]: data cube +[‘intensity_scale_ppm’]: intensity scale +[‘ylabel’]: intensity label +[‘spectra’] dictionary which contains dictionaries for each spectrum style [‘1-2’]:
+++[‘spectrum’] = tags[‘cube’][y,x,:] +[‘spectra’][f’{x}-{y}’][‘energy_scale’] = tags[‘energy_scale’] +[‘intensity_scale’] = 1/tags[‘cube’][y,x,:].sum()*1e6
+
Please note the possibility to load any image for the selection of the spectrum +Also there is the possibility to display the survey image.
+‘fix_energy’: set zero-loss peak maximum to zero !! Low loss spectra only!! +‘fit_zero_loss’: fit zero-loss peak with model function !! Low loss spectra only!! +‘fit_low_loss’: fit low-loss spectrum with model peaks !! Low loss spectra only!!
+‘fit_composition’: fit core-loss spectrum with background and cross-sections!! Core loss spectra only!! +‘fit_ELNES’: fit core-loss edge with model peaks !! Core loss spectra only!!
+Methods
+
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
Bases: object
ipywidget to get a selection of elements.
+Elements that are not having a valid cross-sections are disabled.
+list of strings – use get_output() function
+elements.
+Methods
+
|
++ |
|
++ |
Bases: RectangleSelector
Select ranges of edge fitting interactively
+Methods
++ | Add a state to define the widget's behavior. |
+
+ | Clear the selection and set the selector ready to make a new one. |
+
+ | Connect the major canvas events to methods. |
+
+ | Connect a callback function with an event. |
+
+ | Disconnect all events created by this widget. |
+
|
++ |
+ | Get whether the widget is active. |
+
+ | Get the visibility of the selector artists. |
+
+ | Return whether event should be ignored. |
+
+ | Key press event handler and validator for all selection widgets. |
+
+ | Key release event handler and validator. |
+
+ | Mouse scroll event handler and validator. |
+
+ | Cursor move event handler and validator. |
+
+ | Button press handler and validator. |
+
+ | Button release event handler and validator. |
+
+ | Remove a state to define the widget's behavior. |
+
+ | Set whether the widget is active. |
+
+ | Set the properties of the handles selector artist. |
+
+ | Set the properties of the selector artist. |
+
+ | Set the visibility of the selector artists. |
+
+ | Draw using blit() or draw_idle(), depending on |
+
+ | Force an update of the background. |
+
Attributes
++ | Is the widget active? |
+
+ | Tuple of the artists of the selector. |
+
+ | Center of rectangle in data coordinates. |
+
+ | Corners of rectangle in data coordinates from lower left, moving clockwise. |
+
|
++ |
+ | Midpoint of rectangle edges in data coordinates from left, moving anti-clockwise. |
+
|
++ |
+ | Return (xmin, xmax, ymin, ymax) in data coordinates as defined by the bounding box before rotation. |
+
+ | Return an array of shape (2, 5) containing the x ( |
+
+ | Rotation in degree in interval [-45°, 45°]. |
+
|
++ |
|
++ |
Is the widget active?
+Add a state to define the widget’s behavior. See the +state_modifier_keys parameters for details.
+state (str) – Must be a supported state of the selector. See the +state_modifier_keys parameters for details.
+ValueError – When the state is not supported by the selector.
+Tuple of the artists of the selector.
+Center of rectangle in data coordinates.
+Clear the selection and set the selector ready to make a new one.
+Connect the major canvas events to methods.
+Connect a callback function with an event.
+This should be used in lieu of figure.canvas.mpl_connect
since this
+function stores callback ids for later clean up.
Corners of rectangle in data coordinates from lower left, +moving clockwise.
+Disconnect all events created by this widget.
+Midpoint of rectangle edges in data coordinates from left, +moving anti-clockwise.
+Return (xmin, xmax, ymin, ymax) in data coordinates as defined by the +bounding box before rotation.
+Return an array of shape (2, 5) containing the
+x (RectangleSelector.geometry[1, :]
) and
+y (RectangleSelector.geometry[0, :]
) data coordinates of the four
+corners of the rectangle starting and ending in the top left corner.
Get whether the widget is active.
+Get the visibility of the selector artists.
+Return whether event should be ignored.
+This method should be called at the beginning of any event callback.
+Key press event handler and validator for all selection widgets.
+Key release event handler and validator.
+Mouse scroll event handler and validator.
+Cursor move event handler and validator.
+Button press handler and validator.
+Button release event handler and validator.
+Remove a state to define the widget’s behavior. See the +state_modifier_keys parameters for details.
+state (str) – Must be a supported state of the selector. See the +state_modifier_keys parameters for details.
+ValueError – When the state is not supported by the selector.
+Rotation in degree in interval [-45°, 45°]. The rotation is limited in +range to keep the implementation simple.
+Set whether the widget is active.
+Set the properties of the handles selector artist. See the +handle_props argument in the selector docstring to know which +properties are supported.
+Set the properties of the selector artist. See the props argument +in the selector docstring to know which properties are supported.
+Set the visibility of the selector artists.
+Draw using blit() or draw_idle(), depending on self.useblit
.
Force an update of the background.
+Bases: object
Selects fitting region and the regions that are excluded for each edge.
+Select a region with a spanSelector and then type ‘a’ for all the fitting region or a number for the edge +you want to define the region excluded from the fit (solid state effects).
+see Chapter4 ‘CH4-Working_with_X-Sections,ipynb’ notebook
+Methods
+
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
Interactive routines for EELS analysis
+this file provides additional dialogs for EELS quantification
+Author: Gerd Duscher
+Functions
++ | get likely ionization edges within energy_scale |
+
+ | Info for periodic table dialog |
+
+ | + |
+ | + |
Classes
++ | Adds a Cursor to a plot, which plots all major (possible) ionization edges at the cursor location if left (right) mouse button is clicked. |
+
+ | Adds ionization edges of element z to plot with axis ax |
+
+ | Interactive spectrum imaging plot |
+
+ | ipywidget to get a selection of elements. |
+
+ | Select ranges of edge fitting interactively |
+
+ | Selects fitting region and the regions that are excluded for each edge. |
+
+ | + |
+ | + |
+ | Public constructor |
+
Bases: HBox
Public constructor
+Methods
++ | Adds a class to the top level element of the widget. |
+
+ | Dynamically add trait attributes to the Widget. |
+
+ | Blur the widget. |
+
+ | Get a dict of all event handlers defined on this class, not a parent. |
+
+ | Get a dict of all the traitlets defined on this class, not a parent. |
+
+ | Get a list of all the names of this class' traits. |
+
+ | Get a |
+
+ | Close method. |
+
|
++ |
+ | Focus on the widget. |
+
+ | Returns the full state for a widget manager for embedding |
+
+ | Gets the widget state, or a piece of it. |
+
|
++ |
+ | Static method, called when a widget is constructed. |
+
+ | Class method, called when the comm-open message on the "jupyter.widget.control" comm channel is received |
+
+ | Returns True if the object has a trait with the specified name. |
+
+ | Hold syncing any state until the outermost context manager exits |
+
+ | Context manager for bundling trait change notifications and cross validation. |
+
|
++ |
+ | Called when a property has changed. |
+
+ | Setup a handler to be called when a trait changes. |
+
|
++ |
+ | (Un)Register a custom msg receive callback. |
+
+ | DEPRECATED: Setup a handler to be called when a trait changes. |
+
+ | Registers a callback to be called when a widget is constructed. |
+
+ | Open a comm to the frontend if one isn't already open. |
+
+ | Removes a class from the top level element of the widget. |
+
+ | Sends a custom msg to the widget model in the front-end. |
+
+ | Sends the widget state, or a piece of it, to the front-end, if it exists. |
+
|
++ |
+ | Called when a state is received from the front-end. |
+
+ | Forcibly sets trait attribute, including read-only attributes. |
+
+ | This is called before self.__init__ is called. |
+
+ | Return a trait's default value or a dictionary of them |
+
+ | Get a |
+
+ | Returns True if the specified trait has a value. |
+
+ | Get metadata values for trait by key. |
+
+ | Get a list of all the names of this class' traits. |
+
+ | A |
+
+ | Get a |
+
+ | Remove a trait change handler. |
+
+ | Remove trait change handlers of any type for the specified name. |
+
+ | Draw line in plot |
+
|
++ |
|
++ |
Attributes
++ | Use a predefined styling for the box. |
+
+ | List of widget children |
+
|
+A trait which allows any value. |
+
+ | A contextmanager for running a block with our cross validation lock set to True. |
+
+ | The traits which are synced. |
+
|
+An instance trait which coerces a dict to an instance. |
+
|
+A trait whose value must be an instance of a specified class. |
+
+ | Gets the model id of this widget. |
+
+ | Is widget tabbable? |
+
+ | A tooltip caption. |
+
|
++ |
|
++ |
Object disposal
+Adds a class to the top level element of the widget.
+Doesn’t add the class if it already exists.
+Dynamically add trait attributes to the Widget.
+Blur the widget.
+Use a predefined styling for the box.
+List of widget children
+Get a dict of all event handlers defined on this class, not a parent.
+Works like event_handlers
, except for excluding traits from parents.
Get a dict of all the traitlets defined on this class, not a parent.
+Works like class_traits, except for excluding traits from parents.
+Get a list of all the names of this class’ traits.
+This method is just like the trait_names()
method,
+but is unbound.
Get a dict
of all the traits of this class. The dictionary
+is keyed on the name and the values are the TraitType objects.
This method is just like the traits()
method, but is unbound.
The TraitTypes returned don’t know anything about the values +that the various HasTrait’s instances are holding.
+The metadata kwargs allow functions to be passed in which +filter traits based on metadata values. The functions should +take a single value as an argument and return a boolean. If +any function returns False, then the trait is not included in +the output. If a metadata key doesn’t exist, None will be passed +to the function.
+Close method.
+Closes the underlying comm. +When the comm is closed, all of the widget views are automatically +removed from the front-end.
+A contextmanager for running a block with our cross validation lock set +to True.
+At the end of the block, the lock’s value is restored to its value +prior to entering the block.
+Focus on the widget.
+Returns the full state for a widget manager for embedding
+drop_defaults – when True, it will not include default value
widgets – list with widgets to include in the state (or all widgets when None)
Gets the widget state, or a piece of it.
+key (unicode or iterable (optional)) – A single property’s name or iterable of property names to get.
+state (dict of states)
metadata (dict) – metadata for each field: {key: metadata}
Static method, called when a widget is constructed.
+Class method, called when the comm-open message on the +“jupyter.widget.control” comm channel is received
+Returns True if the object has a trait with the specified name.
+Hold syncing any state until the outermost context manager exits
+Context manager for bundling trait change notifications and cross +validation.
+Use this when doing multiple trait assignments (init, config), to avoid +race conditions in trait notifiers requesting other trait values. +All trait notifications will fire after all values have been assigned.
+The traits which are synced.
+Gets the model id of this widget.
+If a Comm doesn’t exist yet, a Comm will be created automagically.
+Called when a property has changed.
+Setup a handler to be called when a trait changes.
+This is used to setup dynamic notifications of trait changes.
+handler (callable) – A callable that is called when a trait changes. Its
+signature should be handler(change)
, where change
is a
+dictionary. The change dictionary at least holds a ‘type’ key.
+* type
: the type of notification.
+Other keys may be passed depending on the value of ‘type’. In the
+case where type is ‘change’, we also have the following keys:
+* owner
: the HasTraits instance
+* old
: the old value of the modified trait attribute
+* new
: the new value of the modified trait attribute
+* name
: the name of the modified trait attribute.
names (list, str, All) – If names is All, the handler will apply to all traits. If a list +of str, handler will apply to all names in the list. If a +str, the handler will apply just to that name.
type (str, All (default: 'change')) – The type of notification to filter by. If equal to All, then all +notifications are passed to the observe handler.
(Un)Register a custom msg receive callback.
+callback (callable) –
callback will be passed three arguments when a message arrives:
+callback(widget, content, buffers)
+
remove (bool) – True if the callback should be unregistered.
DEPRECATED: Setup a handler to be called when a trait changes.
+This is used to setup dynamic notifications of trait changes.
+Static handlers can be created by creating methods on a HasTraits +subclass with the naming convention ‘_[traitname]_changed’. Thus, +to create static handler for the trait ‘a’, create the method +_a_changed(self, name, old, new) (fewer arguments can be used, see +below).
+If remove is True and handler is not specified, all change +handlers for the specified name are uninstalled.
+handler (callable, None) – A callable that is called when a trait changes. Its +signature can be handler(), handler(name), handler(name, new), +handler(name, old, new), or handler(name, old, new, self).
name (list, str, None) – If None, the handler will apply to all traits. If a list +of str, handler will apply to all names in the list. If a +str, the handler will apply just to that name.
remove (bool) – If False (the default), then install the handler. If True +then unintall it.
Registers a callback to be called when a widget is constructed.
+The callback must have the following signature: +callback(widget)
+Open a comm to the frontend if one isn’t already open.
+Removes a class from the top level element of the widget.
+Doesn’t remove the class if it doesn’t exist.
+Sends a custom msg to the widget model in the front-end.
+ +Sends the widget state, or a piece of it, to the front-end, if it exists.
+key (unicode, or iterable (optional)) – A single property’s name or iterable of property names to sync with the front-end.
+Called when a state is received from the front-end.
+Forcibly sets trait attribute, including read-only attributes.
+This is called before self.__init__ is called.
+Is widget tabbable?
+A tooltip caption.
+Return a trait’s default value or a dictionary of them
+Notes
+Dynamically generated default values may +depend on the current state of the object.
+Get a dict
of all the event handlers of this class.
name (str (default: None)) – The name of a trait of this class. If name is None
then all
+the event handlers of this class will be returned instead.
The event handlers associated with a trait name, or all event handlers.
+Returns True if the specified trait has a value.
+This will return false even if getattr
would return a
+dynamically generated default value. These default values
+will be recognized as existing only after they have been
+generated.
Example
+class MyClass(HasTraits):
+ i = Int()
+
+mc = MyClass()
+assert not mc.trait_has_value("i")
+mc.i # generates a default value
+assert mc.trait_has_value("i")
+
Get metadata values for trait by key.
+Get a list of all the names of this class’ traits.
+A dict
of trait names and their values.
The metadata kwargs allow functions to be passed in which +filter traits based on metadata values. The functions should +take a single value as an argument and return a boolean. If +any function returns False, then the trait is not included in +the output. If a metadata key doesn’t exist, None will be passed +to the function.
+A dict
of trait names and their values.
Notes
+Trait values are retrieved via getattr
, any exceptions raised
+by traits or the operations they may trigger will result in the
+absence of a trait value in the result dict
.
Get a dict
of all the traits of this class. The dictionary
+is keyed on the name and the values are the TraitType objects.
The TraitTypes returned don’t know anything about the values +that the various HasTrait’s instances are holding.
+The metadata kwargs allow functions to be passed in which +filter traits based on metadata values. The functions should +take a single value as an argument and return a boolean. If +any function returns False, then the trait is not included in +the output. If a metadata key doesn’t exist, None will be passed +to the function.
+Remove a trait change handler.
+This is used to unregister handlers to trait change notifications.
+handler (callable) – The callable called when a trait attribute changes.
names (list, str, All (default: All)) – The names of the traits for which the specified handler should be +uninstalled. If names is All, the specified handler is uninstalled +from the list of notifiers corresponding to all changes.
type (str or All (default: 'change')) – The type of notification to filter by. If All, the specified handler +is uninstalled from the list of notifiers corresponding to all types.
GUI definitions for EEELS_dialog
+probabilities of dielectric function eps relative to zero-loss integral (i0 = 1)
+Gives probabilities of dielectric function eps relative to zero-loss integral (i0 = 1) per eV +Details in R.F.Egerton: EELS in the Electron Microscope, 3rd edition, Springer 2011
+# function drude(ep,ew,eb,epc,e0,beta,nn,tnm) +# Given the plasmon energy (ep), plasmon fwhm (ew) and binding energy(eb), +# this program generates: +# EPS1, EPS2 from modified Eq. (3.40), ELF=Im(-1/EPS) from Eq. (3.42), +# single scattering from Eq. (4.26) and SRFINT from Eq. (4.31) +# The output is e, ssd into the file drude.ssd (for use in Flog etc.) +# and e,eps1 ,eps2 into drude.eps (for use in Kroeger etc.) +# Gives probabilities relative to zero-loss integral (i0 = 1) per eV +# Details in R.F.Egerton: EELS in the Electron Microscope, 3rd edition, Springer 2011 +# Version 10.11.26
+b.7 drude Simulation of a Low-Loss Spectrum +The program DRUDE calculates a single-scattering plasmon-loss spectrum for +a specimen of a given thickness tnm (in nm), recorded with electrons of a +specified incident energy e0 by a spectrometer that accepts scattering up to a +specified collection semi-angle beta. It is based on the extended drude model +(Section 3.3.2), with a volume energy-loss function elf in accord with Eq. (3.64) and +a surface-scattering energy-loss function srelf as in Eq. (4.31). Retardation effects +and coupling between the two surface modes are not included. The surface term can +be made negligible by entering a large specimen thickness (tnm > 1000). +Surface intensity srfint and volume intensity volint are calculated from +Eqs. (4.31) and (4.26), respectively. The total spectral intensity ssd is written to +the file DRUDE.SSD, which can be used as input for KRAKRO. These intensities are +all divided by i0, to give relative probabilities (per eV). The real and imaginary parts +of the dielectric function are written to DRUDE.EPS and can be used for comparison +with the results of Kramers–Kronig analysis (KRAKRO.DAT). +Written output includes the surface-loss probability Ps, obtained by integrating +srfint (a value that relates to two surfaces but includes the negative begrenzungs +term), for comparison with the analytical integration represented by Eq. (3.77). The +volume-loss probability p_v is obtained by integrating volint and is used to calculate +the volume plasmon mean free path (lam = tnm/p_v). The latter is listed and +compared with the MFP obtained from Eq. (3.44), which represents analytical integration +assuming a zero-width plasmon peak. The total probability (Pt = p_v+Ps) is +calculated and used to evaluate the thickness (lam.Pt) that would be given by the formula +t/λ = ln(It/i0), ignoring the surface-loss probability. Note that p_v will exceed +1 for thicker specimens (t/λ > 1), since it represents the probability of plasmon +scattering relative to that of no inelastic scattering. +The command-line usage is drude(ep,ew,eb,epc,beta,e0,tnm,nn), where ep is the +plasmon energy, ew the plasmon width, eb the binding energy of the electrons (0 for +a metal), and nn is the number of channels in the output spectrum. An example of +the output is shown in Fig. b.1a,b.
+Calculates the effective collection angle in mrad:
+Translate from original Fortran program +Calculates the effective collection angle in mrad: +Parameter +——— +energy_scale: numpy array
+++first and last energy loss of spectrum in eV
+
convergence angle in mrad
+collection angle in mrad
+acceleration voltage in V
+eff_beta (float) – effective collection angle in mrad
# function y = effbeta(ene, alpha, beta, beam_kv)
#
# This program computes etha(alpha,beta), that is the collection
# efficiency associated to the following geometry
#
# alpha = half angle of illumination (0 -> pi/2)
# beta = half angle of collection (0 -> pi/2)
# (pi/2 = 1570.795 mrad)
#
# A constant angular distribution of incident electrons is assumed
# for any incident angle (-alpha,alpha). These electrons imping the
# target and a single energy-loss event occurs, with a characteristic
# angle theta-e (relativistic). The angular distribution of the
# electrons after the target is analytically derived.
# This program integrates this distribution from theta=0 up to
# theta=beta with an adjustable angular step.
# This program also computes beta which is the theoretical*
# collection angle which would give the same value of etha(alpha,beta)
# with a parallel incident beam.
#
# subroutines and function subprograms required
# ———————————————
# none
#
# comments
# ——–
#
# The following parameters are asked as input
# accelerating voltage (kV), energy loss range (eV) for the study,
# energy loss step (eV) in this range, alpha (mrad), beta (mrad).
# The program returns for each energy loss step
# alpha (mrad), beta (mrad), theta-e (relativistic) (mrad),
# energy loss (eV), etha (#), beta * (mrad)
#
# author
# ——–
# Pierre TREBBIA
# US 41 (“Microscopie Electronique Analytique Quantitative”)
# Laboratoire de Physique des Solides, Bat. 510
# Universite Paris-Sud, F91405 ORSAY Cedex
# Phone ((33-1) 69 41 53 68)
#
fit peaks to spectrum
+spectrum (numpy array) – spectrum to be fitted
energy_scale (numpy array) – energy scale of spectrum
pin (list of float) – intial guess of peaks position amplitude width
start_fit (int) – channel where fit starts
end_fit (int) – channel where fit starts
only_positive_intensity (boolean) – allows only for positive amplitudes if True; default = False
p – fitting parameters
+eels_tools +Model based quantification of electron energy-loss data +Copyright by Gerd Duscher
+The University of Tennessee, Knoxville +Department of Materials Science & Engineering
+Tian et al.
everything is in SI units, except length is given in nm and angles in mrad.
+See the notebooks for examples of these routines
+All the input and output is done through a dictionary which is to be found in the meta_data +attribute of the sidpy.Dataset
+Functions
++ | + |
+ | add peaks to fitting parameters |
+
+ | + |
+ | + |
+ | + |
+ | core loss model for fitting |
+
+ | dielectric function according to Drude theory |
+
+ | dielectric function according to Drude theory for fitting |
+
+ | dielectric function according to Drude-Lorentz theory |
+
+ | probabilities of dielectric function eps relative to zero-loss integral (i0 = 1) |
+
+ | Calculates the effective collection angle in mrad: |
+
+ | Find all (major and minor) edges within an energy range |
+
+ | + |
+ | find edges within a sidpy.Dataset |
+
+ | Find all major edges within an energy range |
+
+ | find the first most prominent peaks |
+
+ | find peaks in spectrum |
+
+ | + |
+ | + |
+ | fit edges for quantification |
+
+ | fit edges for quantification |
+
+ | model for fitting low-loss spectrum |
+
+ | fit peaks to spectrum |
+
+ | Shift energy scale according to zero-loss peak position |
+
+ | Gaussian Function |
+
+ | get shift of spectrum from zero-loss peak position better to use get resolution_functions |
+
+ | get resolution_function and shift of spectra form zero-loss peak position |
+
+ | get spectra from EELS database chemical formula and edge is accepted. |
+
+ | get deBroglie wavelength of electron accelerated by energy (in eV) e0 |
+
+ | Reads X-ray fluorescent cross-sections from a pickle file. |
+
+ | Returns the atomic number independent of input as a string or number |
+
+ | Using first derivative to determine edge onsets Any peak in first derivative higher than noise_level times standard deviation will be considered |
+
+ | This function calculates the differential scattering probability |
+
+ | This function calculates the differential scattering probability |
+
+ | List all ionization edges of an element with atomic number z |
+
+ | lorentzian function |
+
+ | Updates the edges dictionary with collection angle-integrated X-ray photo-absorption cross-sections |
+
+ | Makes the edges dictionary for quantification |
+
+ | model for fitting low-loss spectrum |
+
+ | part of fit |
+
+ | part of fit |
+
+ | Plot loss function |
+
+ | power law for power_law_background |
+
+ | fit of power law to spectrum |
+
+ | read msa formated file |
+
+ | part of fit |
+
+ | part of fit |
+
+ | part of fit |
+
+ | get resolution function (zero-loss peak shape) from low-loss spectrum |
+
+ | + |
+ | Calculates second derivative of a sidpy.dataset |
+
+ | Set previous quantification from a sidpy.Dataset |
+
+ | shift spectrum in energy |
+
+ | sort fitting parameters by peak position |
+
+ | Calculate momentum-integrated cross-section for EELS from X-ray photo-absorption cross-sections. |
+
+ | zero-loss function |
+
+ | zero-loss peak function |
+
Using first derivative to determine edge onsets +Any peak in first derivative higher than noise_level times standard deviation will be considered
+dataset (sidpy.Dataset) – the spectrum
noise_level (float) – ths number times standard deviation in first derivative decides on whether an edge onset is significant
edge_channel
+This function calculates the differential scattering probability
++++\[\frac{d^2P}{d \Omega d_e}\]+
of the low-loss region for total loss and volume plasmon loss
+total loss probability +p_vol (numpy array 2d): volume loss probability
+P (numpy array 2d)
+This function calculates the differential scattering probability
++++\[\frac{d^2P}{d \Omega d_e}\]+
of the low-loss region for total loss and volume plasmon loss
+total loss probability +p_vol (numpy array 2d): volume loss probability
+return P, P*scale*1e2,p_vol*1e2, p_simple*1e2
+ +P (numpy array 2d)
+Makes the edges dictionary for quantification
+edges_present (list) – list of edges
energy_scale (numpy array) – energy scale on which to make cross-section
e_0 (float) – acceleration voltage (in V)
coll_angle (float) – collection angle in mrad
low_loss (numpy array with same length as energy_scale) – low_less spectrum with which to convolve the cross-section (default=None)
edges – dictionary with all information on cross-section
+Calculate momentum-integrated cross-section for EELS from X-ray photo-absorption cross-sections.
+X-ray photo-absorption cross-sections from NIST. +Momentum-integrated cross-section for EELS according to Egerton Ultramicroscopy 50 (1993) 13-28 equation (4)
+ +Bases: object
Widget to select directories or widgets from a list
+Works in google colab. +The widget converts the name of the nion file to the one in Nion’s swift software, +because it is otherwise incomprehensible
+ + + + + + + + + + +Example
+>>from google.colab import drive +>>drive.mount(“/content/drive”) +>>file_list = pyTEMlib.file_tools.FileWidget() +next code cell: +>>dataset = pyTEMlib.file_tools.open_file(file_list.file_name)
+Methods
+
|
++ |
+ | + |
+ | + |
|
++ |
|
++ |
|
++ |
+ | + |
Read crystal structure from NSID file +Any additional information will be read as dictionary into the info attribute of the ase.Atoms object
+structure_group (h5py.Group) – location in hdf5 file to where the structure information is stored
+atoms – crystal structure in ase format
+ase.Atoms object
+file_tools: All tools to load and save data
+++2018 01 31 Included Nion Swift files to be opened +major revision 2020 09 to include sidpy and pyNSID data formats +2022 change to ase format for structures: this changed the default unit of length to Angstrom!!!
+
Functions
++ | Add dataset to datasets dictionary |
+
+ | + |
+ | Determines file name of hdf5 file for newly converted data file |
+
+ | Returns the path of the file last opened |
+
+ | Returns name of first channel group in hdf5-file |
+
+ | Legacy for get start channel |
+
+ | Write crystal structure to NSID file |
+
+ | add dictionary as structure group |
+
+ | Read crystal structure from NSID file Any additional information will be read as dictionary into the info attribute of the ase.Atoms object |
+
+ | + |
+ | Just a wrapper for the sidpy function print_tree, |
+
+ | Log Results in hdf5-file |
+
+ | Opens a file if the extension is .hf5, .ndata, .dm3 or .dm4 |
+
+ | Opens a File dialog which is used in open_file() function |
+
+ | Open a cif file If no file name is provided an open file dialog to select a cif file appears |
+
+ | Read essential parameter from original_metadata originating from a dm3 file |
+
+ | Updates dataset.metadata['experiment'] with essential information read from original metadata |
+
+ | Read essential parameter from original_metadata originating from a dm3 file |
+
+ | Make a sidpy.Dataset from pyUSID style hdf5 group |
+
+ | Open a POSCAR file from Vasp If no file name is provided an open file dialog to select a POSCAR file appears |
+
+ | Saves a dataset to a file in pyNSID format :param dataset: the data :type dataset: sidpy.Dataset :param filename: name of file to be opened, if filename is None, a QT file dialog will try to open :type filename: str :param h5_group: not used yet :type h5_group: hd5py.Group |
+
+ | + |
+ | Opens a File dialog which is used in open_file() function |
+
+ | Save path of last opened file |
+
+ | + |
+ | Attaches correct dimension from old pyTEMlib style. |
+
+ | + |
Classes
++ | Widget to select dataset object |
+
+ | Widget to select directories or widgets from a list |
+
Log Results in hdf5-file
+Saves either a sidpy.Dataset or dictionary in a hdf5-file. +The group for the result will consist of ‘Log_’ and a running index. +That group will be placed in h5_group.
+h5_group (hd5py.Group, or sidpy.Dataset) – groups where result group are to be stored
dataset (sidpy.Dataset or None) – sidpy dataset to be stored
attributes (dict) – dictionary containing results that are not based on a sidpy.Dataset
log_group – group in hdf5 file with results.
+hd5py.Group
+Opens a file if the extension is .hf5, .ndata, .dm3 or .dm4
+If no filename is provided the QT open_file windows opens (if QT_available==True) +Everything will be stored in a NSID style hf5 file. +Subroutines used:
++++
+- +
NSIDReader
- +
+
+- nsid.write_
- +
+
+- +
get_main_tags
- +
get_additional tags
filename (str) – name of file to be opened, if filename is None, a QT file dialog will try to open
h5_group (hd5py.Group) – not used yet #TODO: provide hook for usage of external chosen group
write_hdf_file (bool) – set to false so that sidpy dataset will not be written to hf5-file automatically
sidpy dataset with location of hdf5 dataset as attribute
+sidpy.Dataset
+Opens a File dialog which is used in open_file() function
+This function uses pyQt5. +The app of the Gui has to be running for QT. Tkinter does not run on Macs at this point in time. +In jupyter notebooks use %gui Qt early in the notebook.
+The file looks first for a path.txt file for the last directory you used.
+file_types (string) – file type filter in the form of ‘*.hf5’
+filename – full filename with absolute path and extension as a string
+string
+Example
+>> import file_tools as ft +>> filename = ft.openfile_dialog() +>> print(filename)
+Open a POSCAR file from Vasp +If no file name is provided an open file dialog to select a POSCAR file appears
+file_name (str) – if None is provided an open file dialog will appear
+crystal – crystal structure in ase format
+ase.Atoms
+Saves a dataset to a file in pyNSID format +:param dataset: the data +:type dataset: sidpy.Dataset +:param filename: name of file to be opened, if filename is None, a QT file dialog will try to open +:type filename: str +:param h5_group: not used yet +:type h5_group: hd5py.Group
+Opens a File dialog which is used in open_file() function
+This function uses pyQt5. +The app of the Gui has to be running for QT. Tkinter does not run on Macs at this point in time. +In jupyter notebooks use %gui Qt early in the notebook.
+The file looks first for a path.txt file for the last directory you used.
+file_types (string) – file type filter in the form of ‘*.hf5’
+filename – full filename with absolute path and extension as a string
+string
+Example
+>> import file_tools as ft +>> filename = ft.openfile_dialog() +>> print(filename)
+breadth first search of atoms viewed as a graph
+the projection dictionary has to contain the following items +‘number_of_nearest_neighbours’, ‘rotated_cell’, ‘near_base’, ‘allowed_variation’
+graph[visited] (numpy array (M,2) with M<N) – positions of atoms hopped in unit cell lattice
ideal (numpy array (M,2)) – ideal atom positions
Function finds the center and the radius of the circumsphere of every simplex. +Reference: +Fiedler, Miroslav. Matrices and graphs in geometry. No. 139. Cambridge University Press, 2011. +(p.29 bottom: example 2.1.11) +Code started from https://github.com/spatala/gbpy +with help of https://codereview.stackexchange.com/questions/77593/calculating-the-volume-of-a-tetrahedron
+vertex_pos (numpy array) – The position of vertices of a tetrahedron
tol (float) – Tolerance defined to identify co-planar tetrahedrons
circum_center (numpy array) – The center of the circumsphere
circum_radius (float) – The radius of the circumsphere
get polyhedra information from an ase.Atoms object
+This is following the method of Banadaki and Patala +http://dx.doi.org/10.1038/s41524-017-0016-0
+pyTEMlib.crystal_tools.electronFF[atoms.symbols[vert]][‘bond_length’][1]
the structural information
+does not exist
+polyhedra – dictionary with all information of polyhedra
+dict
+Get polyhedra, and bonds from and edges and lengths of edges for each polyhedron and store it in info dictionary of new ase.Atoms object
+information on all polyhedra
+Find Voronoi vertices and keep track of associated tetrahedrons and interstitial radii
+Used in find_polyhedra function
+tetrahedra (scipy.spatial.Delaunay object) – Delaunay tesselation
atoms (ase.Atoms object) – the structural information
optimize (boolean) – whether to use different atom radii or not
voronoi_vertices (list) – list of positions of voronoi vertices
voronoi_tetrahedra – list of indices of associated vertices of tetrahedra
r_vv (list of float) – list of all interstitial sizes
Functions
++ | breadth first search of atoms viewed as a graph |
+
+ | Function finds the center and the radius of the circumsphere of every simplex. |
+
+ | Make clusters Breadth first search to go through the list of overlapping spheres or circles to determine clusters |
+
+ | Find overlapping spheres |
+
+ | get polyhedra information from an ase.Atoms object |
+
+ | get all bond radii from Kirkland Parameter: ---------- atoms ase.Atoms object structure information in ase format type: str type of bond 'covalent' or 'metallic' |
+
+ | Get polyhedra, and bonds from and edges and lengths of edges for each polyhedron and store it in info dictionary of new ase.Atoms object |
+
+ | + |
+ | Calculates distortion matrix |
+
+ | + |
+ | + |
+ | + |
+ | Calculate average for all points that are closer than distance apart, otherwise leave the points alone |
+
+ | Find Voronoi vertices and keep track of associated tetrahedrons and interstitial radii |
+
+ | Function finds center and radius of the largest interstitial sphere of a simplex. |
+
+ | make polygons from convex hulls of vertices around interstitial positions |
+
+ | collect output data and make dictionary |
+
+ | Plot structure in a ase.Atoms object with plotly |
+
+ | + |
+ | set certain or all bond-radii taken from Kirkland |
+
+ | + |
+ | find transformation matrix A between a polygon and a perfect one |
+
+ | Undistort image according to distortion matrix |
+
+ | use simple ITK to undistort image |
+
+ | Undistort stack with distortion matrix |
+
+ | use simple ITK to undistort stack of image input: image: numpy array with size NxM distortion_matrix: h5 Dataset or numpy array with size 2 x P x Q with P, Q >= M, N output: image M, N |
+
+ | Volumes of voronoi cells from https://stackoverflow.com/questions/19634993/volume-of-voronoi-cell-python |
+
Function finds center and radius of the largest interstitial sphere of a simplex. +Which is the center of the cirumsphere if all atoms have the same radius, +but differs for differently sized atoms. +In the last case, the circumsphere center is used as starting point for refinement.
+vertex_pos (numpy array) – The position of vertices of a tetrahedron
atom_radii (float) – bond radii of atoms
optimize (boolean) – whether atom bond lengths are optimized or not
new_center (numpy array) – The center of the largest interstitial sphere
radius (float) – The radius of the largest interstitial sphere
Plot structure in a ase.Atoms object with plotly
+If the info dictionary of the atoms object contains bond or polyedra information, these can be set tobe plotted
+structure of supercell
+indices of polyhedra to be plotted
+whether to plot bonds or not
+handle to figure needed to modify appearance
+set certain or all bond-radii taken from Kirkland
+Bond_radii are also stored in atoms.info
+structure information in ase format
+type of bond ‘covalent’ or ‘metallic’
+list of atomic bond-radii
+find transformation matrix A between a polygon and a perfect one
+returns: +list of points: all points on a grid within original polygon +list of points: coordinates of these points where pixel have to move to +2x2 matrix aa: transformation matrix
+Undistort image according to distortion matrix
+Uses the griddata interpolation of scipy to apply distortion matrix to image. +The distortion matrix contains in origin and target pixel coordinates +target is where the pixel has to be moved (floats)
+distortion_matrix (numpy array (Nx2)) – distortion matrix (format N x 2)
image_data (numpy array or sidpy.Dataset) – image
interpolated – undistorted image
+numpy array
+use simple ITK to undistort image
+image_data (numpy array with size NxM) –
distortion_matrix (sidpy.Dataset or numpy array with size 2 x P x Q) –
P (with) –
M (Q >=) –
N –
image
+numpy array MXN
+Undistort stack with distortion matrix
+Use the griddata interpolation of scipy to apply distortion matrix to image +The distortion matrix contains in each pixel where the pixel has to be moved (floats)
+distortion_matrix (numpy array) – distortion matrix to undistort image (format image.shape[0], image.shape[2], 2)
data (numpy array or sidpy.Dataset) – image
Volumes of voronoi cells from +https://stackoverflow.com/questions/19634993/volume-of-voronoi-cell-python
+get indices of polyhedra at boundary (assumed to be parallel to x-axis)
+dictionary of all polyhedra
+position of boundary in Angstrom
+width of boundary where center of polyhedra are considered in Angstrom
+optional
+upper and lower limit of polyhedra to plot
+boundary_polyhedra – list of polyhedra at boundary
+list
+part of pyTEMlib +a pycrosccopy package
+Author: Gerd Duscher +First Version: 2022-01-08
+Functions
++ | get indices of polyhedra at boundary (assumed to be parallel to x-axis) |
+
+ | Information to plot bonds with plotly |
+
+ | Information to plot polyhedra with plotly |
+
+ | make a super_cell to plot with extra atoms at periodic boundaries |
+
+ | plot supercell with plotly |
+
+ | plot atoms and bonds with plotly |
+
+ | plot atoms and polyhedra with plotly |
+
+ | plot atoms and polyhedra with plotly |
+
+ | plot polyhedra and atoms of vertices with plotly |
+
Information to plot polyhedra with plotly
+dictionary of all polyhedra
+list or index of polyhedron to plot.
+whether to center polyhedra on origin
+data – instructions to plot for plotly
+dict
+plot supercell with plotly
+optional structure info to plot atoms (with correct color)
+amount of shift in x direction of supercell
+title of plot
+fig – plotly figure instance
+plotly.figure
+plot atoms and bonds with plotly
+dictionary of all polyhedra
+optional structure info to plot atoms (with correct color)
+list of volumes, optional structure
+sie of atoms to plot
+title of plot
+fig – plotly figure instance
+plotly.figure
+plot atoms and polyhedra with plotly
+dictionary of all polyhedra
+list of indices of polyhedra to plot
+optional structure info to plot atoms (with correct color)
+list of volumes, optional structure
+title of plot
+fig – plotly figure instance
+plotly.figure
+plot atoms and polyhedra with plotly
+dictionary of all polyhedra
+list or index of polyhedron to plot.
+optional structure info to plot atoms (with correct color)
+fig – plotly figure instance
+plotly.figure
+plot polyhedra and atoms of vertices with plotly
+dictionary of all polyhedra
+list of indices of polyhedra to plot
+optional structure info to plot atoms (with correct color)
+list of volumes, optional structure
+title of plot
+fig – plotly figure instance
+plotly.figure
+Created on Sat Jan 19 10:07:35 2019
+@author: gduscher
++ | Figures and Animations for TEM in jupyter notebooks part of MSE 672 course at UTK |
+
+ | Atom detection |
+
+ | config_dir: setup of directory ~/.pyTEMlib for custom sources and database |
+
+ | crystal_tools |
+
+ | + |
+ | Dynamic Scattering Library for Multi-Slice Calculations |
+
+ | eds_tools Model based quantification of energy-dispersive X-ray spectroscopy data Copyright by Gerd Duscher |
+
+ | QT dialog window for EELS compositional analysis |
+
+ | Interactive routines for EELS analysis |
+
+ | GUI definitions for EEELS_dialog |
+
+ | eels_tools Model based quantification of electron energy-loss data Copyright by Gerd Duscher |
+
+ | file_tools: All tools to load and save data |
+
+ | + |
+ | + |
+ | # plotting functions for graph_tools |
+
+ | Input Dialog for Image Analysis |
+
+ | Gui for image_dialog |
+
+ | image_tools.py by Gerd Duscher, UTK part of pyTEMlib MIT license except where stated differently |
+
+ | Input Dialog for EELS Analysis |
+
+ | Gui for info_dialog |
+
+ | + |
+ | Interactive routines for EELS analysis |
+
+ | + |
+ | kinematic_scattering Copyright by Gerd Duscher |
+
+ | default microscope parameters from config file |
+
+ | EELS Input Dialog for ELNES Analysis |
+
+ | GUI definitions for peak_fit_dialog |
+
+ | Functions to calculate electron probe |
+
+ | utility functions for sidpy; will move to sidpy |
+
+ | dft simulations tools |
+
+ | version |
+
+ | plotting of sidpy Datasets with bokeh for google colab |
+
+ | X-ray photo-absorption cross-sections for inelastic scattering from NIST The cross sections are given in atoms/nm^3 not barns!! See xsec_xrpa function in eels_tools for usage. |
+
Input Dialog for Image Analysis
+Author: Gerd Duscher
+Gui for image_dialog
+Author: Gerd Duscher
+Use spots in diffractogram for a Fourier Filter
+image to be filtered
+sorted spots in diffractogram in 1/nm
+low pass filter in center of diffractogram in 1/nm
+radius of masked reflections in 1/nm
+++Fourier filtered image
+
Rigid and then non-rigid (demon) registration
+Performs rigid and then non-rigid registration, please see individual functions: +- rigid_registration +- demon_registration
+main_dataset (sidpy.Dataset) – dataset of data_type ‘IMAGE_STACK’ to be registered
storage_channel (h5py.Group) – optional - location in hdf5 file to store datasets
non_rigid_registered (sidpy.Dataset)
rigid_registered_dataset (sidpy.Dataset)
# This task generates a restored image from an input image and point spread function (PSF) using +# the algorithm developed independently by Lucy (1974, Astron. J. 79, 745) and Richardson +# (1972, J. Opt. Soc. Am. 62, 55) and adapted for HST imagery by Snyder +# (1990, in Restoration of HST Images and Spectra, ST ScI Workshop Proceedings; see also +# Snyder, Hammoud, & White, JOSA, v. 10, no. 5, May 1993, in press). +# Additional options developed by Rick White (STScI) are also included. +# +# The Lucy-Richardson method can be derived from the maximum likelihood expression for data +# with a Poisson noise distribution. Thus, it naturally applies to optical imaging data such as HST. +# The method forces the restored image to be positive, in accord with photon-counting statistics. +# +# The Lucy-Richardson algorithm generates a restored image through an iterative method. The essence +# of the iteration is as follows: the (n+1)th estimate of the restored image is given by the nth estimate +# of the restored image multiplied by a correction image. That is, +# +# original data +# image = image ————— * reflect(PSF) +# n+1 n image * PSF +# n
+# where the *’s represent convolution operators and reflect(PSF) is the reflection of the PSF, i.e. +# reflect((PSF)(x,y)) = PSF(-x,-y). When the convolutions are carried out using fast Fourier transforms +# (FFTs), one can use the fact that FFT(reflect(PSF)) = conj(FFT(PSF)), where conj is the complex conjugate +# operator.
+Diffeomorphic Demon Non-Rigid Registration
+simpleITK and numpy
+Please Cite: http://www.simpleitk.org/SimpleITK/project/parti.html +and T. Vercauteren, X. Pennec, A. Perchant and N. Ayache +Diffeomorphic Demons Using ITK’s Finite Difference Solver Hierarchy +The Insight Journal, http://hdl.handle.net/1926/510 2007
+dataset (sidpy.Dataset) – stack of image after rigid registration and cropping
verbose (boolean) – optional for increased output
dem_reg
+stack of images with non-rigid registration
+Example
+dem_reg = demon_reg(stack_dataset, verbose=False)
+Find spots in diffractogram and sort them by distance from center
+Uses blob_log from scipy.spatial
+dset (sidpy.Dataset) – diffractogram
spot_threshold (float) – threshold for blob finder
spots – sorted position (x,y) and radius (r) of all spots
+numpy array
+Reads information into dictionary ‘tags’, performs ‘FFT’, and provides a smoothed FT and reciprocal +and intensity limits for visualization.
+dset (sidpy.Dataset) – image
+fft_dset – Fourier transform with correct dimensions
+sidpy.Dataset
+Example
+>>> fft_dataset = fourier_transform(sidpy_dataset)
+>>> fft_dataset.plot()
+
Get rotation by comparing spots in diffractogram to diffraction Bragg spots
+positions (in 1/nm) of spots in diffractogram
+positions (in 1/nm) of Bragg spots according to kinematic scattering theory
+image_tools.py +by Gerd Duscher, UTK +part of pyTEMlib +MIT license except where stated differently
+Functions
++ | Use spots in diffractogram for a Fourier Filter |
+
+ | Depreciated - use diffraction spots |
+
+ | Calculate Contrast Transfer Function |
+
+ | Calculate the Scherzer defocus. |
+
+ | depreciated get change of scale from comparison of spots to Bragg angles |
+
+ | Cartesian to polar coordinate conversion |
+
+ | Transform cartesian grid to polar grid |
+
+ | De-noising of image by using first component of single value decomposition |
+
+ | Rigid and then non-rigid (demon) registration |
+
+ | Crop images in stack according to drift |
+
+ | # This task generates a restored image from an input image and point spread function (PSF) using # the algorithm developed independently by Lucy (1974, Astron. |
+
+ | Diffeomorphic Demon Non-Rigid Registration |
+
+ | Find spots in diffractogram and sort them by distance from center |
+
+ | Reads information into dictionary 'tags', performs 'FFT', and provides a smoothed FT and reciprocal and intensity limits for visualization. |
+
+ | Get rotation by comparing spots in diffractogram to diffraction Bragg spots |
+
+ | Calculates the relativistic corrected de Broglie wave length of an electron |
+
+ | interactive histogram |
+
+ | Polar to Cartesian coordinate conversion |
+
+ | Calculate power spectrum |
+
+ | rebin an image by the number of pixels in x and y direction given by binning |
+
+ | Shifting images on top of each other |
+
+ | Rigid registration of image stack with pixel accuracy |
+
+ | Test rotational symmetry of diffraction spots |
+
+ | Takes a centered diffraction pattern (as a sidpy dataset)and warps it to a polar grid |
+
+ | Conversion from carthesian to polar coordinates |
+
Classes
++ | Image with line profile |
+
Shifting images on top of each other
+Uses relative drift to shift images on top of each other, +with center image as reference. +Shifting is done with shift routine of ndimage from scipy. +This function is used by rigid_registration routine
+dset (sidpy.Dataset) – dataset with image_stack
rel_drift – relative_drift from image to image as list of [shiftx, shifty]
stack (numpy array)
drift (list of drift in pixel)
Rigid registration of image stack with pixel accuracy
+Uses simple cross_correlation +(we determine drift from one image to next)
+dataset (sidpy.Dataset) – sidpy dataset with image_stack dataset
+rigid_registered – Registered Stack and drift (with respect to center image)
+sidpy.Dataset
+Conversion from carthesian to polar coordinates
+the angles and distances are sorted by r and then phi +The indices of this sort is also returned
+points (numpy array) – number of points in axis 0 first two elements in axis 1 are x and y
rounding (int) – optional rounding in significant digits
r, phi, sorted_indices
+Input Dialog for EELS Analysis
+Author: Gerd Duscher
+Functions
++ | + |
Classes
++ | + |
Gui for info_dialog
+Author: Gerd Duscher
+Bases: EELSWidget
Methods
+
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
Bases: EELSWidget
Methods
+
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
Functions
++ | + |
+ | + |
Classes
++ | + |
+ | + |
+ | + |
Interactive routines for EELS analysis
+this file provides additional dialogs for EELS quantification
+Author: Gerd Duscher
+Input for Figure 3.18 in Zuo and Spence “Advanced TEM”, 2017
+This input acts as an example as well as a reference
+optional to see output
++++
+- atoms: ase.Atoms
- +
Silicon crystal structure
+e +dictionary: tags is the dictionary of all input and output parameter needed to reproduce that figure.
+
Atomic form factor parametrized in 1/Angstrom but converted to 1/Angstrom
+The atomic form factor is from Kirkland: Advanced Computing in Electron Microscopy 2nd edition, Appendix C. +From Appendix C of Kirkland, “Advanced Computing in Electron Microscopy”, 3Ard ed. +Calculation of electron form factor for specific q: +Using equation Kirkland C.15
+ +Calculates the relativistic corrected de Broglie wavelength of an electron in Angstrom
+acceleration voltage in volt
+wave length in Angstrom
+kinematic_scattering +Copyright by Gerd Duscher
+The University of Tennessee, Knoxville +Department of Materials Science & Engineering
+Scattering Theory: +Zuo and Spence, “Advanced TEM”, 2017
+Spence and Zuo, Electron Microdiffraction, Plenum 1992
+Kirkland: Advanced Computing in Electron Microscopy 2nd edition +Appendix C
+everything is in SI units, except length which is given in Angstrom.
+See the notebooks for examples of these routines
+All the input and output is done through a ase.Atoms object and the dictionary in the info attribute
+Functions
++ | Input for Figure 3.18 in Zuo and Spence "Advanced TEM", 2017 |
+
+ | Check sanity of input parameters |
+
+ | same as Zuo_fig_3_18 |
+
+ | Atomic form factor parametrized in 1/Angstrom but converted to 1/Angstrom |
+
+ | Microscope stage coordinates of zone |
+
+ | Test all zone axis up to a maximum of hkl_max |
+
+ | + |
+ | The metric tensor of the lattice. |
+
+ | zone axis in global coordinate system |
+
+ | Calculates the relativistic corrected de Broglie wavelength of an electron in Angstrom |
+
+ | All kinematic scattering calculation |
+
+ | All kinematic scattering calculation |
+
+ | Make pretty labels |
+
+ | + |
+ | Calculate the ring diffraction pattern of a crystal structure |
+
+ | Scattering matrix |
+
+ | Microscope stage coordinate system |
+
+ | Length of vector |
+
+ | Rotation of zone axis by mistilt |
+
All kinematic scattering calculation
+Calculates Bragg spots, Kikuchi lines, excess, and deficient HOLZ lines
+atoms (ase.Atoms) – object with crystal structure: +and with experimental parameters in info attribute: +‘acceleration_voltage_V’, ‘zone_hkl’, ‘Sg_max’, ‘hkl_max’ +Optional parameters are: +‘mistilt’, convergence_angle_mrad’, and ‘crystal_name’ +verbose = True will give extended output of the calculation
verbose (boolean) – default is False
There are three sub_dictionaries in info attribute: +[‘allowed’], [‘forbidden’], and [‘HOLZ’] +[‘allowed’] and [‘forbidden’] dictionaries contain:
+++[‘Sg’], [‘hkl’], [‘g’], [‘structure factor’], [‘intensities’], +[‘ZOLZ’], [‘FOLZ’], [‘SOLZ’], [‘HOLZ’], [‘HHOLZ’], [‘label’], and [‘Laue_zone’]
+
[‘slope’], [‘distance’], [‘theta’], [‘g_deficient’], [‘g_excess’], [‘hkl’], [‘intensities’], +[‘ZOLZ’], [‘FOLZ’], [‘SOLZ’], [‘HOLZ’], and [‘HHOLZ’]
+Please note that the Kikuchi lines are the HOLZ lines of ZOLZ
+[‘wave_length_nm’], [‘reciprocal_unit_cell’], [‘inner_potential_V’], [‘incident_wave_vector’], +[‘volume’], [‘theta’], [‘phi’], and [‘incident_wave_vector_vacuum’]
+atoms
+All kinematic scattering calculation
+Calculates Bragg spots, Kikuchi lines, excess, and deficient HOLZ lines
+atoms (ase.Atoms) – object with crystal structure: +and with experimental parameters in info attribute: +‘acceleration_voltage_V’, ‘zone_hkl’, ‘Sg_max’, ‘hkl_max’ +Optional parameters are: +‘mistilt’, convergence_angle_mrad’, and ‘crystal_name’ +verbose = True will give extended output of the calculation
verbose (boolean) – default is False
There are three sub_dictionaries in info attribute: +[‘allowed’], [‘forbidden’], and [‘HOLZ’] +[‘allowed’] and [‘forbidden’] dictionaries contain:
+++[‘Sg’], [‘hkl’], [‘g’], [‘structure factor’], [‘intensities’], +[‘ZOLZ’], [‘FOLZ’], [‘SOLZ’], [‘HOLZ’], [‘HHOLZ’], [‘label’], and [‘Laue_zone’]
+
[‘slope’], [‘distance’], [‘theta’], [‘g_deficient’], [‘g_excess’], [‘hkl’], [‘intensities’], +[‘ZOLZ’], [‘FOLZ’], [‘SOLZ’], [‘HOLZ’], and [‘HHOLZ’]
+Please note that the Kikuchi lines are the HOLZ lines of ZOLZ
+[‘wave_length_nm’], [‘reciprocal_unit_cell’], [‘inner_potential_V’], [‘incident_wave_vector’], +[‘volume’], [‘theta’], [‘phi’], and [‘incident_wave_vector_vacuum’]
+ato,s
+Make pretty labels
+hkls (np.ndarray) – a numpy array with all the Miller indices to be labeled
hex_label (boolean - optional) – if True this will make for Miller indices.
hkl_label – list of labels in Latex format
+Calculate the ring diffraction pattern of a crystal structure
+atoms (Crystal) – crystal structure
verbose (verbose print-outs) – set to False
tags – dictionary with diffraction information added
+Bases: object
Class to read configuration file and provide microscope information
+Methods
+
|
++ |
|
++ |
|
++ |
Attributes
+
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
default microscope parameters from config file
+Read microscope CSV file
+for pyTEMLib by Gerd
+copyright 2012, Gerd Duscher +updated 2021
+Classes
++ | Class to read configuration file and provide microscope information |
+
Bases: object
Methods
+
|
++ |
|
++ |
+ | Fit spectrum with peaks given in peaks dictionary |
+
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
|
++ |
+ | Fit lots of Gaussian to spectrum and let the program sort it out |
+
|
++ |
EELS Input Dialog for ELNES Analysis
+Functions
++ | + |
+ | Gaussian mixture model (non-Bayesian) |
+
Classes
++ | + |
GUI definitions for peak_fit_dialog
+Functions to calculate electron probe
+Functions
++ | Get aberration function chi without defocus spread |
+
+ | + |
+ | + |
+ | + |
+ | + |
+ | Get Ronchigram |
+
+ | + |
+ | + |
+ | + |
+ | + |
+ | # ## # Aberration function chi without defocus # ## |
+
+ | Make a Gaussian shaped probe |
+
+ | Make a Lorentzian shaped probe |
+
+ | + |
+ |
|
+
+ | + |
This function creates an incident STEM probe
at position (0,0)
with parameters given in ab dictionary
The following Aberration functions are being used:
ddf = Cc*de/E but not + Cc2*(de/E)^2,
Cc, Cc2 = chrom. Aber. (1st, 2nd order) [1]
chi(qx,qy) = (2*pi/lambda)*{0.5*C1*(qx^2+qy^2)+
0.5*C12a*(qx^2-qy^2)+
C12b*qx*qy+
C21a/3*qx*(qx^2+qy^2)+
…
+0.5*C3*(qx^2+qy^2)^2
+0.125*C5*(qx^2+qy^2)^3
… (need to finish)
qx = acos(k_x/K), qy = acos(k_y/K)
References:
[1] J. Zach, M. Haider,
“Correction of spherical and Chromatic Aberration
in a low Voltage SEM”, Optik 98 (3), 112-118 (1995)
[2] O.L. Krivanek, N. Delby, A.R. Lupini,
“Towards sub-Angstrom Electron Beams”,
Ultramicroscopy 78, 1-11 (1999)
# Internally reciprocal lattice vectors in 1/nm or rad. +# All calculations of chi in angles. +# All aberration coefficients in nm
+utility functions for sidpy; will move to sidpy
+Functions
++ | get dimension |
+
+ | get dimension by dimension_type name |
+
+ | get extent to plot with matplotlib |
+
+ | Get all spatial dimensions |
+
+ | Make a dummy sidpy.Dataset |
+
+ | + |
Classes
++ | Widget to select dataset object |
+
dft simulations tools
+Part of pyTEMlib +by Gerd Duscher +created 10/29/2020
+Supports the conversion of DFT data to simulated EELS spectra
+exciting_get_spectra: importing dielectric function from the exciting program
final_state_broadening: apply final state broadening to loss-spectra
Functions
++ | get EELS spectra from exciting calculation |
+
+ | Final state smearing of ELNES edges |
+
version
+plotting of sidpy Datasets with bokeh for google colab
+Functions
++ | + |
+ | plot according to data_type |
+
+ | Plotting an image |
+
+ | Plot spectrum |
+
+ | Plotting a stack of images |
+
+ | + |
+ | + |
Classes
++ | Plots a sidpy.Dataset with spectral dimension |
+
+ | + |
Plotting an image
+Plotting an image contained in a sidpy.Dataset.
+dataset (sidpy.Dataset) – sidpy dataset with data_type ‘IMAGE_STACK’
palette (bokeh palette) – palette is optional
p
+bokeh plot
+Example
+>> import pyTEMlib +>> from bokeh.plotting import figure, show, output_notebook +>> output_notebook() +>> p = pyTEMlib.viz(dataset) +>> p.show(p)
+Plotting a stack of images
+Plotting a stack of images contained in a sidpy.Dataset. +The images can be scrolled through with a slider widget.
+dataset (sidpy.Dataset) – sidpy dataset with data_type ‘IMAGE_STACK’
palette (bokeh palette) – palette is optional
p
+bokeh plot
+Example
+>> import pyTEMlib +>> from bokeh.plotting import figure, show, output_notebook +>> output_notebook() +>> p = pyTEMlib.viz(dataset) +>> p.show(p)
+X-ray photo-absorption cross-sections for inelastic scattering from NIST +The cross sections are given in atoms/nm^3 not barns!! +See xsec_xrpa function in eels_tools for usage.
+for pyTEMLib by Gerd
+copyright 2022, Gerd Duscher
+extended to larger energy scales 01/20/2023
+
+"""Figures and Animations for TEM in jupyter notebooks
+part of MSE 672 course at UTK
+
+Author: Gerd Duscher
+revision: 01/11/2021
+03/17/2021 added Aberration Animation
+ """
+
+import numpy as np
+import matplotlib.pyplot as plt
+import matplotlib.patches as patches
+
+from ipywidgets import widgets
+from IPython.display import display
+
+import pyTEMlib.kinematic_scattering as ks
+
+
+[docs]def geometric_ray_diagram(focal_length=1., magnification=False):
+ """ Sketch of geometric ray diagram od one lens
+
+ Parameters
+ ----------
+ focal_length: float
+ focal length of lens
+ magnification: boolean
+ draw magnification on the side
+
+ Returns
+ -------
+ matplotlib figure
+ """
+
+ f = focal_length
+
+ u = 1.5
+ v = 1 / (1 / f - 1 / u)
+ m = v / u
+ if magnification:
+ line_strong = .5
+ else:
+ line_strong = 2
+
+ x = 0.4
+
+ fig, ax = plt.subplots()
+
+ # add an ellipse
+ ellipse = patches.Ellipse((0.0, 0.0), 3.4, 0.3, alpha=0.3, color='blue')
+ ax.add_patch(ellipse)
+ ax.plot([1.5, -1.5], [0, 0], '--', color='black')
+ ax.plot([0, 0], [u, -v], '--', color='black')
+ single_prop = dict(arrowstyle="->", shrinkA=0, shrinkB=0)
+ double_prop = dict(arrowstyle="<->", shrinkA=0, shrinkB=0)
+
+ if magnification:
+ ax.annotate("", xy=(-x, u), xytext=(x, u), arrowprops=single_prop)
+ ax.annotate("", xy=(x * m, -v), xytext=(-x * m, -v), arrowprops=single_prop)
+
+ else:
+ ax.annotate("", xy=(-x, u), xytext=(0, u), arrowprops=single_prop)
+ ax.annotate("", xy=(x * m, -v), xytext=(0, -v), arrowprops=single_prop)
+
+ ax.text(x + 0.1, u, 'object plane', va='center')
+ ax.plot([1, -1], [-f, -f], '--', color='black')
+ ax.text(1.1, -f, 'back focal\n plane', va='center')
+ ax.text(x * m + 0.1, -v, 'image plane', va='center')
+
+ ax.annotate("", xy=(-.9, 0), xytext=(-.9, -f), arrowprops=double_prop)
+ ax.text(-1, -f / 2, 'f')
+ if magnification:
+ ax.annotate("", xy=(-1.8, 0), xytext=(-1.8, -v), arrowprops=double_prop)
+ ax.text(-1.7, -v / 2, 'v')
+ ax.annotate("", xy=(-1.8, 0), xytext=(-1.8, u), arrowprops=double_prop)
+ ax.text(-1.7, u / 2, 'u')
+
+ ax.plot([-x, x * m], [u, -v], color='black', linewidth=line_strong)
+ ax.plot([-x, -x], [u, 0], color='black', linewidth=line_strong)
+ ax.plot([-x, x * m], [0, -v], color='black', linewidth=line_strong)
+
+ ax.plot([-x, -2 * x], [u, 0], color='black', linewidth=0.5)
+ ax.plot([-2 * x, x * m], [0, -v], color='black', linewidth=0.5)
+ if magnification:
+ ax.plot([x, -x * m], [u, -v], color='black', linewidth=0.5)
+ ax.plot([x, x], [u, 0], color='black', linewidth=0.5)
+ ax.plot([x, -x * m], [0, -v], color='black', linewidth=0.5)
+
+ ax.plot([x, 2 * x], [u, 0], color='black', linewidth=0.5)
+ ax.plot([2 * x, -x * m], [0, -v], color='black', linewidth=0.5)
+ else:
+ ax.plot([-x, x * m], [u, 0], color='black', linewidth=0.5)
+ ax.plot([x * m, x * m], [0, -v], color='black', linewidth=0.5)
+
+ ax.set_xlim(-2, 3)
+ ax.set_ylim(-3.5, 2)
+ ax.set_aspect('equal')
+
+
+# ----------------------------------------------------------------
+# Modified from Michael Fairchild :simply draws a thin-lens at the provided location parameters:
+# - z: location along the optical axis (in mm)
+# - f: focal length (in mm, can be negative if div. lens)
+# - diam: lens diameter in mm
+# - lens_labels: label to identify the lens on the drawing
+# ----------------------------------------------------------------
+[docs]def add_lens(z, f, diam, lens_labels):
+ """add lens to propagate beam plot"""
+ ww, tw, rad = diam / 10.0, diam / 3.0, diam / 2.0
+ plt.plot([z, z], [-rad, rad], 'k', linewidth=2)
+ plt.plot([z, z + tw], [-rad, -rad + np.sign(f) * ww], 'k', linewidth=2)
+ plt.plot([z, z - tw], [-rad, -rad + np.sign(f) * ww], 'k', linewidth=2)
+ plt.plot([z, z + tw], [rad, rad - np.sign(f) * ww], 'k', linewidth=2)
+ plt.plot([z, z - tw], [rad, rad - np.sign(f) * ww], 'k', linewidth=2)
+ plt.plot([z + f, z + f], [-ww, ww], 'k', linewidth=2)
+ plt.plot([z - f, z - f], [-ww, ww], 'k', linewidth=2)
+ plt.text(z, rad + 5.0, lens_labels, fontsize=12)
+ plt.text(z, rad + 2.0, 'f=' + str(int(f)), fontsize=10)
+
+
+[docs]def add_aperture(z, diam, radius, lens_labels):
+ """add aperture to propagate beam plot"""
+
+ ww, tw, rad = diam / 10.0, diam / 3.0, diam / 2.0
+ radius = radius / 2
+ plt.plot([z, z], [-rad, -radius], 'k', linewidth=2)
+ plt.plot([z, z], [rad, radius], 'k', linewidth=2)
+ plt.text(z, -rad - 2.0, lens_labels, fontsize=12)
+
+
+[docs]def propagate_beam(source_position, numerical_aperture, number_of_rays, lens_positions, focal_lengths,
+ lens_labels='', color='b'):
+ """geometrical propagation of light rays from given source
+
+ Parameters
+ ----------
+ source_position: list
+ location of the source (z0, x0) along and off axis (in mm)
+ numerical_aperture: float
+ numerical aperture of the beam (in degrees)
+ number_of_rays: int
+ number of rays to trace
+ lens_positions: numpy array
+ array with the location of the lenses
+ focal_lengths: numpy array
+ array with the focal length of lenses
+ lens_labels: list of string
+ label for the nature of lenses
+ color: str
+ color of the rays on plot
+ """
+
+ plt.figure()
+ z_max = 1600.
+
+ # aperture (maximum angle) in radians
+ apa = numerical_aperture * np.pi / 180.0
+
+ for i in range(np.size(lens_positions)):
+ add_lens(lens_positions[i], focal_lengths[i], 25, lens_labels[i])
+
+ add_aperture(840, 25, 7, 'CA')
+
+ # position of source is z0,x0
+ z0 = source_position[0]
+ if np.size(source_position) == 2:
+ x0 = source_position[1]
+ else:
+ x0 = 0.0
+
+ # list of lens positions
+ zl1, ff1 = lens_positions[(z0 < lens_positions)], focal_lengths[(z0 < lens_positions)]
+ nl = np.size(zl1) # number of lenses
+
+ zz, xx, tani = np.zeros(nl + 2), np.zeros(nl + 2), np.zeros(nl + 2)
+ tan0 = np.tan(apa / 2.0) - np.tan(apa) * np.arange(number_of_rays) / (number_of_rays - 1)
+
+ for i in range(number_of_rays):
+ tani[0] = tan0[i] # initial incidence angle
+ zz[0], xx[0] = z0, x0
+ for j in range(nl):
+ zz[j + 1] = zl1[j]
+ xx[j + 1] = xx[j] + (zz[j + 1] - zz[j]) * tani[j]
+ tani[j + 1] = tani[j] - xx[j + 1] / ff1[j]
+
+ zz[nl + 1] = z_max
+ xx[nl + 1] = xx[nl] + (zz[nl + 1] - zz[nl]) * tani[nl]
+ plt.plot(zz, xx, color)
+ plt.axis([-20, z_max, -20, 20])
+
+
+[docs]def deficient_holz_line(exact_bragg=False, shift=False, laue_zone=1, color='black'):
+ """
+ Ewald sphere construction to explain Laue Circle and deficient HOLZ lines
+
+ Parameters:
+ exact_bragg: boolean
+ whether to tilt into exact Bragg condition or along zone axis
+ shift: boolean
+ whether to shift exact Bragg-condition onto zone axis origin
+ laue_zone: int
+ first or second Laue zone only
+ color: string
+ color of wave vectors and Ewald sphere
+ """
+
+ k_0 = [0, 1 / ks.get_wavelength(600)]
+
+ d = 5. # lattice parameter in nm
+
+ if laue_zone == 0:
+ s_g = 1 / d + 0.06
+ else:
+ s_g = .1
+
+ g = np.linspace(-5, 6, 12) * 1 / d
+ g_d = np.array([5. / d + laue_zone * 1 / d / 2, laue_zone * 1 / d])
+ g_sg = g_d.copy()
+ g_sg[1] = g_d[1] + s_g # point on Ewald sphere
+
+ # reciprocal lattice
+ plt.scatter(g[:-1], [0] * 11, color='red')
+ plt.scatter(g - 1 / d / 2, [1 / d] * 12, color='blue')
+
+ shift_x = shift_y = 0.
+ d_theta = d_theta1 = d_theta2 = 0
+
+ if exact_bragg:
+
+ d_theta1 = np.arctan((1 / d * laue_zone + s_g) / g_d[0])
+ d_theta2 = np.arctan((1 / d * laue_zone) / g_d[0])
+ d_theta = -(d_theta1 - d_theta2)
+ s_g = 0
+ s = np.sin(d_theta)
+ c = np.cos(d_theta)
+ k_0 = [-s * k_0[1], c * k_0[1]]
+ if shift:
+ shift_x = -k_0[0]
+ shift_y = np.linalg.norm(k_0) - k_0[1]
+ d_theta = np.degrees(d_theta)
+
+ k_0[0] += shift_x
+ k_0[1] += shift_y
+
+ # Ewald Sphere
+ ewald_sphere = patches.Circle((k_0[0], k_0[1]), radius=np.linalg.norm(k_0), clip_on=False, zorder=10, linewidth=1,
+ edgecolor=color, fill=False)
+ plt.gca().add_artist(ewald_sphere)
+
+ plt.gca().arrow(g[-1] + .1 / d / 4, 1 / d / 2, 0, 1 / d / 2, head_width=0.03, head_length=0.04, fc='k', ec='k',
+ length_includes_head=True)
+ plt.gca().arrow(g[-1] + .1 / d / 4, 1 / d / 2, 0, -1 / d / 2, head_width=0.03, head_length=0.04, fc='k', ec='k',
+ length_includes_head=True)
+ plt.gca().annotate("$|g_{HOLZ}|$", xytext=(g[-1] + .1 / d / 3, 1 / d / 3), xy=(g[-1] + 1 / d / 3, 1 / d / 3))
+
+ # k_0
+ plt.scatter(k_0[0], k_0[1])
+ plt.gca().arrow(k_0[0], k_0[1], -k_0[0] + shift_x, -k_0[1] + shift_y, head_width=0.03, head_length=0.04, fc=color,
+ ec=color, length_includes_head=True)
+ plt.gca().annotate("K$_0$", xytext=(k_0[0] / 2, k_0[1] / 3), xy=(k_0[0] / 2, k_0[1] / 2))
+
+ # K_d Bragg of HOLZ reflection
+ plt.gca().arrow(k_0[0], k_0[1], -k_0[0] + g_d[0] + shift_x, -k_0[1] + g_d[1] + s_g + shift_y, head_width=0.03,
+ head_length=0.04, fc=color,
+ ec=color, length_includes_head=True)
+ plt.gca().annotate("K$_d$", xytext=(k_0[0] + (g_d[0] - k_0[0]) / 2, k_0[1] / 2), xy=(6.5 / d / 2, k_0[1] / 2))
+
+ # s_g excitation Error of HOLZ reflection
+ if s_g > 0:
+ plt.gca().arrow(g_d[0], g_d[1], 0, s_g, head_width=0.03, head_length=0.04, fc='k',
+ ec='k', length_includes_head=True)
+ plt.gca().annotate("s$_g$", xytext=(g_d[0] * 1.01, g_d[1] + s_g / 3), xy=(g_d[0] * 1.01, g_d[1] + s_g / 3))
+
+ # Bragg angle
+ g_sg = g_d
+ g_sg[1] = g_d[1] + s_g
+ plt.plot([0 + shift_x, g_sg[0] + shift_x], [0 + shift_y, g_d[1] + shift_y], color=color, linewidth=1, alpha=0.5,
+ linestyle='--')
+ plt.plot([k_0[0], g_sg[0] / 2 + shift_x], [k_0[1], g_sg[1] / 2 + shift_y], color=color, linewidth=1, alpha=0.5,
+ linestyle='--')
+ # d_theta = np.degrees(np.arctan(k_0[0]/k_0[1]))
+ bragg_angle = patches.Arc((k_0[0], k_0[1]), width=k_0[1], height=k_0[1], theta1=-90 + d_theta,
+ theta2=-90 + d_theta + np.degrees(np.arcsin(np.linalg.norm(g_sg / 2) / k_0[1])), fc=color,
+ ec=color)
+
+ plt.gca().annotate(r"$\theta $", xytext=(k_0[0] / 1.3, k_0[1] / 1.5), xy=(k_0[0] / 2 + g_d[0] / 4, k_0[1] / 2))
+ plt.gca().add_patch(bragg_angle)
+
+ # deviation/tilt angle
+ if np.abs(d_theta) > 0:
+ if shift:
+ deviation_angle = patches.Arc((k_0[0], k_0[1]), width=k_0[1] * 1.5, height=k_0[1] * 1.5,
+ theta1=-90 + d_theta,
+ theta2=-90,
+ fc=color, ec=color, linewidth=3)
+ plt.gca().annotate(r"$d \theta $", xytext=(k_0[0] - .13, k_0[1] / 3.7),
+ xy=(k_0[0] + g_d[0] / 4, k_0[1] / 2))
+ plt.gca().arrow(shift_x, -.2, 0, .2, head_width=0.05, head_length=0.06, fc=color, ec='black',
+ length_includes_head=True, linewidth=3)
+ plt.gca().annotate("deficient line", xytext=(shift_x * 2, -.2), xy=(shift_x, 0))
+ else:
+ deviation_angle = patches.Arc((0, 0), width=k_0[1], height=k_0[1],
+ theta1=np.degrees(d_theta2),
+ theta2=np.degrees(d_theta1),
+ fc=color, ec=color, linewidth=3)
+ plt.gca().annotate(r"$d \theta $", xytext=(g_d[0] * .8, 1 / d / 3), xy=(g_d[0], 1 / d))
+
+ plt.gca().add_patch(deviation_angle)
+ plt.gca().set_aspect('equal')
+ plt.gca().set_ylim(-.5, 2.2)
+ plt.gca().set_xlim(-1.1, 1.6)
+
+
+[docs]def deficient_kikuchi_line(s_g=0., color_b='black'):
+ k_len = 1 / ks.get_wavelength(20)
+ d = 2 # lattice parameter in nm
+
+ g = np.linspace(-2, 2, 5) * 1 / d
+ g_d = np.array([1 / d, 0])
+
+ # reciprocal lattice
+ plt.scatter(g, [0] * 5, color='blue')
+
+ alpha = -np.arctan(s_g / g_d[0])
+ theta = -np.arcsin(g_d[0] / 2 / k_len)
+
+ k_0 = np.array([-np.sin(theta - alpha) * k_len, np.cos(theta - alpha) * k_len])
+ k_d = np.array([-np.sin(-theta - alpha) * k_len, np.cos(-theta - alpha) * k_len])
+ k_i = np.array([-np.sin(theta - alpha) * 1., np.cos(theta - alpha) * 1.])
+ k_i_t = np.array([-np.sin(-alpha), np.cos(-alpha)])
+
+ kk_e = np.array([-np.sin(-theta) * k_len, np.cos(-theta) * k_len])
+ kk_d = np.array([-np.sin(theta) * k_len, np.cos(theta) * k_len])
+
+ # Ewald Sphere
+ ewald_sphere = patches.Circle((k_0[0], k_0[1]), radius=np.linalg.norm(k_0), clip_on=False, zorder=10, linewidth=1,
+ edgecolor=color_b, fill=False)
+ plt.gca().add_artist(ewald_sphere)
+
+ # K_0
+ plt.plot([k_0[0], k_0[0]], [k_0[1], k_0[1] + .4], color='gray', linestyle='-', alpha=0.3)
+
+ plt.gca().arrow(k_0[0] + k_i[0], k_0[1] + k_i[1], -k_i[0], -k_i[1], head_width=0.01, head_length=0.015, fc=color_b,
+ ec=color_b, length_includes_head=True)
+ plt.plot([k_0[0] + k_i_t[0], k_0[0] - k_i_t[0]], [k_0[1] + k_i_t[1], k_0[1] - k_i_t[1]], color='black',
+ linestyle='--', alpha=0.5)
+ plt.scatter(k_0[0], k_0[1], color='black')
+ plt.gca().arrow(k_0[0], k_0[1], -k_0[0], -k_0[1], head_width=0.01, head_length=0.015, fc=color_b,
+ ec=color_b, length_includes_head=True)
+ plt.gca().annotate("K$_0$", xytext=(-k_0[0] / 2, 0), xy=(k_0[0] / 2, 0))
+
+ plt.gca().arrow(k_0[0], k_0[1], -k_d[0], -k_d[1], head_width=0.01, head_length=0.015, fc=color_b,
+ ec=color_b, length_includes_head=True)
+ # K_e excess line
+ plt.gca().arrow(k_0[0], k_0[1], -kk_e[0], -kk_e[1], head_width=0.01, head_length=0.015, fc='red',
+ ec='red', length_includes_head=True)
+ plt.gca().annotate("excess", xytext=(k_0[0] - kk_e[0], -1), xy=(-kk_e[0] + k_0[0], 0))
+ plt.plot([k_0[0] - kk_e[0], k_0[0] - kk_e[0]], [-.1, .1], color='red')
+
+ # k_d deficient line
+ plt.gca().arrow(k_0[0], k_0[1], -kk_d[0], -kk_d[1], head_width=0.01, head_length=0.015, fc='blue',
+ ec='blue', length_includes_head=True)
+ plt.plot([k_0[0] - kk_d[0], k_0[0] - kk_d[0]], [-.1, .1], color='blue')
+ plt.gca().annotate("deficient", xytext=(k_0[0] - kk_d[0], -1), xy=(k_0[0] - kk_d[0], 0))
+
+ # s_g excitation Error of HOLZ reflection
+ plt.gca().arrow(g_d[0], g_d[1], 0, s_g, head_width=0.01, head_length=0.015, fc='k',
+ ec='k', length_includes_head=True)
+ plt.gca().annotate("s$_g$", xytext=(g_d[0] * 1.01, g_d[1] + s_g / 3), xy=(g_d[0] * 1.01, g_d[1] + s_g / 3))
+
+ theta = np.degrees(theta)
+ alpha = np.degrees(alpha)
+
+ bragg_angle = patches.Arc((k_0[0], k_0[1]), width=.55, height=.55,
+ theta1=90 + theta - alpha, theta2=90 - alpha, fc='black', ec='black')
+ if alpha > 0:
+ deviation_angle = patches.Arc((k_0[0], k_0[1]), width=.6, height=.6,
+ theta1=90 - alpha, theta2=90, fc='black', ec='red')
+ else:
+ deviation_angle = patches.Arc((k_0[0], k_0[1]), width=.6, height=.6,
+ theta1=90, theta2=90 - alpha, fc='black', ec='red')
+
+ plt.gca().annotate(r"$\theta$", xytext=(k_0[0] + k_i_t[0] / 20, k_0[1] + .2), xy=(k_0[0] + k_i_t[0], k_0[1] + .2))
+ plt.gca().annotate(r"$\alpha$", xytext=(k_0[0] + k_i_t[0] / 10, k_0[1] + .3), xy=(k_0[0] + k_i_t[0], k_0[1] + .3),
+ color='red')
+ plt.gca().add_patch(bragg_angle)
+ plt.gca().add_patch(deviation_angle)
+
+ plt.gca().set_aspect('equal')
+ plt.gca().set_xlabel('angle (1/$\AA$)')
+ plt.gca().set_ylim(-.1, k_0[1] * 2.2)
+ plt.gca().set_xlim(-.2, 1.03)
+
+
+[docs]class InteractiveAberration(object):
+ """
+ ### Interactive explanation of aberrations
+
+ """
+
+ def __init__(self, horizontal=True):
+
+ box_layout = widgets.Layout(display='flex',
+ flex_flow='row',
+ align_items='stretch',
+ width='100%')
+
+ self.words = ['ideal rays', 'aberrated rays', 'aberrated wavefront', 'aberration function']
+
+ self.buttons = [widgets.ToggleButton(value=False, description=word, disabled=False) for word in self.words]
+ box = widgets.Box(children=self.buttons, layout=box_layout)
+ display(box)
+
+ # Button(description='edge_quantification')
+ for button in self.buttons:
+ button.observe(self.on_button_clicked, 'value') # on_click(self.on_button_clicked)
+
+ self.figure = plt.figure()
+ self.ax = plt.gca()
+ self.horizontal = horizontal
+ self.ax.set_aspect('equal')
+ self.analysis = []
+ self.update()
+ # self.cid = self.figure.canvas.mpl_connect('button_press_event', self.onclick)
+
+ def on_button_clicked(self, b):
+ # print(b['owner'].description)
+ selection = b['owner'].description
+ if selection in self.analysis:
+ self.analysis.remove(selection)
+ else:
+ self.analysis.append(selection)
+ self.update()
+
+ def update(self):
+ ax = self.ax
+ ax.clear()
+ selection = self.analysis
+ ax.plot([0, 15], [0, 0], color='black')
+ ax.plot([9, 9], [-.3, .3], color='black')
+ lens = patches.Ellipse((2, 0),
+ width=.4,
+ height=7,
+ facecolor='gray')
+ ax.add_patch(lens)
+ ax.set_ylim(-6.5, 6.5)
+ ax.set_aspect('equal')
+
+ if self.words[0] in selection:
+ color = 'gray'
+ ax.plot([0, 2], [1, 1], color=color)
+ ax.plot([0, 2], [-1, -1], color=color)
+ ax.plot([2, 9], [1, 0], color=color)
+ ax.plot([2, 9], [-1, 0], color=color)
+
+ gauss = patches.Ellipse((9, 0),
+ width=12,
+ height=12,
+ fill=False)
+ ax.add_patch(gauss)
+
+ if self.words[1] in selection:
+ color = 'blue'
+ ax.plot([0, 2], [2, 2], color=color)
+ ax.plot([0, 2], [-2, -2], color=color)
+ ax.plot([2, 7], [2, 0], color=color)
+ ax.plot([2, 7], [-2, 0], color=color)
+ gauss2 = patches.Ellipse((7, 0),
+ width=8,
+ height=8,
+ fill=False,
+ color=color, linestyle='--')
+ plt.gca().add_patch(gauss2)
+
+ if self.words[2] in selection:
+ color = 'red'
+ ax.plot([0, 2], [2, 2], color=color)
+ ax.plot([0, 2], [-2, -2], color=color)
+ ax.plot([2, 7], [2, 0], color=color)
+ ax.plot([2, 7], [-2, 0], color=color)
+ ax.plot([0, 2], [1, 1], color=color)
+ ax.plot([0, 2], [-1, -1], color=color)
+ ax.plot([2, 9], [1, 0], color=color)
+ ax.plot([2, 9], [-1, 0], color=color)
+ gauss3 = patches.Ellipse((9, 0),
+ width=12,
+ height=9.7,
+ fill=False,
+ color=color)
+ plt.gca().add_patch(gauss3)
+
+ if self.words[3] in selection:
+ color = 'green'
+ x = np.arange(100) / 100 - 6
+ x2 = np.arange(100) / 100 * 1.5 - 6
+ b = 4.8
+ a = 6
+ y = np.sqrt(a ** 2 - x ** 2)
+ y2 = b / a * np.sqrt(a ** 2 - x2 ** 2)
+
+ x = np.append(x[::-1], x[1:])
+ y = np.append(y[::-1], -y[1:])
+ x2 = np.append(x2[::-1], x2[1:])
+ y2 = np.append(y2[::-1], -y2[1:])
+
+ dif = y2 - y
+
+ x = np.append(x[::-1], x2)
+ y = np.append(y[::-1], y2)
+ aberration = patches.Polygon(np.array([x + 9, y]).T,
+ fill=True,
+ color=color, alpha=.5)
+
+ aberration2 = patches.Polygon(np.array(
+ [np.append(np.abs(dif), [0, 0]) * 2 + 2.5, np.append(np.linspace(-3.3, 3.3, len(dif)), [3.3, -3.3])]).T,
+ fill=True,
+ color=color, alpha=.9)
+
+ plt.gca().add_patch(aberration)
+ plt.gca().add_patch(aberration2)
+
+
+[docs]class InteractiveRonchigramMagnification(object):
+ """
+ ### Interactive explanation of magnification
+
+ """
+
+ def __init__(self, horizontal=True):
+
+ box_layout = widgets.Layout(display='flex',
+ flex_flow='row',
+ align_items='stretch',
+ width='100%')
+
+ self.words = ['ideal rays', 'radial circle rays', 'axial circle rays', 'over-focused rays']
+
+ self.buttons = [widgets.ToggleButton(value=False, description=word, disabled=False) for word in self.words]
+ box = widgets.Box(children=self.buttons, layout=box_layout)
+ display(box)
+
+ # Button(description='edge_quantification')
+ for button in self.buttons:
+ button.observe(self.on_button_clicked, 'value') # on_click(self.on_button_clicked)
+
+ self.figure = plt.figure()
+ self.ax = plt.gca()
+ self.horizontal = horizontal
+ self.ax.set_aspect('equal')
+ self.analysis = []
+ self.update()
+ # self.cid = self.figure.canvas.mpl_connect('button_press_event', self.onclick)
+
+ def on_button_clicked(self, b):
+ # print(b['owner'].description)
+ selection = b['owner'].description
+ if selection in self.analysis:
+ self.analysis.remove(selection)
+ else:
+ self.analysis.append(selection)
+ self.update()
+
+ def update(self):
+ ax = self.ax
+ ax.clear()
+ selection = self.analysis
+ ax.plot([0, 24], [0, 0], color='black')
+ ax.plot([14, 14], [-.3, .3], color='black')
+ ax.text(14, 1, s='f')
+ lens = patches.Ellipse((4, 0),
+ width=.8,
+ height=14,
+ facecolor='gray')
+ ax.add_patch(lens)
+ ax.text(4, 8, s='lens')
+ sample = patches.Rectangle((10, -2),
+ width=.2,
+ height=4,
+ facecolor='gray')
+
+ ax.add_patch(sample)
+ ax.text(9, 3, s='sample')
+ ax.set_ylim(-10, 10)
+ ax.set_aspect('equal')
+
+ if self.words[0] in selection:
+ color = 'gray'
+ ax.plot([0, 4], [1, 1], color=color)
+ ax.plot([0, 4], [-1, -1], color=color)
+ ax.plot([4, 24], [1, -1], color=color)
+ ax.plot([4, 24], [-1, 1], color=color)
+
+ circle1 = patches.Ellipse((24, 0), width=.2, height=2, fill=False, color=color)
+ ax.add_patch(circle1)
+
+ if self.words[1] in selection:
+ color = 'red'
+ ax.plot([0, 4], [3, 3], color=color)
+ ax.plot([0, 4], [-3, -3], color=color)
+ ax.plot([4, 24], [3, -4], color=color)
+ ax.plot([4, 24], [-3, 4], color=color)
+ ax.plot([0, 4], [2.5, 2.5], color=color)
+ ax.plot([0, 4], [-2.50, -2.5], color=color)
+ ax.plot([4, 24], [2.5, -2.8], color=color)
+ ax.plot([4, 24], [-2.5, 2.8], color=color)
+
+ circle2 = patches.Ellipse((24, 0), width=.9, height=8, fill=False, color=color)
+ ax.add_patch(circle2)
+ circle3 = patches.Ellipse((24, 0), width=.6, height=5.6, fill=False, color=color)
+ ax.add_patch(circle3)
+ circle3 = patches.Ellipse((24, 0), width=.7, height=7.3, fill=False, color=color, linewidth=5, alpha=.5)
+ ax.add_patch(circle3)
+
+ if self.words[2] in selection:
+ color = 'orange'
+ ax.plot([0, 4], [4, 4], color=color)
+ ax.plot([0, 4], [-4, -4], color=color)
+ ax.plot([4, 24], [4, -9.25], color=color)
+ ax.plot([4, 24], [-4, 9.25], color=color)
+
+ circle4 = patches.Ellipse((24, 0), width=2, height=18.5, fill=False, color=color)
+ plt.gca().add_patch(circle4)
+
+ if self.words[3] in selection:
+ color = 'green'
+ ax.plot([0, 4], [5, 5], color=color, linestyle='--')
+ ax.plot([0, 4], [-5, -5], color=color, linestyle='--')
+ ax.plot([4, 24], [5, -13], color=color, linestyle='--')
+ ax.plot([4, 24], [-5, 13], color=color, linestyle='--')
+
+ circle6 = patches.Ellipse((24, 0), width=4, height=26, fill=False, color=color, linestyle='--')
+ plt.gca().add_patch(circle6)
+
+""" Atom detection
+
+All atom detection is done here
+Everything is in unit of pixel!!
+
+Author: Gerd Duscher
+
+part of pyTEMlib
+
+a pycroscopy package
+"""
+
+import numpy as np
+import sys
+
+# from skimage.feature import peak_local_max
+from skimage.feature import blob_log
+
+from sklearn.cluster import KMeans
+from scipy.spatial import cKDTree
+import scipy.optimize as optimization
+
+import pyTEMlib.probe_tools as probe_tools
+import pyTEMlib.file_tools as ft
+import sidpy
+from tqdm.auto import trange
+
+
+[docs]def find_atoms(image, atom_size=0.1, threshold=0.):
+ """ Find atoms is a simple wrapper for blob_log in skimage.feature
+
+ Parameters
+ ----------
+ image: sidpy.Dataset
+ the image to find atoms
+ atom_size: float
+ visible size of atom blob diameter in nm gives minimal distance between found blobs
+ threshold: float
+ threshold for blob finder; (usually between 0.001 and 1.0) for threshold <= 0 we use the RMS contrast
+
+ Returns
+ -------
+ atoms: numpy array(Nx3)
+ atoms positions and radius of blob
+ """
+
+ if not isinstance(image, sidpy.Dataset):
+ raise TypeError('We need a sidpy.Dataset')
+ if image.data_type.name != 'IMAGE':
+ raise TypeError('We need sidpy.Dataset of sidpy.Datatype: IMAGE')
+ if not isinstance(atom_size, (float, int)):
+ raise TypeError('atom_size parameter has to be a number')
+ if not isinstance(threshold, float):
+ raise TypeError('threshold parameter has to be a float number')
+
+ scale_x = ft.get_slope(image.dim_0)
+ im = np.array(image-image.min())
+ im = im/im.max()
+ if threshold <= 0.:
+ threshold = np.std(im)
+ atoms = blob_log(im, max_sigma=atom_size/scale_x, threshold=threshold)
+
+ return atoms
+
+
+[docs]def atoms_clustering(atoms, mid_atoms, number_of_clusters=3, nearest_neighbours=7):
+ """ A wrapper for sklearn.cluster kmeans clustering of atoms.
+
+ Parameters
+ ----------
+ atoms: list or np.array (Nx2)
+ list of all atoms
+ mid_atoms: list or np.array (Nx2)
+ atoms to be evaluated
+ number_of_clusters: int
+ number of clusters to sort (ini=3)
+ nearest_neighbours: int
+ number of nearest neighbours evaluated
+
+ Returns
+ -------
+ clusters, distances, indices: numpy arrays
+ """
+
+ # get distances
+ nn_tree = cKDTree(np.array(atoms)[:, 0:2])
+
+ distances, indices = nn_tree.query(np.array(mid_atoms)[:, 0:2], nearest_neighbours)
+
+ # Clustering
+ k_means = KMeans(n_clusters=number_of_clusters, random_state=0) # Fixing the RNG in kmeans
+ k_means.fit(distances)
+ clusters = k_means.predict(distances)
+
+ return clusters, distances, indices
+
+
+[docs]def gauss_difference(params, area):
+ """
+ Difference between part of an image and a Gaussian
+ This function is used int he atom refine function of pyTEMlib
+
+ Parameters
+ ----------
+ params: list
+ list of Gaussian parameters [width, position_x, position_y, intensity]
+ area: numpy array
+ 2D matrix = part of an image
+
+ Returns
+ -------
+ numpy array: flattened array of difference
+
+ """
+ gauss = probe_tools.make_gauss(area.shape[0], area.shape[1], width=params[0], x0=params[1], y0=params[2],
+ intensity=params[3])
+ return (area - gauss).flatten()
+
+
+[docs]def atom_refine(image, atoms, radius, max_int=0, min_int=0, max_dist=4):
+ """Fits a Gaussian in a blob of an image
+
+ Parameters
+ ----------
+ image: np.array or sidpy Dataset
+ atoms: list or np.array
+ positions of atoms
+ radius: float
+ radius of circular mask to define fitting of Gaussian
+ max_int: float
+ optional - maximum intensity to be considered for fitting (to exclude contaminated areas for example)
+ min_int: float
+ optional - minimum intensity to be considered for fitting (to exclude contaminated holes for example)
+ max_dist: float
+ optional - maximum distance of movement of Gaussian during fitting
+
+ Returns
+ -------
+ sym: dict
+ dictionary containing new atom positions and other output such as intensity of the fitted Gaussian
+ """
+ rr = int(radius + 0.5) # atom radius
+ print('using radius ', rr, 'pixels')
+
+ pixels = np.linspace(0, 2 * rr, 2 * rr + 1) - rr
+ x, y = np.meshgrid(pixels, pixels)
+ mask = (x ** 2 + y ** 2) < rr ** 2
+
+ guess = [rr * 2, 0.0, 0.0, 1]
+
+ sym = {'number_of_atoms': len(atoms)}
+
+ volume = []
+ position = []
+ intensities = []
+ maximum_area = []
+ new_atoms = []
+ gauss_width = []
+ gauss_amplitude = []
+ gauss_intensity = []
+
+ for i in trange(len(atoms)):
+ x, y = atoms[i][0:2]
+ x = int(x)
+ y = int(y)
+
+ area = image[x - rr:x + rr + 1, y - rr:y + rr + 1]
+
+ append = False
+
+ if (x - rr) < 0 or y - rr < 0 or x + rr + 1 > image.shape[0] or y + rr + 1 > image.shape[1]: # atom not found
+ position.append(-1)
+ intensities.append(-1.)
+ maximum_area.append(-1.)
+ else: # atom found
+ position.append(1)
+ intensities.append((area * mask).sum())
+ maximum_area.append((area * mask).max())
+
+ if max_int > 0:
+ if area.sum() < max_int:
+ if area.sum() > min_int:
+ append = True
+ elif area.sum() > min_int:
+ append = True
+
+ pout = [0, 0, 0, 0]
+ if append:
+ if (x - rr) < 0 or y - rr < 0 or x + rr + 1 > image.shape[0] or y + rr + 1 > image.shape[1]:
+ pass
+ else:
+ [pout, _] = optimization.leastsq(gauss_difference, guess, args=area)
+
+ if (abs(pout[1]) > max_dist) or (abs(pout[2]) > max_dist):
+ pout = [0, 0, 0, 0]
+
+ volume.append(2 * np.pi * pout[3] * pout[0] * pout[0])
+
+ new_atoms.append([x + pout[1], y + pout[2]]) # ,pout[0], volume)) #,pout[3]))
+ if all(v == 0 for v in pout):
+ gauss_intensity.append(0.)
+ else:
+ gauss = probe_tools.make_gauss(area.shape[0], area.shape[1], width=pout[0], x0=pout[1], y0=pout[2],
+ intensity=pout[3])
+ gauss_intensity.append((gauss * mask).sum())
+ gauss_width.append(pout[0])
+ gauss_amplitude.append(pout[3])
+
+ sym['inside'] = position
+ sym['intensity_area'] = intensities
+ sym['maximum_area'] = maximum_area
+ sym['atoms'] = new_atoms
+ sym['gauss_width'] = gauss_width
+ sym['gauss_amplitude'] = gauss_amplitude
+ sym['gauss_intensity'] = gauss_intensity
+ sym['gauss_volume'] = volume
+
+ return sym
+
+
+[docs]def intensity_area(image, atoms, radius):
+ """
+ integrated intensity of atoms in an image with a mask around each atom of radius radius
+ """
+ rr = int(radius + 0.5) # atom radius
+ print('using radius ', rr, 'pixels')
+
+ pixels = np.linspace(0, 2 * rr, 2 * rr + 1) - rr
+ x, y = np.meshgrid(pixels, pixels)
+ mask = np.array((x ** 2 + y ** 2) < rr ** 2)
+ intensities = []
+ for i in range(len(atoms)):
+ x = int(atoms[i][1])
+ y = int(atoms[i][0])
+ area = image[x - rr:x + rr + 1, y - rr:y + rr + 1]
+ if area.shape == mask.shape:
+ intensities.append((area * mask).sum())
+ else:
+ intensities.append(-1)
+ return intensities
+
+"""
+crystal_tools
+
+part of pyTEMlib
+
+Author: Gerd Duscher
+
+Provides convenient functions to make most regular crystal structures
+
+Contains also a dictionary of crystal structures and atomic form factors
+
+Units:
+ everything is in SI units, except length is given in nm.
+ angles are assumed to be in degree but will be internally converted to rad
+
+Usage:
+ See the notebooks for examples of these routines
+
+"""
+
+import numpy as np
+import itertools
+import ase
+import ase.spacegroup
+import ase.build
+import ase.data.colors
+
+import matplotlib.pylab as plt # basic plotting
+from scipy.spatial import cKDTree
+_spglib_present = True
+try:
+ import spglib
+except ModuleNotFoundError:
+ _spglib_present = False
+
+if _spglib_present:
+ print('Symmetry functions of spglib enabled')
+else:
+ print('spglib not installed; Symmetry functions of spglib disabled')
+
+
+# from mpl_toolkits.mplot3d import Axes3D # 3D plotting
+# from matplotlib.patches import Circle # , Ellipse, Rectangle
+# from matplotlib.collections import PatchCollection
+
+
+[docs]def get_dictionary(atoms):
+ """
+ structure dictionary from ase.Atoms object
+ """
+ tags = {'unit_cell': atoms.cell.array,
+ 'elements': atoms.get_chemical_formula(),
+ 'base': atoms.get_scaled_positions(),
+ 'metadata': atoms.info}
+
+ return tags
+
+
+[docs]def atoms_from_dictionary(tags):
+ atoms = ase.Atoms(cell=tags['unit_cell'],
+ symbols=tags['elements'],
+ scaled_positions=tags['base'])
+ if 'metadata' in tags:
+ atoms.info = tags['metadata']
+ return atoms
+
+
+[docs]def get_symmetry(atoms, verbose=True):
+ """
+ Symmetry analysis with spglib
+
+ spglib must be installed
+
+ Parameters
+ ----------
+ atoms: ase.Atoms object
+ crystal structure
+ verbose: bool
+
+ Returns
+ -------
+
+ """
+ if _spglib_present:
+ if verbose:
+ print('#####################')
+ print('# Symmetry Analysis #')
+ print('#####################')
+
+ base = atoms.get_scaled_positions()
+ for i, atom in enumerate(atoms):
+ if verbose:
+ print(f'{i + 1}: {atom.number} = {2} : [{base[i][0]:.2f}, {base[i][1]:.2f}, {base[i][2]:.2f}]')
+
+ lattice = (atoms.cell, atoms.get_scaled_positions(), atoms.numbers)
+ spgroup = spglib.get_spacegroup(lattice, symprec=1e-2)
+ sym = spglib.get_symmetry(lattice)
+
+ if verbose:
+ print(" Spacegroup is %s." % spgroup)
+ print(' Crystal has {0} symmetry operation'.format(sym['rotations'].shape[0]))
+
+ p_lattice, p_positions, p_numbers = spglib.find_primitive(lattice, symprec=1e-5)
+ print("\n########################\n #Basis vectors of primitive Cell:")
+ for i in range(3):
+ print('[{0:.4f}, {1:.4f}, {2:.4f}]'.format(p_lattice[i][0], p_lattice[i][1], p_lattice[i][2]))
+
+ print('There {0} atoms and {1} species in primitive unit cell:'.format(len(p_positions), p_numbers))
+ else:
+ print('spglib is not installed')
+
+ return True
+
+
+[docs]def set_bond_radii(atoms):
+ bond_radii = np.ones(len(atoms))
+ for i in range(len(atoms)):
+ bond_radii[i] = electronFF[atoms.symbols[i]]['bond_length'][1]
+ atoms.info['bond_radii'] = bond_radii
+
+
+[docs]def get_projection(crystal, layers=1):
+ zone_axis = crystal.info['experimental']['zone_axis']
+ angle = crystal.info['experimental']['angle']
+ projected_crystal = ase.build.surface(crystal, zone_axis, vacuum=.0, layers=layers)
+
+ element_tree = cKDTree(projected_crystal.positions[:, 0:2])
+ done = []
+ projected = []
+ for atom in projected_crystal:
+ if atom.index not in done:
+ near = element_tree.query_ball_point(atom.position[:2], 0.05)
+ projected.append(near)
+ done.extend(near)
+ print('projected atomic numbers')
+ atomic_numbers = []
+ for pro in projected:
+ atomic_numbers.append(projected_crystal.get_atomic_numbers()[pro].sum())
+
+ projected_crystal.rotate(np.degrees(angle)%360, 'z', rotate_cell=True)
+
+ near_base = np.array([projected_crystal.cell[0,:2], -projected_crystal.cell[0,:2],
+ projected_crystal.cell[1,:2], -projected_crystal.cell[1,:2],
+ projected_crystal.cell[0,:2] + projected_crystal.cell[1,:2],
+ -(projected_crystal.cell[0,:2] + projected_crystal.cell[1,:2])])
+ lines = np.array( [[[0, near_base[0,0]],[0, near_base[0,1]]],
+ [[0, near_base[2,0]],[0, near_base[2,1]]],
+ [[near_base[0,0], near_base[4,0]],[near_base[0,1], near_base[4,1]]],
+ [[near_base[2,0], near_base[4,0]],[near_base[2,1], near_base[4,1]]]])
+ projected_atoms = []
+ for index in projected:
+ projected_atoms.append(index[0])
+
+ projected_crystal.info['projection']={'indices': projected,
+ 'projected': projected_atoms,
+ 'projected_Z': atomic_numbers,
+ 'angle': np.degrees(angle)+180%360,
+ 'near_base': near_base,
+ 'lines': lines}
+ return projected_crystal
+
+
+[docs]def jmol_viewer(atoms, size=2):
+ """
+ jmol viewer of ase .Atoms object
+ requires jupyter-jsmol to be installed (available through conda or pip)
+
+ Parameter
+ ---------
+ atoms: ase.Atoms
+ structure info
+ size: int, list, or np.array of size 3; default 1
+ size of unit_cell; maximum = 8
+
+ Returns
+ -------
+ view: JsmolView object
+
+ Example
+ -------
+ from jupyter_jsmol import JsmolView
+ import ase
+ import ase.build
+ import itertools
+ import numpy as np
+ atoms = ase.build.bulk('Cu', 'fcc', a=5.76911, cubic=True)
+ for pos in list(itertools.product([0.25, .75], repeat=3)):
+ atoms += ase.Atom('Al', al2cu.cell.lengths()*pos)
+
+ view = plot_ase(atoms, size = 8)
+ display(view)
+ """
+ try:
+ from jupyter_jsmol import JsmolView
+ from IPython.display import display
+ except ImportError:
+ print('this function is based on jupyter-jsmol, please install with: \n '
+ 'conda install -c conda-forge jupyter-jsmol')
+ return
+
+ if isinstance(size, int):
+ size = [size] * 3
+
+ [a, b, c] = atoms.cell.lengths()
+ [alpha, beta, gamma] = atoms.cell.angles()
+
+ view = JsmolView.from_ase(atoms, f"{{{size[0]} {size[1]} {size[2]}}}"
+ f" unitcell {{{a:.3f} {b:.3f} {c:.3f} {alpha:.3f} {beta:.3f} {gamma:.3f}}}")
+
+ display(view)
+
+ return view
+
+
+[docs]def plot_super_cell(super_cell, shift_x=0.):
+ """ make a super_cell to plot with extra atoms at periodic boundaries"""
+
+ if not isinstance(super_cell, ase.Atoms):
+ raise TypeError('Need an ase Atoms object')
+
+ super_cell2plot = super_cell * (2, 2, 2)
+ super_cell2plot.positions[:, 0] = super_cell2plot.positions[:, 0] - super_cell2plot.cell[0, 0] * shift_x
+
+ del super_cell2plot[super_cell2plot.positions[:, 2] > super_cell.cell[2, 2] + 0.1]
+ del super_cell2plot[super_cell2plot.positions[:, 1] > super_cell.cell[1, 1] + 0.1]
+ del super_cell2plot[super_cell2plot.positions[:, 0] > super_cell.cell[0, 0] + 0.1]
+ del super_cell2plot[super_cell2plot.positions[:, 0] < -0.1]
+ super_cell2plot.cell = super_cell.cell * (1, 1, 1)
+
+ return super_cell2plot
+
+
+[docs]def ball_and_stick(atoms, extend=1, max_bond_length=0.):
+ """Calculates the data to plot a ball and stick model
+
+ Parameters
+ ----------
+ atoms: ase.Atoms object
+ object containing the structural information like 'cell', 'positions', and 'symbols' .
+
+ extend: integer or list f 3 integers
+ The *extend* argument scales the effective cell in which atoms
+ will be included. It must either be a list of three integers or a single
+ integer scaling all 3 directions. By setting this value to one,
+ all corner and edge atoms will be included in the returned cell.
+ This will of cause make the returned cell non-repeatable, but this is
+ very useful for visualisation.
+
+ max_bond_length: 1 float
+ The max_bond_length argument defines the distance for which a bond will be shown.
+ If max_bond_length is zero, the tabulated atom radii will be used.
+
+ Returns
+ -------
+ super_cell: ase.Atoms object
+ structure with additional information in info dictionary
+ """
+
+ if not isinstance(atoms, ase.Atoms):
+ raise TypeError('Need an ase Atoms object')
+
+ from ase import neighborlist
+ from scipy import sparse
+ from scipy.sparse import dok_matrix
+
+ super_cell = plot_super_cell(atoms*extend)
+ cell = super_cell.cell.array
+ # Corners and Outline of unit cell
+ h = (0, 1)
+ corner_vectors = np.dot(np.array(list(itertools.product(h, h, h))), cell)
+ corner_matrix = dok_matrix((8, 8), dtype=bool)
+ trace = [[0, 1], [1, 3], [2, 3], [0, 2], [0, 4], [4, 5], [5, 7], [6, 7], [4, 6], [1, 5], [2, 6], [3, 7]]
+ for s, e in trace:
+ corner_matrix[s, e] = True
+
+ # List of bond lengths taken from electronFF database below
+ bond_lengths = []
+ for atom in super_cell:
+ bond_lengths.append(electronFF[atom.symbol]['bond_length'][1])
+
+ super_cell.set_cell(cell*2, scale_atoms=False) # otherwise, corner atoms have distance 0
+ neighbor_list = neighborlist.NeighborList(bond_lengths, self_interaction=False, bothways=False)
+ neighbor_list.update(super_cell)
+ bond_matrix = neighbor_list.get_connectivity_matrix()
+
+ del_double = []
+ for (k, s) in bond_matrix.keys():
+ if k > s:
+ del_double.append((k, s))
+ for key in del_double:
+ bond_matrix.pop(key)
+
+ if super_cell.info is None:
+ super_cell.info = {}
+ super_cell.info['plot_cell'] = {'bond_matrix': bond_matrix, 'corner_vectors': corner_vectors,
+ 'bond_length': bond_lengths, 'corner_matrix': corner_matrix}
+ super_cell.set_cell(cell/2, scale_atoms=False)
+ return super_cell
+
+
+[docs]def plot_unit_cell(atoms, extend=1, max_bond_length=1.0, ax = None):
+ """
+ Simple plot of unit cell
+ """
+
+ super_cell = ball_and_stick(atoms, extend=extend, max_bond_length=max_bond_length)
+
+ corners = super_cell.info['plot_cell']['corner_vectors']
+ positions = super_cell.positions - super_cell.cell.lengths()/2
+
+ if ax is None:
+ fig = plt.figure()
+ ax = fig.add_subplot(111, projection='3d')
+ # draw unit_cell
+
+ for line in super_cell.info['plot_cell']['corner_matrix'].keys():
+ ax.plot3D(corners[line, 0], corners[line, 1], corners[line, 2], color="blue")
+
+ # draw bonds
+ bond_matrix = super_cell.info['plot_cell']['bond_matrix']
+ for bond in super_cell.info['plot_cell']['bond_matrix'].keys():
+ ax.plot3D(positions[bond, 0], positions[bond, 1], positions[bond, 2], color="black", linewidth=4)
+ # , tube_radius=0.02)
+
+ # draw atoms
+ ax.scatter(super_cell.positions[:, 0], super_cell.positions[:, 1], super_cell.positions[:, 2],
+ color=tuple(jmol_colors[super_cell.get_atomic_numbers()]), alpha=1.0, s=50)
+ maximum_position = super_cell.positions.max()*1.05
+ ax.set_proj_type('ortho')
+
+ ax.set_zlim(-maximum_position/2, maximum_position/2)
+ ax.set_ylim(-maximum_position/2, maximum_position/2)
+ ax.set_xlim(-maximum_position/2, maximum_position/2)
+
+ if 'name' in super_cell.info:
+ ax.set_title(super_cell.info['name'])
+
+ ax.set_xlabel('x [Å]')
+ ax.set_ylabel('y [Å]')
+ ax.set_zlabel('z [Å]')
+ return ax.get_figure()
+
+
+# Jmol colors. See: http://jmol.sourceforge.net/jscolors/#color_U
+jmol_colors = ase.data.colors.jmol_colors
+
+
+[docs]def structure_by_name(crystal_name):
+ """
+ Provides crystal structure in ase.Atoms format.
+ Additional information is stored in the info attribute as a dictionary
+
+
+ Parameter
+ ---------
+ crystal_name: str
+ Please note that the chemical expressions are not case-sensitive.
+
+ Returns
+ -------
+ atoms: ase.Atoms
+ structure
+
+ Example
+ -------
+ >> # for a list of pre-defined crystal structures
+ >> import pyTEMlib.crystal_tools
+ >> print(pyTEMlib.crystal_tools.crystal_data_base.keys())
+ >>
+ >> atoms = pyTEMlib.crystal_tools.structure_by_name('Silicon')
+ >> print(atoms)
+ >> print(atoms.info)
+
+ """
+
+ # Check whether name is in the crystal_data_base
+ import ase
+ import ase.build
+
+ if crystal_name.lower() in cdb:
+ tags = cdb[crystal_name.lower()].copy()
+ else:
+ print(f'Crystal name {crystal_name.lower()} not defined')
+ return
+
+ if 'symmetry' in tags:
+ if tags['symmetry'].lower() == 'fcc':
+ atoms = ase.build.bulk(tags['elements'], 'fcc', a=tags['a'], cubic=True)
+
+ elif tags['symmetry'].lower() == 'bcc':
+ atoms = ase.build.bulk(tags['elements'], 'bcc', a=tags['a'], cubic=True)
+
+ elif tags['symmetry'].lower() == 'diamond':
+ import ase.lattice.cubic
+ atoms = ase.lattice.cubic.Diamond(tags['elements'], latticeconstant=tags['a'])
+
+ elif 'rocksalt' in tags['symmetry']: # B1
+ import ase.lattice.compounds
+ atoms = ase.lattice.compounds.Rocksalt(tags['elements'], latticeconstant=tags['a'])
+
+ elif 'zincblende' in tags['symmetry']:
+ import ase.lattice.compounds
+ atoms = ase.lattice.compounds.B3(tags['elements'], latticeconstant=tags['a'])
+
+ elif 'B2' in tags['symmetry']:
+ import ase.lattice.compounds
+ atoms = ase.lattice.compounds.B2(tags['elements'], latticeconstant=tags['a'])
+
+ elif 'graphite' in tags['symmetry']:
+ base = [(0, 0, 0), (0, 0, 1/2), (2/3, 1/3, 0), (1/3, 2/3, 1/2)]
+ structure_matrix = np.array([[tags['a'], 0., 0.],
+ [np.cos(np.pi/3*2)*tags['a'], np.sin(np.pi/3*2)*tags['a'], 0.],
+ [0., 0., tags['c']]])
+
+ atoms = ase.Atoms(tags['elements'], cell=structure_matrix, scaled_positions=base)
+
+ elif 'perovskite' in tags['symmetry']:
+ import ase.spacegroup
+ atom_positions = [(0.0, 0.0, 0.0), (0.5, 0.5, 0.5), (0.5, 0.5, 0.0)]
+ atoms = ase.spacegroup.crystal(tags['elements'], atom_positions, spacegroup=221, cellpar=tags['a'])
+
+ elif 'wurzite' in tags['symmetry']:
+ import ase.spacegroup
+ atom_positions = [(1/3, 2/3, 0.0), (1/3, 2/3, tags['u'])]
+ atoms = ase.spacegroup.crystal(tags['elements'], atom_positions, spacegroup=186,
+ cellpar=[tags['a'], tags['a'], tags['c'], 90, 90, 120])
+
+ elif 'rutile' in tags['symmetry']:
+ import ase.spacegroup
+ atoms = ase.spacegroup.crystal(tags['elements'], basis=[(0, 0, 0), (0.3, 0.3, 0.0)],
+ spacegroup=136, cellpar=[tags['a'], tags['a'], tags['c'], 90, 90, 90])
+ elif 'dichalcogenide' in tags['symmetry']:
+ import ase.spacegroup
+
+ u = tags['u']
+ base = [(1 / 3., 2 / 3., 1 / 4.), (2 / 3., 1 / 3., 3 / 4.),
+ (2 / 3., 1 / 3., 1 / 4. + u), (2 / 3., 1 / 3., 1 / 4. - u),
+ (1 / 3., 2 / 3., 3 / 4. + u), (1 / 3., 2 / 3., 3 / 4. - u)]
+ atoms = ase.spacegroup.crystal(tags['elements'][0] * 2 + tags['elements'][1] * 4, base, spacegroup=194,
+ cellpar=[tags['a'], tags['a'], tags['c'], 90, 90, 120])
+
+ elif tags['symmetry'].lower() in ['primitive', 'hexagonal']:
+ atoms = ase.Atoms(tags['elements'], cell=tags['unit_cell'], scaled_positions=tags['base'])
+
+ else:
+ print(' symmetry of structure is wrong')
+
+ atoms.info = {'structure': {'reference': tags['reference'], 'link': tags['link']},
+ 'title': tags['crystal_name']}
+ return atoms
+
+
+# crystal data base cbd
+cdb = {'aluminum': {'crystal_name': 'aluminum',
+ 'symmetry': 'FCC',
+ 'elements': 'Al',
+ 'a': 4.05, # Angstrom
+ 'reference': 'W. Witt, Z. Naturforsch. A, 1967, 22A, 92',
+ 'link': 'http://doi.org/10.1515/zna-1967-0115'}}
+cdb['al'] = cdb['aluminium'] = cdb['aluminum']
+
+cdb['gold'] = {'crystal_name': 'gold',
+ 'symmetry': 'FCC',
+ 'elements': 'Au',
+ 'a': 4.0782, # Angstrom
+ 'reference': '',
+ 'link': ''}
+cdb['au'] = cdb['gold']
+
+cdb['silver'] = {'crystal_name': 'silver',
+ 'symmetry': 'FCC',
+ 'elements': 'Ag',
+ 'a': 4.0853, # Angstrom
+ 'reference': '', 'link': ''}
+cdb['ag'] = cdb['silver']
+
+cdb['copper'] = {'crystal_name': 'copper',
+ 'symmetry': 'FCC',
+ 'elements': 'Cu',
+ 'a': 4.0853, # Angstrom
+ 'reference': '', 'link': ''}
+cdb['cu'] = cdb['copper']
+
+cdb['diamond'] = {'crystal_name': 'diamond',
+ 'symmetry': 'diamond',
+ 'elements': 'C',
+ 'a': 3.5668, # Angstrom
+ 'reference': '', 'link': ''}
+
+cdb['germanium'] = {'crystal_name': 'germanium',
+ 'symmetry': 'diamond',
+ 'elements': 'Ge',
+ 'a': 5.66806348, # Angstrom for 300K
+ 'reference': 'H. P. Singh, Acta Crystallogr., 1968, 24A, 469',
+ 'link': 'https://doi.org/10.1107/S056773946800094X'}
+cdb['ge'] = cdb['germanium']
+
+cdb['silicon'] = {'crystal_name': 'silicon',
+ 'symmetry': 'diamond',
+ 'elements': 'Si',
+ 'a': 5.430880, # Angstrom for 300K
+ 'reference': 'C. R. Hubbard, H. E. Swanson, and F. A. Mauer, J. Appl. Crystallogr., 1975, 8, 45',
+ 'link': 'https://doi.org/10.1107/S0021889875009508'}
+cdb['si'] = cdb['silicon']
+
+cdb['gaas'] = {'crystal_name': 'GaAs',
+ 'symmetry': 'zincblende(B3)',
+ 'elements': ['Ga', 'As'],
+ 'a': 5.65325, # Angstrom for 300K
+ 'reference': 'J.F.C. Baker, M. Hart, M.A.G. Halliwell, R. Heckingbottom, Solid-State Electronics, 19, '
+ '1976, 331-334,',
+ 'link': 'https://doi.org/10.1016/0038-1101(76)90031-9'}
+
+cdb['fcc fe'] = {'crystal_name': 'FCC Fe',
+ 'symmetry': 'FCC',
+ 'elements': 'Fe',
+ 'a': 3.3571, # Angstrom
+ 'reference': 'R. Kohlhaas, P. Donner, and N. Schmitz-Pranghe, Z. Angew. Phys., 1967, 23, 245',
+ 'link': ''}
+
+cdb['iron'] = {'crystal_name': 'BCC Fe',
+ 'symmetry': 'BCC',
+ 'elements': 'Fe',
+ 'a': 2.866, # Angstrom
+ 'reference': 'Z. S. Basinski, W. Hume-Rothery and A. L. Sutton, Proceedings of the Royal Society of '
+ 'London. Series A, Mathematical and Physical Sciences Vol. 229, No. 1179 '
+ '(May 24, 1955), pp. 459-467',
+ 'link': 'http://www.jstor.org/stable/99693'}
+cdb['bcc fe'] = cdb['alpha iron'] = cdb['iron']
+
+cdb['srtio3'] = {'crystal_name': 'SrTiO3',
+ 'symmetry': 'perovskite',
+ 'elements': ['Sr', 'Ti', 'O'],
+ 'a': 3.905268, # Angstrom
+ 'reference': 'M. Schmidbauer, A. Kwasniewski and J. Schwarzkopf, Acta Cryst. (2012). B68, 8-14',
+ 'link': 'http://doi.org/10.1107/S0108768111046738'}
+cdb['strontium titanate'] = cdb['srtio3']
+
+cdb['graphite'] = {'crystal_name': 'graphite',
+ 'symmetry': 'graphite hexagonal',
+ 'elements': 'C4',
+ 'a': 2.46772414,
+ 'c': 6.711,
+ 'reference': 'P. Trucano and R. Chen, Nature, 1975, 258, 136',
+ 'link': 'https://doi.org/10.1038/258136a0'}
+
+cdb['cscl'] = {'crystal_name': 'CsCl',
+ 'symmetry': 'CsCl (B2)',
+ 'a': 4.209, # Angstrom
+ 'elements': ['Cs', 'Cl'],
+ 'reference': '', 'link': ''}
+cdb['cesium chlorid'] = cdb['cscl']
+
+cdb['mgo'] = {'crystal_name': 'MgO',
+ 'symmetry': 'rocksalt (B1)',
+ 'elements': ['Mg', 'O'],
+ 'a': 4.256483, # Angstrom
+ 'reference': '', 'link': ''}
+
+cdb['titanium nitride'] = {'crystal_name': 'TiN',
+ 'symmetry': 'rocksalt (B1)',
+ 'elements': ['Ti', 'N'],
+ 'a': 4.25353445, # Angstrom
+ 'reference': '', 'link': '',
+ 'space_group': 225,
+ 'symmetry_name': 'Fm-3m'}
+
+cdb['zno wurzite'] = {'crystal_name': 'ZnO Wurzite',
+ 'symmetry': 'wurzite',
+ 'elements': ['Zn', 'O'],
+ 'a': 3.278, # Angstrom
+ 'c': 5.292, # Angstrom
+ 'u': 0.382,
+ 'reference': '', 'link': ''}
+cdb['zno'] = cdb['wzno'] = cdb['zno wurzite']
+
+cdb['gan'] = {'crystal_name': 'GaN Wurzite',
+ 'symmetry': 'wurzite',
+ 'elements': ['Ga', 'N'],
+ 'a': 3.186, # Angstrom
+ 'c': 5.186, # Angstrom
+ 'u': 0.376393,
+ 'reference': '', 'link': ''}
+cdb['gan wurzite'] = cdb['wgan'] = cdb['gallium nitride'] = cdb['gan']
+
+
+cdb['tio2'] = {'crystal_name': 'TiO2 rutile',
+ 'symmetry': 'rutile',
+ 'elements': ['Ti', 'O'],
+ 'a': 4.6, # Angstrom
+ 'c': 2.95, # Angstrom
+ 'reference': '', 'link': ''}
+
+cdb['mos2'] = {'crystal_name': 'MoS2',
+ 'symmetry': 'dichalcogenide',
+ 'elements': ['Mo', 'S'],
+ 'a': 3.19031573, # Angstrom
+ 'c': 14.87900430, # Angstrom
+ 'u': 0.105174,
+ 'reference': '', 'link': ''}
+
+cdb['ws2'] = {'crystal_name': 'WS2',
+ 'symmetry': 'dichalcogenide',
+ 'elements': ['W', 'S'],
+ 'a': 3.19073051, # Angstrom
+ 'c': 14.20240204, # Angstrom
+ 'u': 0.110759,
+ 'reference': '', 'link': ''}
+
+cdb['wse2'] = {'crystal_name': 'WSe2',
+ 'symmetry': 'dichalcogenide',
+ 'elements': ['W', 'Se'],
+ 'a': 3.32706918, # Angstrom
+ 'c': 15.06895072, # Angstrom
+ 'u': 0.111569,
+ 'reference': '', 'link': ''}
+
+cdb['mose2'] = {'crystal_name': 'MoSe2',
+ 'symmetry': 'dichalcogenide',
+ 'elements': ['Mo', 'Se'],
+ 'a': 3.32694913, # Angstrom
+ 'c': 15.45142322, # Angstrom
+ 'u': 0.108249,
+ 'reference': '', 'link': ''}
+a_l = 0.3336
+c_l = 0.4754
+base_l = [(2. / 3., 1. / 3., .5), (1. / 3., 2. / 3., 0.), (2. / 3., 1. / 3., 0.), (1. / 3., 2. / 3., .5)]
+
+cdb['zno hexagonal'] = {'crystal_name': 'ZnO hexagonal',
+ 'symmetry': 'hexagonal',
+ 'a': a_l, # nm
+ 'c': c_l, # not np.sqrt(8/3)*1
+ 'elements': ['Zn', 'Zn', 'O', 'O'],
+ 'unit_cell': [[a_l, 0., 0.],
+ [np.cos(120 / 180 * np.pi) * a_l, np.sin(120 / 180 * np.pi) * a_l, 0.],
+ [0., 0., c_l]],
+ 'base': np.array(base_l),
+ 'reference': '', 'link': ''}
+
+cdb['pdse2'] = {'crystal_name': 'PdSe2',
+ 'symmetry': 'primitive',
+ 'unit_cell': (np.identity(3) * (.579441832, 0.594542204, 0.858506072)),
+ 'elements': ['Pd'] * 4 + ['Se'] * 8,
+ 'base': np.array([[.5, .0, .0], [.0, 0.5, 0.0],
+ [.5, 0.5, 0.5], [.0, 0.5, 0.5],
+ [0.611300, 0.119356, 0.585891],
+ [0.111300, 0.380644, 0.414109],
+ [0.388700, 0.619356, 0.914109],
+ [0.888700, 0.880644, 0.085891],
+ [0.111300, 0.119356, 0.914109],
+ [0.611300, 0.380644, 0.085891],
+ [0.888700, 0.619356, 0.585891],
+ [0.388700, 0.880644, 0.414109]]),
+ 'reference': '', 'link': ''}
+
+crystal_data_base = cdb
+
+# From Appendix C of Kirkland, "Advanced Computing in Electron Microscopy", 2nd ed.
+electronFF = {
+ # form factor coefficients
+ # Z= 6, chisq= 0.143335
+ # a1 b1 a2 b2
+ # a3 b3 c1 d1
+ # c2 d2 c3 d3
+
+ # name of the file: feKirkland.txt
+ # converted with program sortFF.py
+ # form factor parametrized in 1/Angstrom
+ # bond_length as a list of atom Sizes, bond radii, angle radii, H-bond radii
+
+ 'H': {'Z': 1, 'chisq': 0.170190,
+ 'bond_length': [0.98, 0.78, 1.20, 0],
+ 'fa': [4.20298324e-003, 6.27762505e-002, 3.00907347e-002],
+ 'fb': [2.25350888e-001, 2.25366950e-001, 2.25331756e-001],
+ 'fc': [6.77756695e-002, 3.56609237e-003, 2.76135815e-002],
+ 'fd': [4.38854001e+000, 4.03884823e-001, 1.44490166e+000]},
+ 'He': {'Z': 2, 'chisq': 0.396634,
+ 'bond_length': [1.45, 1.25, 1.40, 0],
+ 'fa': [1.87543704e-005, 4.10595800e-004, 1.96300059e-001],
+ 'fb': [2.12427997e-001, 3.32212279e-001, 5.17325152e-001],
+ 'fc': [8.36015738e-003, 2.95102022e-002, 4.65928982e-007],
+ 'fd': [3.66668239e-001, 1.37171827e+000, 3.75768025e+004]},
+ 'Li': {'Z': 3, 'chisq': 0.286232,
+ 'bond_length': [1.76, 1.56, 1.82, 0],
+ 'fa': [7.45843816e-002, 7.15382250e-002, 1.45315229e-001],
+ 'fb': [8.81151424e-001, 4.59142904e-002, 8.81301714e-001],
+ 'fc': [1.12125769e+000, 2.51736525e-003, 3.58434971e-001],
+ 'fd': [1.88483665e+001, 1.59189995e-001, 6.12371000e+000]},
+ 'Be': {'Z': 4, 'chisq': 0.195442,
+ 'bond_length': [1.33, 1.13, 1.70, 0],
+ 'fa': [6.11642897e-002, 1.25755034e-001, 2.00831548e-001],
+ 'fb': [9.90182132e-002, 9.90272412e-002, 1.87392509e+000],
+ 'fc': [7.87242876e-001, 1.58847850e-003, 2.73962031e-001],
+ 'fd': [9.32794929e+000, 8.91900236e-002, 3.20687658e+000]},
+ 'B': {'Z': 5, 'chisq': 0.146989,
+ 'bond_length': [1.18, 0.98, 2.08, 0],
+ 'fa': [1.25716066e-001, 1.73314452e-001, 1.84774811e-001],
+ 'fb': [1.48258830e-001, 1.48257216e-001, 3.34227311e+000],
+ 'fc': [1.95250221e-001, 5.29642075e-001, 1.08230500e-003],
+ 'fd': [1.97339463e+000, 5.70035553e+000, 5.64857237e-002]},
+ 'C': {'Z': 6, 'chisq': 0.102440,
+ 'bond_length': [1.12, 0.92, 1.95, 0],
+ 'fa': [2.12080767e-001, 1.99811865e-001, 1.68254385e-001],
+ 'fb': [2.08605417e-001, 2.08610186e-001, 5.57870773e+000],
+ 'fc': [1.42048360e-001, 3.63830672e-001, 8.35012044e-004],
+ 'fd': [1.33311887e+000, 3.80800263e+000, 4.03982620e-002]},
+ 'N': {'Z': 7, 'chisq': 0.060249,
+ 'bond_length': [1.08, 0.88, 1.85, 1.30],
+ 'fa': [5.33015554e-001, 5.29008883e-002, 9.24159648e-002],
+ 'fb': [2.90952515e-001, 1.03547896e+001, 1.03540028e+001],
+ 'fc': [2.61799101e-001, 8.80262108e-004, 1.10166555e-001],
+ 'fd': [2.76252723e+000, 3.47681236e-002, 9.93421736e-001]},
+ 'O': {'Z': 8, 'chisq': 0.039944,
+ 'bond_length': [1.09, 0.89, 1.70, 1.40],
+ 'fa': [3.39969204e-001, 3.07570172e-001, 1.30369072e-001],
+ 'fb': [3.81570280e-001, 3.81571436e-001, 1.91919745e+001],
+ 'fc': [8.83326058e-002, 1.96586700e-001, 9.96220028e-004],
+ 'fd': [7.60635525e-001, 2.07401094e+000, 3.03266869e-002]},
+ 'F': {'Z': 9, 'chisq': 0.027866,
+ 'bond_length': [1.30, 1.10, 1.73, 0],
+ 'fa': [2.30560593e-001, 5.26889648e-001, 1.24346755e-001],
+ 'fb': [4.80754213e-001, 4.80763895e-001, 3.95306720e+001],
+ 'fc': [1.24616894e-003, 7.20452555e-002, 1.53075777e-001],
+ 'fd': [2.62181803e-002, 5.92495593e-001, 1.59127671e+000]},
+ 'Ne': {'Z': 10, 'chisq': 0.021836,
+ 'bond_length': [1.50, 1.30, 1.54, 0],
+ 'fa': [4.08371771e-001, 4.54418858e-001, 1.44564923e-001],
+ 'fb': [5.88228627e-001, 5.88288655e-001, 1.21246013e+002],
+ 'fc': [5.91531395e-002, 1.24003718e-001, 1.64986037e-003],
+ 'fd': [4.63963540e-001, 1.23413025e+000, 2.05869217e-002]},
+ 'Na': {'Z': 11, 'chisq': 0.064136,
+ 'bond_length': [2.10, 1.91, 2.27, 0],
+ 'fa': [1.36471662e-001, 7.70677865e-001, 1.56862014e-001],
+ 'fb': [4.99965301e-002, 8.81899664e-001, 1.61768579e+001],
+ 'fc': [9.96821513e-001, 3.80304670e-002, 1.27685089e-001],
+ 'fd': [2.00132610e+001, 2.60516254e-001, 6.99559329e-001]},
+ 'Mg': {'Z': 12, 'chisq': 0.051303,
+ 'bond_length': [1.80, 1.60, 1.73, 0],
+ 'fa': [3.04384121e-001, 7.56270563e-001, 1.01164809e-001],
+ 'fb': [8.42014377e-002, 1.64065598e+000, 2.97142975e+001],
+ 'fc': [3.45203403e-002, 9.71751327e-001, 1.20593012e-001],
+ 'fd': [2.16596094e-001, 1.21236852e+001, 5.60865838e-001]},
+ 'Al': {'Z': 13, 'chisq': 0.049529,
+ 'bond_length': [1.60, 1.43, 2.05, 0],
+ 'fa': [7.77419424e-001, 5.78312036e-002, 4.26386499e-001],
+ 'fb': [2.71058227e+000, 7.17532098e+001, 9.13331555e-002],
+ 'fc': [1.13407220e-001, 7.90114035e-001, 3.23293496e-002],
+ 'fd': [4.48867451e-001, 8.66366718e+000, 1.78503463e-001]},
+ 'Si': {'Z': 14, 'chisq': 0.071667,
+ 'bond_length': [1.52, 1.32, 2.10, 0],
+ 'fa': [1.06543892e+000, 1.20143691e-001, 1.80915263e-001],
+ 'fb': [1.04118455e+000, 6.87113368e+001, 8.87533926e-002],
+ 'fc': [1.12065620e+000, 3.05452816e-002, 1.59963502e+000],
+ 'fd': [3.70062619e+000, 2.14097897e-001, 9.99096638e+000]},
+ 'P': {'Z': 15, 'chisq': 0.047673,
+ 'bond_length': [1.48, 1.28, 2.08, 0],
+ 'fa': [1.05284447e+000, 2.99440284e-001, 1.17460748e-001],
+ 'fb': [1.31962590e+000, 1.28460520e-001, 1.02190163e+002],
+ 'fc': [9.60643452e-001, 2.63555748e-002, 1.38059330e+000],
+ 'fd': [2.87477555e+000, 1.82076844e-001, 7.49165526e+000]},
+ 'S': {'Z': 16, 'chisq': 0.033482,
+ 'bond_length': [1.47, 1.27, 2.00, 0],
+ 'fa': [1.01646916e+000, 4.41766748e-001, 1.21503863e-001],
+ 'fb': [1.69181965e+000, 1.74180288e-001, 1.67011091e+002],
+ 'fc': [8.27966670e-001, 2.33022533e-002, 1.18302846e+000],
+ 'fd': [2.30342810e+000, 1.56954150e-001, 5.85782891e+000]},
+ 'Cl': {'Z': 17, 'chisq': 0.206186,
+ 'bond_length': [1.70, 1.50, 1.97, 0],
+ 'fa': [9.44221116e-001, 4.37322049e-001, 2.54547926e-001],
+ 'fb': [2.40052374e-001, 9.30510439e+000, 9.30486346e+000],
+ 'fc': [5.47763323e-002, 8.00087488e-001, 1.07488641e-002],
+ 'fd': [1.68655688e-001, 2.97849774e+000, 6.84240646e-002]},
+ 'Ar': {'Z': 18, 'chisq': 0.263904,
+ 'bond_length': [2.00, 1.80, 1.88, 0],
+ 'fa': [1.06983288e+000, 4.24631786e-001, 2.43897949e-001],
+ 'fb': [2.87791022e-001, 1.24156957e+001, 1.24158868e+001],
+ 'fc': [4.79446296e-002, 7.64958952e-001, 8.23128431e-003],
+ 'fd': [1.36979796e-001, 2.43940729e+000, 5.27258749e-002]},
+ 'K': {'Z': 19, 'chisq': 0.161900,
+ 'bond_length': [2.58, 2.38, 2.75, 0],
+ 'fa': [6.92717865e-001, 9.65161085e-001, 1.48466588e-001],
+ 'fb': [7.10849990e+000, 3.57532901e-001, 3.93763275e-002],
+ 'fc': [2.64645027e-002, 1.80883768e+000, 5.43900018e-001],
+ 'fd': [1.03591321e-001, 3.22845199e+001, 1.67791374e+000]},
+ 'Ca': {'Z': 20, 'chisq': 0.085209,
+ 'bond_length': [2.17, 1.97, 1.97, 0],
+ 'fa': [3.66902871e-001, 8.66378999e-001, 6.67203300e-001],
+ 'fb': [6.14274129e-002, 5.70881727e-001, 7.82965639e+000],
+ 'fc': [4.87743636e-001, 1.82406314e+000, 2.20248453e-002],
+ 'fd': [1.32531318e+000, 2.10056032e+001, 9.11853450e-002]},
+ 'Sc': {'Z': 21, 'chisq': 0.052352,
+ 'bond_length': [1.84, 1.64, 1.70, 0],
+ 'fa': [3.78871777e-001, 9.00022505e-001, 7.15288914e-001],
+ 'fb': [6.98910162e-002, 5.21061541e-001, 7.87707920e+000],
+ 'fc': [1.88640973e-002, 4.07945949e-001, 1.61786540e+000],
+ 'fd': [8.17512708e-002, 1.11141388e+000, 1.80840759e+001]},
+ 'Ti': {'Z': 22, 'chisq': 0.035298,
+ 'bond_length': [1.66, 1.46, 1.70, 0],
+ 'fa': [3.62383267e-001, 9.84232966e-001, 7.41715642e-001],
+ 'fb': [7.54707114e-002, 4.97757309e-001, 8.17659391e+000],
+ 'fc': [3.62555269e-001, 1.49159390e+000, 1.61659509e-002],
+ 'fd': [9.55524906e-001, 1.62221677e+001, 7.33140839e-002]},
+ 'V': {'Z': 23, 'chisq': 0.030745,
+ 'bond_length': [1.55, 1.35, 1.70, 0],
+ 'fa': [3.52961378e-001, 7.46791014e-001, 1.08364068e+000],
+ 'fb': [8.19204103e-002, 8.81189511e+000, 5.10646075e-001],
+ 'fc': [1.39013610e+000, 3.31273356e-001, 1.40422612e-002],
+ 'fd': [1.48901841e+001, 8.38543079e-001, 6.57432678e-002]},
+ 'Cr': {'Z': 24, 'chisq': 0.015287,
+ 'bond_length': [1.56, 1.36, 1.70, 0],
+ 'fa': [1.34348379e+000, 5.07040328e-001, 4.26358955e-001],
+ 'fb': [1.25814353e+000, 1.15042811e+001, 8.53660389e-002],
+ 'fc': [1.17241826e-002, 5.11966516e-001, 3.38285828e-001],
+ 'fd': [6.00177061e-002, 1.53772451e+000, 6.62418319e-001]},
+ 'Mn': {'Z': 25, 'chisq': 0.031274,
+ 'bond_length': [1.54, 1.30, 1.70, 0],
+ 'fa': [3.26697613e-001, 7.17297000e-001, 1.33212464e+000],
+ 'fb': [8.88813083e-002, 1.11300198e+001, 5.82141104e-001],
+ 'fc': [2.80801702e-001, 1.15499241e+000, 1.11984488e-002],
+ 'fd': [6.71583145e-001, 1.26825395e+001, 5.32334467e-002]},
+ 'Fe': {'Z': 26, 'chisq': 0.031315,
+ 'bond_length': [1.47, 1.27, 1.70, 0],
+ 'fa': [3.13454847e-001, 6.89290016e-001, 1.47141531e+000],
+ 'fb': [8.99325756e-002, 1.30366038e+001, 6.33345291e-001],
+ 'fc': [1.03298688e+000, 2.58280285e-001, 1.03460690e-002],
+ 'fd': [1.16783425e+001, 6.09116446e-001, 4.81610627e-002]},
+ 'Co': {'Z': 27, 'chisq': 0.031643,
+ 'bond_length': [1.45, 1.25, 1.70, 0],
+ 'fa': [3.15878278e-001, 1.60139005e+000, 6.56394338e-001],
+ 'fb': [9.46683246e-002, 6.99436449e-001, 1.56954403e+001],
+ 'fc': [9.36746624e-001, 9.77562646e-003, 2.38378578e-001],
+ 'fd': [1.09392410e+001, 4.37446816e-002, 5.56286483e-001]},
+ 'Ni': {'Z': 28, 'chisq': 0.032245,
+ 'bond_length': [1.45, 1.25, 1.63, 0],
+ 'fa': [1.72254630e+000, 3.29543044e-001, 6.23007200e-001],
+ 'fb': [7.76606908e-001, 1.02262360e-001, 1.94156207e+001],
+ 'fc': [9.43496513e-003, 8.54063515e-001, 2.21073515e-001],
+ 'fd': [3.98684596e-002, 1.04078166e+001, 5.10869330e-001]},
+ 'Cu': {'Z': 29, 'chisq': 0.010467,
+ 'bond_length': [1.48, 1.28, 1.40, 0],
+ 'fa': [3.58774531e-001, 1.76181348e+000, 6.36905053e-001],
+ 'fb': [1.06153463e-001, 1.01640995e+000, 1.53659093e+001],
+ 'fc': [7.44930667e-003, 1.89002347e-001, 2.29619589e-001],
+ 'fd': [3.85345989e-002, 3.98427790e-001, 9.01419843e-001]},
+ 'Zn': {'Z': 30, 'chisq': 0.026698,
+ 'bond_length': [1.59, 1.39, 1.39, 0],
+ 'fa': [5.70893973e-001, 1.98908856e+000, 3.06060585e-001],
+ 'fb': [1.26534614e-001, 2.17781965e+000, 3.78619003e+001],
+ 'fc': [2.35600223e-001, 3.97061102e-001, 6.85657228e-003],
+ 'fd': [3.67019041e-001, 8.66419596e-001, 3.35778823e-002]},
+ 'Ga': {'Z': 31, 'chisq': 0.008110,
+ 'bond_length': [1.61, 1.41, 1.87, 0],
+ 'fa': [6.25528464e-001, 2.05302901e+000, 2.89608120e-001],
+ 'fb': [1.10005650e-001, 2.41095786e+000, 4.78685736e+001],
+ 'fc': [2.07910594e-001, 3.45079617e-001, 6.55634298e-003],
+ 'fd': [3.27807224e-001, 7.43139061e-001, 3.09411369e-002]},
+ 'Ge': {'Z': 32, 'chisq': 0.032198,
+ 'bond_length': [1.57, 1.37, 1.70, 0],
+ 'fa': [5.90952690e-001, 5.39980660e-001, 2.00626188e+000],
+ 'fb': [1.18375976e-001, 7.18937433e+001, 1.39304889e+000],
+ 'fc': [7.49705041e-001, 1.83581347e-001, 9.52190743e-003],
+ 'fd': [6.89943350e+000, 3.64667232e-001, 2.69888650e-002]},
+ 'As': {'Z': 33, 'chisq': 0.034014,
+ 'bond_length': [1.59, 1.39, 1.85, 0],
+ 'fa': [7.77875218e-001, 5.93848150e-001, 1.95918751e+000],
+ 'fb': [1.50733157e-001, 1.42882209e+002, 1.74750339e+000],
+ 'fc': [1.79880226e-001, 8.63267222e-001, 9.59053427e-003],
+ 'fd': [3.31800852e-001, 5.85490274e+000, 2.33777569e-002]},
+ 'Se': {'Z': 34, 'chisq': 0.035703,
+ 'bond_length': [1.60, 1.40, 1.90, 0],
+ 'fa': [9.58390681e-001, 6.03851342e-001, 1.90828931e+000],
+ 'fb': [1.83775557e-001, 1.96819224e+002, 2.15082053e+000],
+ 'fc': [1.73885956e-001, 9.35265145e-001, 8.62254658e-003],
+ 'fd': [3.00006024e-001, 4.92471215e+000, 2.12308108e-002]},
+ 'Br': {'Z': 35, 'chisq': 0.039250,
+ 'bond_length': [1.80, 1.60, 2.10, 0],
+ 'fa': [1.14136170e+000, 5.18118737e-001, 1.85731975e+000],
+ 'fb': [2.18708710e-001, 1.93916682e+002, 2.65755396e+000],
+ 'fc': [1.68217399e-001, 9.75705606e-001, 7.24187871e-003],
+ 'fd': [2.71719918e-001, 4.19482500e+000, 1.99325718e-002]},
+ 'Kr': {'Z': 36, 'chisq': 0.045421,
+ 'bond_length': [2.10, 1.90, 2.02, 0],
+ 'fa': [3.24386970e-001, 1.31732163e+000, 1.79912614e+000],
+ 'fb': [6.31317973e+001, 2.54706036e-001, 3.23668394e+000],
+ 'fc': [4.29961425e-003, 1.00429433e+000, 1.62188197e-001],
+ 'fd': [1.98965610e-002, 3.61094513e+000, 2.45583672e-001]},
+ 'Rb': {'Z': 37, 'chisq': 0.130044,
+ 'bond_length': [2.75, 2.55, 1.70, 0],
+ 'fa': [2.90445351e-001, 2.44201329e+000, 7.69435449e-001],
+ 'fb': [3.68420227e-002, 1.16013332e+000, 1.69591472e+001],
+ 'fc': [1.58687000e+000, 2.81617593e-003, 1.28663830e-001],
+ 'fd': [2.53082574e+000, 1.88577417e-002, 2.10753969e-001]},
+ 'Sr': {'Z': 38, 'chisq': 0.188055,
+ 'bond_length': [2.35, 2.15, 1.70, 0],
+ 'fa': [1.37373086e-002, 1.97548672e+000, 1.59261029e+000],
+ 'fb': [1.87469061e-002, 6.36079230e+000, 2.21992482e-001],
+ 'fc': [1.73263882e-001, 4.66280378e+000, 1.61265063e-003],
+ 'fd': [2.01624958e-001, 2.53027803e+001, 1.53610568e-002]},
+ 'Y': {'Z': 39, 'chisq': 0.174927,
+ 'bond_length': [2.00, 1.80, 1.70, 0],
+ 'fa': [6.75302747e-001, 4.70286720e-001, 2.63497677e+000],
+ 'fb': [6.54331847e-002, 1.06108709e+002, 2.06643540e+000],
+ 'fc': [1.09621746e-001, 9.60348773e-001, 5.28921555e-003],
+ 'fd': [1.93131925e-001, 1.63310938e+000, 1.66083821e-002]},
+ 'Zr': {'Z': 40, 'chisq': 0.072078,
+ 'bond_length': [1.80, 1.60, 1.70, 0],
+ 'fa': [2.64365505e+000, 5.54225147e-001, 7.61376625e-001],
+ 'fb': [2.20202699e+000, 1.78260107e+002, 7.67218745e-002],
+ 'fc': [6.02946891e-003, 9.91630530e-002, 9.56782020e-001],
+ 'fd': [1.55143296e-002, 1.76175995e-001, 1.54330682e+000]},
+ 'Nb': {'Z': 41, 'chisq': 0.011800,
+ 'bond_length': [1.67, 1.47, 1.70, 0],
+ 'fa': [6.59532875e-001, 1.84545854e+000, 1.25584405e+000],
+ 'fb': [8.66145490e-002, 5.94774398e+000, 6.40851475e-001],
+ 'fc': [1.22253422e-001, 7.06638328e-001, 2.62381591e-003],
+ 'fd': [1.66646050e-001, 1.62853268e+000, 8.26257859e-003]},
+ 'Mo': {'Z': 42, 'chisq': 0.008976,
+ 'bond_length': [1.60, 1.40, 1.70, 0],
+ 'fa': [6.10160120e-001, 1.26544000e+000, 1.97428762e+000],
+ 'fb': [9.11628054e-002, 5.06776025e-001, 5.89590381e+000],
+ 'fc': [6.48028962e-001, 2.60380817e-003, 1.13887493e-001],
+ 'fd': [1.46634108e+000, 7.84336311e-003, 1.55114340e-001]},
+ 'Tc': {'Z': 43, 'chisq': 0.023771,
+ 'bond_length': [1.56, 1.36, 1.70, 0],
+ 'fa': [8.55189183e-001, 1.66219641e+000, 1.45575475e+000],
+ 'fb': [1.02962151e-001, 7.64907000e+000, 1.01639987e+000],
+ 'fc': [1.05445664e-001, 7.71657112e-001, 2.20992635e-003],
+ 'fd': [1.42303338e-001, 1.34659349e+000, 7.90358976e-003]},
+ 'Ru': {'Z': 44, 'chisq': 0.010613,
+ 'bond_length': [1.54, 1.34, 1.70, 0],
+ 'fa': [4.70847093e-001, 1.58180781e+000, 2.02419818e+000],
+ 'fb': [9.33029874e-002, 4.52831347e-001, 7.11489023e+000],
+ 'fc': [1.97036257e-003, 6.26912639e-001, 1.02641320e-001],
+ 'fd': [7.56181595e-003, 1.25399858e+000, 1.33786087e-001]},
+ 'Rh': {'Z': 45, 'chisq': 0.012895,
+ 'bond_length': [1.54, 1.34, 1.70, 0],
+ 'fa': [4.20051553e-001, 1.76266507e+000, 2.02735641e+000],
+ 'fb': [9.38882628e-002, 4.64441687e-001, 8.19346046e+000],
+ 'fc': [1.45487176e-003, 6.22809600e-001, 9.91529915e-002],
+ 'fd': [7.82704517e-003, 1.17194153e+000, 1.24532839e-001]},
+ 'Pd': {'Z': 46, 'chisq': 0.009172,
+ 'bond_length': [1.58, 1.38, 1.63, 0],
+ 'fa': [2.10475155e+000, 2.03884487e+000, 1.82067264e-001],
+ 'fb': [8.68606470e+000, 3.78924449e-001, 1.42921634e-001],
+ 'fc': [9.52040948e-002, 5.91445248e-001, 1.13328676e-003],
+ 'fd': [1.17125900e-001, 1.07843808e+000, 7.80252092e-003]},
+ 'Ag': {'Z': 47, 'chisq': 0.006648,
+ 'bond_length': [1.64, 1.44, 1.72, 0],
+ 'fa': [2.07981390e+000, 4.43170726e-001, 1.96515215e+000],
+ 'fb': [9.92540297e+000, 1.04920104e-001, 6.40103839e-001],
+ 'fc': [5.96130591e-001, 4.78016333e-001, 9.46458470e-002],
+ 'fd': [8.89594790e-001, 1.98509407e+000, 1.12744464e-001]},
+ 'Cd': {'Z': 48, 'chisq': 0.005588,
+ 'bond_length': [1.77, 1.57, 1.58, 0],
+ 'fa': [1.63657549e+000, 2.17927989e+000, 7.71300690e-001],
+ 'fb': [1.24540381e+001, 1.45134660e+000, 1.26695757e-001],
+ 'fc': [6.64193880e-001, 7.64563285e-001, 8.61126689e-002],
+ 'fd': [7.77659202e-001, 1.66075210e+000, 1.05728357e-001]},
+ 'In': {'Z': 49, 'chisq': 0.002569,
+ 'bond_length': [1.86, 1.66, 1.93, 0],
+ 'fa': [2.24820632e+000, 1.64706864e+000, 7.88679265e-001],
+ 'fb': [1.51913507e+000, 1.30113424e+001, 1.06128184e-001],
+ 'fc': [8.12579069e-002, 6.68280346e-001, 6.38467475e-001],
+ 'fd': [9.94045620e-002, 1.49742063e+000, 7.18422635e-001]},
+ 'Sn': {'Z': 50, 'chisq': 0.005051,
+ 'bond_length': [1.82, 1.62, 2.17, 0],
+ 'fa': [2.16644620e+000, 6.88691021e-001, 1.92431751e+000],
+ 'fb': [1.13174909e+001, 1.10131285e-001, 6.74464853e-001],
+ 'fc': [5.65359888e-001, 9.18683861e-001, 7.80542213e-002],
+ 'fd': [7.33564610e-001, 1.02310312e+001, 9.31104308e-002]},
+ 'Sb': {'Z': 51, 'chisq': 0.004383,
+ 'bond_length': [1.79, 1.59, 2.20, 0],
+ 'fa': [1.73662114e+000, 9.99871380e-001, 2.13972409e+000],
+ 'fb': [8.84334719e-001, 1.38462121e-001, 1.19666432e+001],
+ 'fc': [5.60566526e-001, 9.93772747e-001, 7.37374982e-002],
+ 'fd': [6.72672880e-001, 8.72330411e+000, 8.78577715e-002]},
+ 'Te': {'Z': 52, 'chisq': 0.004105,
+ 'bond_length': [1.80, 1.60, 2.06, 0],
+ 'fa': [2.09383882e+000, 1.56940519e+000, 1.30941993e+000],
+ 'fb': [1.26856869e+001, 1.21236537e+000, 1.66633292e-001],
+ 'fc': [6.98067804e-002, 1.04969537e+000, 5.55594354e-001],
+ 'fd': [8.30817576e-002, 7.43147857e+000, 6.17487676e-001]},
+ 'I': {'Z': 53, 'chisq': 0.004068,
+ 'bond_length': [1.90, 1.70, 2.15, 0],
+ 'fa': [1.60186925e+000, 1.98510264e+000, 1.48226200e+000],
+ 'fb': [1.95031538e-001, 1.36976183e+001, 1.80304795e+000],
+ 'fc': [5.53807199e-001, 1.11728722e+000, 6.60720847e-002],
+ 'fd': [5.67912340e-001, 6.40879878e+000, 7.86615429e-002]},
+ 'Xe': {'Z': 54, 'chisq': 0.004381,
+ 'bond_length': [2.30, 2.10, 2.16, 0],
+ 'fa': [1.60015487e+000, 1.71644581e+000, 1.84968351e+000],
+ 'fb': [2.92913354e+000, 1.55882990e+001, 2.22525983e-001],
+ 'fc': [6.23813648e-002, 1.21387555e+000, 5.54051946e-001],
+ 'fd': [7.45581223e-002, 5.56013271e+000, 5.21994521e-001]},
+ 'Cs': {'Z': 55, 'chisq': 0.042676,
+ 'bond_length': [2.93, 2.73, 1.70, 0],
+ 'fa': [2.95236854e+000, 4.28105721e-001, 1.89599233e+000],
+ 'fb': [6.01461952e+000, 4.64151246e+001, 1.80109756e-001],
+ 'fc': [5.48012938e-002, 4.70838600e+000, 5.90356719e-001],
+ 'fd': [7.12799633e-002, 4.56702799e+001, 4.70236310e-001]},
+ 'Ba': {'Z': 56, 'chisq': 0.043267,
+ 'bond_length': [2.44, 2.24, 1.70, 0],
+ 'fa': [3.19434243e+000, 1.98289586e+000, 1.55121052e-001],
+ 'fb': [9.27352241e+000, 2.28741632e-001, 3.82000231e-002],
+ 'fc': [6.73222354e-002, 4.48474211e+000, 5.42674414e-001],
+ 'fd': [7.30961745e-002, 2.95703565e+001, 4.08647015e-001]},
+ 'La': {'Z': 57, 'chisq': 0.033249,
+ 'bond_length': [2.08, 1.88, 1.70, 0],
+ 'fa': [2.05036425e+000, 1.42114311e-001, 3.23538151e+000],
+ 'fb': [2.20348417e-001, 3.96438056e-002, 9.56979169e+000],
+ 'fc': [6.34683429e-002, 3.97960586e+000, 5.20116711e-001],
+ 'fd': [6.92443091e-002, 2.53178406e+001, 3.83614098e-001]},
+ 'Ce': {'Z': 58, 'chisq': 0.029355,
+ 'bond_length': [2.02, 1.82, 1.70, 0],
+ 'fa': [3.22990759e+000, 1.57618307e-001, 2.13477838e+000],
+ 'fb': [9.94660135e+000, 4.15378676e-002, 2.40480572e-001],
+ 'fc': [5.01907609e-001, 3.80889010e+000, 5.96625028e-002],
+ 'fd': [3.66252019e-001, 2.43275968e+001, 6.59653503e-002]},
+ 'Pr': {'Z': 59, 'chisq': 0.029725,
+ 'bond_length': [2.03, 1.83, 1.70, 0],
+ 'fa': [1.58189324e-001, 3.18141995e+000, 2.27622140e+000],
+ 'fb': [3.91309056e-002, 1.04139545e+001, 2.81671757e-001],
+ 'fc': [3.97705472e+000, 5.58448277e-002, 4.85207954e-001],
+ 'fd': [2.61872978e+001, 6.30921695e-002, 3.54234369e-001]},
+ 'Nd': {'Z': 60, 'chisq': 0.027597,
+ 'bond_length': [2.02, 1.82, 1.70, 0],
+ 'fa': [1.81379417e-001, 3.17616396e+000, 2.35221519e+000],
+ 'fb': [4.37324793e-002, 1.07842572e+001, 3.05571833e-001],
+ 'fc': [3.83125763e+000, 5.25889976e-002, 4.70090742e-001],
+ 'fd': [2.54745408e+001, 6.02676073e-002, 3.39017003e-001]},
+ 'Pm': {'Z': 61, 'chisq': 0.025208,
+ 'bond_length': [2.01, 1.81, 1.70, 0],
+ 'fa': [1.92986811e-001, 2.43756023e+000, 3.17248504e+000],
+ 'fb': [4.37785970e-002, 3.29336996e-001, 1.11259996e+001],
+ 'fc': [3.58105414e+000, 4.56529394e-001, 4.94812177e-002],
+ 'fd': [2.46709586e+001, 3.24990282e-001, 5.76553100e-002]},
+ 'Sm': {'Z': 62, 'chisq': 0.023540,
+ 'bond_length': [2.00, 1.80, 1.70, 0],
+ 'fa': [2.12002595e-001, 3.16891754e+000, 2.51503494e+000],
+ 'fb': [4.57703608e-002, 1.14536599e+001, 3.55561054e-001],
+ 'fc': [4.44080845e-001, 3.36742101e+000, 4.65652543e-002],
+ 'fd': [3.11953363e-001, 2.40291435e+001, 5.52266819e-002]},
+ 'Eu': {'Z': 63, 'chisq': 0.022204,
+ 'bond_length': [2.24, 2.04, 1.70, 0],
+ 'fa': [2.59355002e+000, 3.16557522e+000, 2.29402652e-001],
+ 'fb': [3.82452612e-001, 1.17675155e+001, 4.76642249e-002],
+ 'fc': [4.32257780e-001, 3.17261920e+000, 4.37958317e-002],
+ 'fd': [2.99719833e-001, 2.34462738e+001, 5.29440680e-002]},
+ 'Gd': {'Z': 64, 'chisq': 0.017492,
+ 'bond_length': [2.00, 1.80, 1.70, 0],
+ 'fa': [3.19144939e+000, 2.55766431e+000, 3.32681934e-001],
+ 'fb': [1.20224655e+001, 4.08338876e-001, 5.85819814e-002],
+ 'fc': [4.14243130e-002, 2.61036728e+000, 4.20526863e-001],
+ 'fd': [5.06771477e-002, 1.99344244e+001, 2.85686240e-001]},
+ 'Tb': {'Z': 65, 'chisq': 0.020036,
+ 'bond_length': [1.98, 1.78, 1.70, 0],
+ 'fa': [2.59407462e-001, 3.16177855e+000, 2.75095751e+000],
+ 'fb': [5.04689354e-002, 1.23140183e+001, 4.38337626e-001],
+ 'fc': [2.79247686e+000, 3.85931001e-002, 4.10881708e-001],
+ 'fd': [2.23797309e+001, 4.87920992e-002, 2.77622892e-001]},
+ 'Dy': {'Z': 66, 'chisq': 0.019351,
+ 'bond_length': [1.97, 1.77, 1.70, 0],
+ 'fa': [3.16055396e+000, 2.82751709e+000, 2.75140255e-001],
+ 'fb': [1.25470414e+001, 4.67899094e-001, 5.23226982e-002],
+ 'fc': [4.00967160e-001, 2.63110834e+000, 3.61333817e-002],
+ 'fd': [2.67614884e-001, 2.19498166e+001, 4.68871497e-002]},
+ 'Ho': {'Z': 67, 'chisq': 0.018720,
+ 'bond_length': [1.98, 1.78, 1.70, 0],
+ 'fa': [2.88642467e-001, 2.90567296e+000, 3.15960159e+000],
+ 'fb': [5.40507687e-002, 4.97581077e-001, 1.27599505e+001],
+ 'fc': [3.91280259e-001, 2.48596038e+000, 3.37664478e-002],
+ 'fd': [2.58151831e-001, 2.15400972e+001, 4.50664323e-002]},
+ 'Er': {'Z': 68, 'chisq': 0.018677,
+ 'bond_length': [1.96, 1.76, 1.70, 0],
+ 'fa': [3.15573213e+000, 3.11519560e-001, 2.97722406e+000],
+ 'fb': [1.29729009e+001, 5.81399387e-002, 5.31213394e-001],
+ 'fc': [3.81563854e-001, 2.40247532e+000, 3.15224214e-002],
+ 'fd': [2.49195776e-001, 2.13627616e+001, 4.33253257e-002]},
+ 'Tm': {'Z': 69, 'chisq': 0.018176,
+ 'bond_length': [1.95, 1.75, 1.70, 0],
+ 'fa': [3.15591970e+000, 3.22544710e-001, 3.05569053e+000],
+ 'fb': [1.31232407e+001, 5.97223323e-002, 5.61876773e-001],
+ 'fc': [2.92845100e-002, 3.72487205e-001, 2.27833695e+000],
+ 'fd': [4.16534255e-002, 2.40821967e-001, 2.10034185e+001]},
+ 'Yb': {'Z': 70, 'chisq': 0.018460,
+ 'bond_length': [2.10, 1.90, 1.70, 0],
+ 'fa': [3.10794704e+000, 3.14091221e+000, 3.75660454e-001],
+ 'fb': [6.06347847e-001, 1.33705269e+001, 7.29814740e-002],
+ 'fc': [3.61901097e-001, 2.45409082e+000, 2.72383990e-002],
+ 'fd': [2.32652051e-001, 2.12695209e+001, 3.99969597e-002]},
+ 'Lu': {'Z': 71, 'chisq': 0.015021,
+ 'bond_length': [1.93, 1.73, 1.70, 0],
+ 'fa': [3.11446863e+000, 5.39634353e-001, 3.06460915e+000],
+ 'fb': [1.38968881e+001, 8.91708508e-002, 6.79919563e-001],
+ 'fc': [2.58563745e-002, 2.13983556e+000, 3.47788231e-001],
+ 'fd': [3.82808522e-002, 1.80078788e+001, 2.22706591e-001]},
+ 'Hf': {'Z': 72, 'chisq': 0.012070,
+ 'bond_length': [1.78, 1.58, 1.70, 0],
+ 'fa': [3.01166899e+000, 3.16284788e+000, 6.33421771e-001],
+ 'fb': [7.10401889e-001, 1.38262192e+001, 9.48486572e-002],
+ 'fc': [3.41417198e-001, 1.53566013e+000, 2.40723773e-002],
+ 'fd': [2.14129678e-001, 1.55298698e+001, 3.67833690e-002]},
+ 'Ta': {'Z': 73, 'chisq': 0.010775,
+ 'bond_length': [1.67, 1.47, 1.70, 0],
+ 'fa': [3.20236821e+000, 8.30098413e-001, 2.86552297e+000],
+ 'fb': [1.38446369e+001, 1.18381581e-001, 7.66369118e-001],
+ 'fc': [2.24813887e-002, 1.40165263e+000, 3.33740596e-001],
+ 'fd': [3.52934622e-002, 1.46148877e+001, 2.05704486e-001]},
+ 'W': {'Z': 74, 'chisq': 0.009479,
+ 'bond_length': [1.61, 1.41, 1.70, 0],
+ 'fa': [9.24906855e-001, 2.75554557e+000, 3.30440060e+000],
+ 'fb': [1.28663377e-001, 7.65826479e-001, 1.34471170e+001],
+ 'fc': [3.29973862e-001, 1.09916444e+000, 2.06498883e-002],
+ 'fd': [1.98218895e-001, 1.35087534e+001, 3.38918459e-002]},
+ 'Re': {'Z': 75, 'chisq': 0.004620,
+ 'bond_length': [1.58, 1.38, 1.70, 0],
+ 'fa': [1.96952105e+000, 1.21726619e+000, 4.10391685e+000],
+ 'fb': [4.98830620e+001, 1.33243809e-001, 1.84396916e+000],
+ 'fc': [2.90791978e-002, 2.30696669e-001, 6.08840299e-001],
+ 'fd': [2.84192813e-002, 1.90968784e-001, 1.37090356e+000]},
+ 'Os': {'Z': 76, 'chisq': 0.003085,
+ 'bond_length': [1.55, 1.35, 1.70, 0],
+ 'fa': [2.06385867e+000, 1.29603406e+000, 3.96920673e+000],
+ 'fb': [4.05671697e+001, 1.46559047e-001, 1.82561596e+000],
+ 'fc': [2.69835487e-002, 2.31083999e-001, 6.30466774e-001],
+ 'fd': [2.84172045e-002, 1.79765184e-001, 1.38911543e+000]},
+ 'Ir': {'Z': 77, 'chisq': 0.003924,
+ 'bond_length': [1.56, 1.36, 1.70, 0],
+ 'fa': [2.21522726e+000, 1.37573155e+000, 3.78244405e+000],
+ 'fb': [3.24464090e+001, 1.60920048e-001, 1.78756553e+000],
+ 'fc': [2.44643240e-002, 2.36932016e-001, 6.48471412e-001],
+ 'fd': [2.82909938e-002, 1.70692368e-001, 1.37928390e+000]},
+ 'Pt': {'Z': 78, 'chisq': 0.003817,
+ 'bond_length': [1.59, 1.39, 1.72, 0],
+ 'fa': [9.84697940e-001, 2.73987079e+000, 3.61696715e+000],
+ 'fb': [1.60910839e-001, 7.18971667e-001, 1.29281016e+001],
+ 'fc': [3.02885602e-001, 2.78370726e-001, 1.52124129e-002],
+ 'fd': [1.70134854e-001, 1.49862703e+000, 2.83510822e-002]},
+ 'Au': {'Z': 79, 'chisq': 0.003143,
+ 'bond_length': [1.64, 1.44, 1.66, 0],
+ 'fa': [9.61263398e-001, 3.69581030e+000, 2.77567491e+000],
+ 'fb': [1.70932277e-001, 1.29335319e+001, 6.89997070e-001],
+ 'fc': [2.95414176e-001, 3.11475743e-001, 1.43237267e-002],
+ 'fd': [1.63525510e-001, 1.39200901e+000, 2.71265337e-002]},
+ 'Hg': {'Z': 80, 'chisq': 0.002717,
+ 'bond_length': [1.77, 1.57, 1.55, 0],
+ 'fa': [1.29200491e+000, 2.75161478e+000, 3.49387949e+000],
+ 'fb': [1.83432865e-001, 9.42368371e-001, 1.46235654e+001],
+ 'fc': [2.77304636e-001, 4.30232810e-001, 1.48294351e-002],
+ 'fd': [1.55110144e-001, 1.28871670e+000, 2.61903834e-002]},
+ 'Tl': {'Z': 81, 'chisq': 0.003492,
+ 'bond_length': [1.92, 1.72, 1.96, 0],
+ 'fa': [3.75964730e+000, 3.21195904e+000, 6.47767825e-001],
+ 'fb': [1.35041513e+001, 6.66330993e-001, 9.22518234e-002],
+ 'fc': [2.76123274e-001, 3.18838810e-001, 1.31668419e-002],
+ 'fd': [1.50312897e-001, 1.12565588e+000, 2.48879842e-002]},
+ 'Pb': {'Z': 82, 'chisq': 0.001158,
+ 'bond_length': [1.95, 1.75, 2.02, 0],
+ 'fa': [1.00795975e+000, 3.09796153e+000, 3.61296864e+000],
+ 'fb': [1.17268427e-001, 8.80453235e-001, 1.47325812e+001],
+ 'fc': [2.62401476e-001, 4.05621995e-001, 1.31812509e-002],
+ 'fd': [1.43491014e-001, 1.04103506e+000, 2.39575415e-002]},
+ 'Bi': {'Z': 83, 'chisq': 0.026436,
+ 'bond_length': [1.90, 1.70, 1.70, 0],
+ 'fa': [1.59826875e+000, 4.38233925e+000, 2.06074719e+000],
+ 'fb': [1.56897471e-001, 2.47094692e+000, 5.72438972e+001],
+ 'fc': [1.94426023e-001, 8.22704978e-001, 2.33226953e-002],
+ 'fd': [1.32979109e-001, 9.56532528e-001, 2.23038435e-002]},
+ 'Po': {'Z': 84, 'chisq': 0.008962,
+ 'bond_length': [1.96, 1.76, 1.70, 0],
+ 'fa': [1.71463223e+000, 2.14115960e+000, 4.37512413e+000],
+ 'fb': [9.79262841e+001, 2.10193717e-001, 3.66948812e+000],
+ 'fc': [2.16216680e-002, 1.97843837e-001, 6.52047920e-001],
+ 'fd': [1.98456144e-002, 1.33758807e-001, 7.80432104e-001]},
+ 'At': {'Z': 85, 'chisq': 0.033776,
+ 'bond_length': [2.00, 1.80, 1.70, 0],
+ 'fa': [1.48047794e+000, 2.09174630e+000, 4.75246033e+000],
+ 'fb': [1.25943919e+002, 1.83803008e-001, 4.19890596e+000],
+ 'fc': [1.85643958e-002, 2.05859375e-001, 7.13540948e-001],
+ 'fd': [1.81383503e-002, 1.33035404e-001, 7.03031938e-001]},
+ 'Rn': {'Z': 86, 'chisq': 0.050132,
+ 'bond_length': [2.40, 2.20, 1.70, 0],
+ 'fa': [6.30022295e-001, 3.80962881e+000, 3.89756067e+000],
+ 'fb': [1.40909762e-001, 3.08515540e+001, 6.51559763e-001],
+ 'fc': [2.40755100e-001, 2.62868577e+000, 3.14285931e-002],
+ 'fd': [1.08899672e-001, 6.42383261e+000, 2.42346699e-002]},
+ 'Fr': {'Z': 87, 'chisq': 0.056720,
+ 'bond_length': [3.00, 2.80, 1.70, 0],
+ 'fa': [5.23288135e+000, 2.48604205e+000, 3.23431354e-001],
+ 'fb': [8.60599536e+000, 3.04543982e-001, 3.87759096e-002],
+ 'fc': [2.55403596e-001, 5.53607228e-001, 5.75278889e-003],
+ 'fd': [1.28717724e-001, 5.36977452e-001, 1.29417790e-002]},
+ 'Ra': {'Z': 88, 'chisq': 0.081498,
+ 'bond_length': [2.46, 2.26, 1.70, 0],
+ 'fa': [1.44192685e+000, 3.55291725e+000, 3.91259586e+000],
+ 'fb': [1.18740873e-001, 1.01739750e+000, 6.31814783e+001],
+ 'fc': [2.16173519e-001, 3.94191605e+000, 4.60422605e-002],
+ 'fd': [9.55806441e-002, 3.50602732e+001, 2.20850385e-002]},
+ 'Ac': {'Z': 89, 'chisq': 0.077643,
+ 'bond_length': [2.09, 1.88, 1.70, 0],
+ 'fa': [1.45864127e+000, 4.18945405e+000, 3.65866182e+000],
+ 'fb': [1.07760494e-001, 8.89090649e+001, 1.05088931e+000],
+ 'fc': [2.08479229e-001, 3.16528117e+000, 5.23892556e-002],
+ 'fd': [9.09335557e-002, 3.13297788e+001, 2.08807697e-002]},
+ 'Th': {'Z': 90, 'chisq': 0.048096,
+ 'bond_length': [2.00, 1.80, 1.70, 0],
+ 'fa': [1.19014064e+000, 2.55380607e+000, 4.68110181e+000],
+ 'fb': [7.73468729e-002, 6.59693681e-001, 1.28013896e+001],
+ 'fc': [2.26121303e-001, 3.58250545e-001, 7.82263950e-003],
+ 'fd': [1.08632194e-001, 4.56765664e-001, 1.62623474e-002]},
+ 'Pa': {'Z': 91, 'chisq': 0.070186,
+ 'bond_length': [1.83, 1.63, 1.70, 0],
+ 'fa': [4.68537504e+000, 2.98413708e+000, 8.91988061e-001],
+ 'fb': [1.44503632e+001, 5.56438592e-001, 6.69512914e-002],
+ 'fc': [2.24825384e-001, 3.04444846e-001, 9.48162708e-003],
+ 'fd': [1.03235396e-001, 4.27255647e-001, 1.77730611e-002]},
+ 'U': {'Z': 92, 'chisq': 0.072478,
+ 'bond_length': [1.76, 1.56, 1.86, 0],
+ 'fa': [4.63343606e+000, 3.18157056e+000, 8.76455075e-001],
+ 'fb': [1.63377267e+001, 5.69517868e-001, 6.88860012e-002],
+ 'fc': [2.21685477e-001, 2.72917100e-001, 1.11737298e-002],
+ 'fd': [9.84254550e-002, 4.09470917e-001, 1.86215410e-002]},
+ 'Np': {'Z': 93, 'chisq': 0.074792,
+ 'bond_length': [1.80, 1.60, 1.70, 0],
+ 'fa': [4.56773888e+000, 3.40325179e+000, 8.61841923e-001],
+ 'fb': [1.90992795e+001, 5.90099634e-001, 7.03204851e-002],
+ 'fc': [2.19728870e-001, 2.38176903e-001, 1.38306499e-002],
+ 'fd': [9.36334280e-002, 3.93554882e-001, 1.94437286e-002]},
+ 'Pu': {'Z': 94, 'chisq': 0.071877,
+ 'bond_length': [1.84, 1.64, 1.70, 0],
+ 'fa': [5.45671123e+000, 1.11687906e-001, 3.30260343e+000],
+ 'fb': [1.01892720e+001, 3.98131313e-002, 3.14622212e-001],
+ 'fc': [1.84568319e-001, 4.93644263e-001, 3.57484743e+000],
+ 'fd': [1.04220860e-001, 4.63080540e-001, 2.19369542e+001]},
+ 'Am': {'Z': 95, 'chisq': 0.062156,
+ 'bond_length': [2.01, 1.81, 1.70, 0],
+ 'fa': [5.38321999e+000, 1.23343236e-001, 3.46469090e+000],
+ 'fb': [1.07289857e+001, 4.15137806e-002, 3.39326208e-001],
+ 'fc': [1.75437132e-001, 3.39800073e+000, 4.69459519e-001],
+ 'fd': [9.98932346e-002, 2.11601535e+001, 4.51996970e-001]},
+ 'Cm': {'Z': 96, 'chisq': 0.050111,
+ 'bond_length': [2.20, 2.00, 1.70, 0],
+ 'fa': [5.38402377e+000, 3.49861264e+000, 1.88039547e-001],
+ 'fb': [1.11211419e+001, 3.56750210e-001, 5.39853583e-002],
+ 'fc': [1.69143137e-001, 3.19595016e+000, 4.64393059e-001],
+ 'fd': [9.60082633e-002, 1.80694389e+001, 4.36318197e-001]},
+ 'Bk': {'Z': 97, 'chisq': 0.044081,
+ 'bond_length': [2.20, 2.00, 1.70, 0],
+ 'fa': [3.66090688e+000, 2.03054678e-001, 5.30697515e+000],
+ 'fb': [3.84420906e-001, 5.48547131e-002, 1.17150262e+001],
+ 'fc': [1.60934046e-001, 3.04808401e+000, 4.43610295e-001],
+ 'fd': [9.21020329e-002, 1.73525367e+001, 4.27132359e-001]},
+ 'Cf': {'Z': 98, 'chisq': 0.041053,
+ 'bond_length': [2.20, 2.00, 1.70, 0],
+ 'fa': [3.94150390e+000, 5.16915345e+000, 1.61941074e-001],
+ 'fb': [4.18246722e-001, 1.25201788e+001, 4.81540117e-002],
+ 'fc': [4.15299561e-001, 2.91761325e+000, 1.51474927e-001],
+ 'fd': [4.24913856e-001, 1.90899693e+001, 8.81568925e-002]}
+ }
+
+import matplotlib.pyplot as plt
+
+import matplotlib.patches as patches
+from mpl_toolkits.axes_grid1.anchored_artists import AnchoredSizeBar
+from matplotlib.patches import Circle # , Ellipse, Rectangle
+from matplotlib.collections import PatchCollection
+from matplotlib.lines import Line2D
+
+from scipy.interpolate import interp1d
+from scipy.ndimage import map_coordinates, geometric_transform
+
+import ase
+import numpy as np
+import sidpy
+
+# ##################################
+# Plot Reciprocal Unit Cell in 2D #
+# ##################################
+
+
+[docs]def plot_reciprocal_unit_cell_2D(atoms):
+ """Plot # unit cell in reciprocal space in 2D"""
+
+ reciprocal_unit_cell = atoms.get_reciprocal_cell()
+
+ # ignore y direction
+
+ x = [reciprocal_unit_cell[0, 0], reciprocal_unit_cell[0, 0], reciprocal_unit_cell[1, 0], reciprocal_unit_cell[1, 0]]
+ z = [reciprocal_unit_cell[0, 2], reciprocal_unit_cell[2, 2], reciprocal_unit_cell[2, 2], reciprocal_unit_cell[0, 2]]
+
+ # Plot 2D
+ fig = plt.figure()
+ ax = plt.gca() # current axis
+
+ ax.scatter(x, z, c='red', s=80)
+ ax.add_patch(
+ patches.Rectangle(
+ (0, 0), # (x,y)
+ reciprocal_unit_cell[0, 0], # width
+ reciprocal_unit_cell[2, 2], # height
+ fill=False # remove background
+ )
+ )
+ ax.add_patch(
+ patches.FancyArrow(0, 0, reciprocal_unit_cell[0, 0], 0, width=0.02,
+ color='black',
+ head_width=0.08, # Default: 3 * width
+ head_length=0.1, # Default: 1.5 * head_width
+ length_includes_head=True # Default: False
+ )
+ )
+ ax.add_patch(
+ patches.FancyArrow(0, 0, 0, reciprocal_unit_cell[2, 2], width=0.02,
+ color='black',
+ head_width=0.08, # Default: 3 * width
+ head_length=0.1, # Default: 1.5 * head_width
+ length_includes_head=True # Default: False
+ )
+ )
+
+ plt.xlabel('x 1/nm')
+ plt.ylabel('z 1/nm')
+ ax.axis('equal')
+ # plt.title('Unit Cell in Reciprocal Space of {0}'.format(tags['crystal']) )
+ # texfig.savefig("recip_unit_cell")
+ # fig.savefig('recip_unit_cell.jpg', dpi=90, bbox_inches='tight')
+ plt.show()
+ return fig
+
+
+# ####################
+# Plot SAED Pattern #
+# ####################
+[docs]def plotSAED_parameter(gray=False):
+
+ tags = {'convergence_angle_A-1': 0,
+ 'background': 'white', # 'white' 'grey'
+ 'color_map': 'plasma', # ,'cubehelix' #'Greys'#'plasma'
+ 'color_reflections': 'intensity'}
+
+ if gray:
+ tags['color_map'] = 'gray'
+ tags['background'] = '#303030' # 'darkgray'
+ tags['color_reflections'] = 'intensity'
+ tags['plot_HOLZ'] = 0
+ tags['plot_HOLZ_excess'] = 0
+ tags['plot_Kikuchi'] = 1
+ tags['plot_reflections'] = 1
+
+ tags['color_Kikuchi'] = 'green'
+
+ tags['linewidth_HOLZ'] = -1 # -1: linewidth according to intensity (structure factor F^2
+ tags['linewidth_Kikuchi'] = -1 # -1: linewidth according to intensity (structure factor F^2
+
+ tags['label_HOLZ'] = 0
+ tags['label_Kikuchi'] = 0
+ tags['label_reflections'] = 0
+
+ tags['label_color'] = 'white'
+ tags['label_size'] = 10
+
+ tags['color_Laue_Zones'] = ['red', 'blue', 'green', 'blue', 'green'] # , 'green', 'red'] #for OLZ give a sequence
+ tags['color_zero'] = 'red' # 'None' #'white'
+ tags['color_ring_zero'] = 'None' # 'Red' #'white' #, 'None'
+ tags['width_ring_zero'] = .2
+
+ # plotDiffPattern(tags,True)
+ tags['plot_rotation'] = 0. # degree
+ tags['plot_shift_x'] = -0.0
+ tags['plot_shift_y'] = 0.0
+
+ return tags
+
+
+########################
+# Plot Kikuchi Pattern #
+########################
+[docs]def plotKikuchi(grey=False):
+ tags = {'background': 'black', # 'white' 'grey'
+ 'color_map': 'plasma', # ,'cubehelix'#'Greys'#'plasma'
+ 'color_reflections': 'intensity',
+ 'plot_HOLZ': 0,
+ 'plot_HOLZ_excess': 0,
+ 'plot_Kikuchi': 1,
+ 'plot_reflections': 1,
+ 'label_HOLZ': 0,
+ 'label_Kikuchi': 0,
+ 'label_reflections': 0,
+ 'label_color': 'white',
+ 'label_size': 10,
+ 'color_Kikuchi': 'green',
+ 'linewidth_HOLZ': -1, # -1: linewidth according to intensity (structure factor F^2
+ 'linewidth_Kikuchi': -1, # -1: linewidth according to intensity (structure factor F^2
+ 'color_Laue_Zones': ['red', 'blue', 'green', 'blue', 'green'], # , 'green', 'red'] #for OLZ give a sequence
+ 'color_zero': 'white', # 'None' #'white'
+ 'color_ring_zero': 'None', # 'Red' #'white' #, 'None'
+ 'width_ring_zero': 2}
+
+ if grey:
+ tags['color_map'] = 'gray'
+ tags['background'] = '#303030' # 'darkgray'
+ tags['color_reflections'] = 'intensity'
+
+ return tags
+
+ # plotDiffPattern(tags,True)
+
+
+########################
+# Plot HOLZ Pattern #
+########################
+
+[docs]def plotHOLZ_parameter(grey=False):
+ tags = {'background': 'gray', 'color_map': 'plasma', 'color_reflections': 'intensity', 'plot_HOLZ': 1,
+ 'plot_HOLZ_excess': 1, 'plot_Kikuchi': 1, 'plot_reflections': 1, 'label_HOLZ': 0, 'label_Kikuchi': 0,
+ 'label_reflections': 0, 'label_color': 'white', 'label_size': 12, 'color_Kikuchi': 'green',
+ 'linewidth_HOLZ': 1, 'linewidth_Kikuchi': -1,
+ 'color_Laue_Zones': ['red', 'blue', 'lightblue', 'green', 'red'], 'color_zero': 'None',
+ 'color_ring_zero': 'Red', 'width_ring_zero': 2, 'plot_rotation': 0., 'plot_shift_x': -0.0,
+ 'plot_shift_y': 0.0} # 'white' 'grey'
+
+ # plotDiffPattern(holz,True)
+ return tags
+
+
+########################
+# Plot CBED Pattern #
+########################
+
+[docs]def plotCBED_parameter():
+ tags = {'background': 'black', 'color_map': 'plasma', 'color_reflections': 'intensity', 'plot_HOLZ': 1,
+ 'plot_HOLZ_excess': 1, 'plot_Kikuchi': 1, 'plot_reflections': 1, 'label_HOLZ': 0, 'label_Kikuchi': 0,
+ 'label_reflections': 0, 'label_color': 'white', 'label_size': 10, 'color_Kikuchi': 'green',
+ 'linewidth_HOLZ': -1, 'linewidth_Kikuchi': -1, 'color_Laue_Zones': ['red', 'blue', 'green'],
+ 'color_zero': 'white', 'color_ring_zero': 'Red', 'width_ring_zero': 2} # 'white' 'grey'
+
+ # plotDiffPattern(tags,True)
+ return tags
+
+########################
+# Plot HOLZ Pattern #
+########################
+
+
+[docs]def circles(x, y, s, c='b', vmin=None, vmax=None, **kwargs):
+ """
+ Make a scatter plot of circles.
+ Similar to plt.scatter, but the size of circles are in data scale.
+ Parameters
+ ----------
+ x, y : scalar or array_like, shape (n, )
+ Input data
+ s : scalar or array_like, shape (n, )
+ Radius of circles.
+ c : color or sequence of color, optional, default : 'b'
+ `c` can be a single color format string, or a sequence of color
+ specifications of length `N`, or a sequence of `N` numbers to be
+ mapped to colors using the `cmap` and `norm` specified via kwargs.
+ Note that `c` should not be a single numeric RGB or RGBA sequence
+ because that is indistinguishable from an array of values
+ to be colormapped. (If you insist, use `color` instead.)
+ `c` can be a 2-D array in which the rows are RGB or RGBA, however.
+ vmin, vmax : scalar, optional, default: None
+ `vmin` and `vmax` are used in conjunction with `norm` to normalize
+ luminance data. If either are `None`, the min and max of the
+ color array is used.
+ kwargs : `~matplotlib.collections.Collection` properties
+ Eg. alpha, edgecolor(ec), facecolor(fc), linewidth(lw), linestyle(ls),
+ norm, cmap, transform, etc.
+ Returns
+ -------
+ paths : `~matplotlib.collections.PathCollection`
+ Examples
+ --------
+ a = np.arange(11)
+ circles(a, a, s=a*0.2, c=a, alpha=0.5, ec='none')
+ plt.colorbar()
+ License
+ --------
+ This code is under [The BSD 3-Clause License]
+ (http://opensource.org/licenses/BSD-3-Clause)
+ """
+
+ if np.isscalar(c):
+ kwargs.setdefault('color', c)
+ c = None
+
+ if 'fc' in kwargs:
+ kwargs.setdefault('facecolor', kwargs.pop('fc'))
+ if 'ec' in kwargs:
+ kwargs.setdefault('edgecolor', kwargs.pop('ec'))
+ if 'ls' in kwargs:
+ kwargs.setdefault('linestyle', kwargs.pop('ls'))
+ if 'lw' in kwargs:
+ kwargs.setdefault('linewidth', kwargs.pop('lw'))
+ # You can set `facecolor` with an array for each patch,
+ # while you can only set `facecolors` with a value for all.
+
+ zipped = np.broadcast(x, y, s)
+ patches = [Circle((x_, y_), s_, picker=True)
+ for x_, y_, s_ in zipped]
+ collection = PatchCollection(patches, **kwargs)
+ if c is not None:
+ c = np.broadcast_to(c, zipped.shape).ravel()
+ collection.set_array(c)
+ collection.set_clim(vmin, vmax)
+
+ ax = plt.gca()
+ ax.add_collection(collection)
+ ax.autoscale_view()
+ plt.draw_if_interactive()
+ if c is not None:
+ plt.sci(collection)
+ return collection
+
+
+[docs]def cartesian2polar(x, y, grid, r, t, order=3):
+ R, T = np.meshgrid(r, t)
+
+ new_x = R * np.cos(T)
+ new_y = R * np.sin(T)
+
+ ix = interp1d(x, np.arange(len(x)))
+ iy = interp1d(y, np.arange(len(y)))
+
+ new_ix = ix(new_x.ravel())
+ new_iy = iy(new_y.ravel())
+
+ return map_coordinates(grid, np.array([new_ix, new_iy]),
+ order=order).reshape(new_x.shape)
+
+
+[docs]def warp(diff, center):
+ """
+ Define original polar grid
+
+ Parameter:
+ ----------
+ diff: sidpy object or numpy ndarray of
+ diffraction pattern
+ center: list or numpy array of length 2
+ coordinates of center in pixel
+
+ Return:
+ ------
+ numpy array of diffraction pattern in polar coordinates
+
+ """
+ nx = diff.shape[0]
+ ny = diff.shape[1]
+
+ x = np.linspace(1, nx, nx, endpoint=True) - center[0]
+ y = np.linspace(1, ny, ny, endpoint=True) - center[1]
+ z = diff
+
+ # Define new polar grid
+ nr = int(min([center[0], center[1], diff.shape[0] - center[0], diff.shape[1] - center[1]]) - 1)
+ print(nr)
+ nt = 360 * 3
+
+ r = np.linspace(1, nr, nr)
+ t = np.linspace(0., np.pi, nt, endpoint=False)
+ return cartesian2polar(x, y, z, r, t, order=3).T
+
+
+[docs]def topolar(img, order=1):
+ """
+ Transform img to its polar coordinate representation.
+
+ order: int, default 1
+ Specify the spline interpolation order.
+ High orders may be slow for large images.
+ """
+ # max_radius is the length of the diagonal
+ # from a corner to the mid-point of img.
+ max_radius = 0.5 * np.linalg.norm(img.shape)
+
+ def transform(coords):
+ # Put coord[1] in the interval, [-pi, pi]
+ theta = 2 * np.pi * coords[1] / (img.shape[1] - 1.)
+
+ # Then map it to the interval [0, max_radius].
+ # radius = float(img.shape[0]-coords[0]) / img.shape[0] * max_radius
+ radius = max_radius * coords[0] / img.shape[0]
+
+ i = 0.5 * img.shape[0] - radius * np.sin(theta)
+ j = radius * np.cos(theta) + 0.5 * img.shape[1]
+ return i, j
+
+ polar = geometric_transform(img, transform, order=order)
+
+ rads = max_radius * np.linspace(0, 1, img.shape[0])
+ angs = np.linspace(0, 2 * np.pi, img.shape[1])
+
+ return polar, (rads, angs)
+
+
+[docs]def plot_ring_pattern(atoms, diffraction_pattern=None, grey=False):
+ """
+ Plot of ring diffraction pattern with matplotlib
+
+ Parameters
+ ----------
+ atoms: dictionary or sidpy.Dataset
+ information stored as dictionary either directly or in metadata attribute of sidpy.Dataset
+ grey: bool
+ plotting in greyscale if True
+
+ Returns
+ -------
+ fig: matplotlib figure
+ reference to matplotlib figure
+ """
+
+ if isinstance(atoms, dict):
+ tags = atoms
+ elif isinstance(atoms, ase.Atoms):
+ if 'diffraction' in atoms.info:
+ tags = atoms.info['diffraction']
+ plot_diffraction_pattern = True
+ else:
+ raise TypeError('Diffraction information must be in metadata')
+ else:
+ raise TypeError('Diffraction info must be in sidpy Dataset or dictionary form')
+ if diffraction_pattern is not None:
+ if not(diffraction_pattern, sidpy.Dataset):
+ print('diffraction_pattern must be a sidpy.Dataset \n -> Ignoring this variable')
+ diffraction_pattern = None
+ d = tags['Ring_Pattern']['allowed']['g norm']
+ label = tags['Ring_Pattern']['allowed']['label']
+ if 'label_color' not in tags:
+ tags['label_color'] = 'navy'
+ if 'profile color' not in tags:
+ tags['profile color'] = 'navy'
+ if 'ring color' not in tags:
+ tags['ring color'] = 'red'
+ if 'label_size' not in tags:
+ tags['label_size'] = 10
+ if 'profile height' not in tags:
+ tags['profile height'] = 5
+ if 'plot_scalebar' not in tags:
+ tags['plot_scalebar'] = False
+
+ fg, ax = plt.subplots(1, 1)
+
+ # ###
+ # plot arcs of the rings
+ # ###
+ for i in range(len(d)):
+ pac = patches.Arc((0, 0), d[i] * 2, d[i] * 2, angle=0, theta1=45, theta2=360, color=tags['ring color'])
+ ax.add_patch(pac)
+
+ ####
+ # show image in background
+ ####
+ if plot_diffraction_pattern is not None:
+ plt.imshow(diffraction_pattern, extent=diffraction_pattern.get_extent(), cmap='gray')
+
+ ax.set_aspect("equal")
+
+ # fg.canvas.draw()
+
+ if tags['plot_scalebar']:
+ def f(axis):
+ l = axis.get_majorticklocs()
+ return len(l) > 1 and (l[1] - l[0])
+
+ sizex = f(ax.xaxis)
+ labelx = str(sizex) + ' 1/nm'
+ scalebar = AnchoredSizeBar(ax.transData, sizex, labelx, loc=3,
+ pad=0.5, color='white', frameon=False)
+ # size_vertical=.2, fill_bar = True) # will be implemented in matplotlib 2.1
+
+ ax.add_artist(scalebar)
+ ax.axis('off')
+
+ # ####
+ # plot profile
+ # ####
+
+ y = tags['Ring_Pattern']['profile_y']
+ y = y / y.max() * tags['profile height']
+ x = tags['Ring_Pattern']['profile_x']
+ ax.plot(x, y, c=tags['profile color'])
+
+ ax.plot([0, x[-1]], [0, 0], c=tags['profile color'])
+
+ if 'experimental profile_y' in tags:
+ yy = tags['experimental profile_y']
+ yy = yy / yy.max() * tags['profile height']
+ xx = tags['experimental profile_x']
+ ax.plot(xx, yy, c=tags['experimental profile color'])
+
+ if 'plot_image_FOV' in tags:
+ max_d = tags['plot_image_FOV'] / 2 + tags['plot_shift_x']
+ else:
+ max_d = d.max()
+ for i in range(len(d)):
+ if d[i] < max_d:
+ plt.text(d[i] - .2, -.5, label[i], fontsize=tags['label_size'], color=tags['label_color'], rotation=90)
+
+ if 'plot_FOV' in tags:
+ l = -tags['plot_FOV'] / 2
+ r = tags['plot_FOV'] / 2
+ t = -tags['plot_FOV'] / 2
+ b = tags['plot_FOV'] / 2
+ plt.xlim(l, r)
+ plt.ylim(t, b)
+
+ fg.show()
+ return fg
+
+
+[docs]def plot_diffraction_pattern(atoms, diffraction_pattern=None, grey=False):
+ """
+ Plot of spot diffraction pattern with matplotlib
+ Plot of spot diffraction pattern with matplotlib
+
+ Parameters
+ ----------
+ atoms: dictionary or ase.Atoms object
+ information stored as dictionary either directly or in info attribute of ase.Atoms object
+ diffraction_pattern: None or sidpy.Dataset
+ diffraction pattern in background
+ grey: bool
+ plotting in greyscale if True
+
+ Returns
+ -------
+ fig: matplotlib figure
+ reference to matplotlib figure
+ """
+
+ if isinstance(atoms, dict):
+ tags_out = atoms
+
+ elif isinstance(atoms, ase.Atoms):
+ if 'diffraction' in atoms.info:
+ tags_out = atoms.info['diffraction']
+ plot_diffraction_pattern = True
+ else:
+ raise TypeError('Diffraction information must be in info dictionary of ase.Atoms object')
+ else:
+ raise TypeError('Diffraction info must be in ase.Atoms object or dictionary form')
+
+ if 'output' not in atoms.info:
+ return
+
+ # Get information from dictionary
+ HOLZ = tags_out['HOLZ']
+ ZOLZ = tags_out['allowed']['ZOLZ']
+ # Kikuchi = tags_out['Kikuchi']
+
+ Laue_Zone = tags_out['allowed']['Laue_Zone']
+
+ label = tags_out['allowed']['label']
+ hkl_label = tags_out['allowed']['hkl']
+
+ angle = np.radians(atoms.info['output']['plot_rotation']) # mrad
+ c = np.cos(angle)
+ s = np.sin(angle)
+ r_mat = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
+
+ # HOLZ and Kikuchi lines coordinates in Hough space
+ LC = tags_out['Laue_circle']
+ gd = np.dot(tags_out['HOLZ']['g_deficient'] + LC, r_mat)
+ ge = np.dot(tags_out['HOLZ']['g_excess'], r_mat)
+ points = np.dot(tags_out['allowed']['g'] + LC, r_mat)
+
+ theta = tags_out['HOLZ']['theta'] + angle
+
+ if 'thickness' not in tags_out:
+ tags_out['thickness'] = 0.
+ if tags_out['thickness'] > 0.1:
+ intensity = np.real(tags_out['allowed']['Ig'])
+ else:
+ intensity = tags_out['allowed']['intensities']
+
+ radius = atoms.info['experimental']['convergence_angle_A-1']
+
+ if radius < 0.1:
+ radiusI = 2
+ else:
+ radiusI = radius
+ # Beginning and ends of HOLZ lines
+ max_length = radiusI * 1.3
+ h_xp = gd[:, 0] + max_length * np.cos(np.pi - theta)
+ h_yp = gd[:, 1] + max_length * np.sin(np.pi - theta)
+ h_xm = gd[:, 0] - max_length * np.cos(np.pi - theta)
+ h_ym = gd[:, 1] - max_length * np.sin(np.pi - theta)
+
+ # Beginning and ends of excess HOLZ lines
+ max_length = radiusI * .8
+ e_xp = ge[:, 0] + max_length * np.cos(np.pi - theta)
+ e_yp = ge[:, 1] + max_length * np.sin(np.pi - theta)
+ e_xm = ge[:, 0] - max_length * np.cos(np.pi - theta)
+ e_ym = ge[:, 1] - max_length * np.sin(np.pi - theta)
+
+ # Beginning and ends of Kikuchi lines
+ if 'max_length' not in tags_out['Kikuchi']:
+ tags_out['Kikuchi']['max_length'] = 20
+ max_length = tags_out['Kikuchi']['max_length']
+
+ gd = tags_out['Kikuchi']['g_deficient']
+ theta = tags_out['Kikuchi']['theta']
+ k_xp = gd[:, 0] + max_length * np.cos(np.pi - theta)
+ k_yp = gd[:, 1] + max_length * np.sin(np.pi - theta)
+ k_xm = gd[:, 0] - max_length * np.cos(np.pi - theta)
+ k_ym = gd[:, 1] - max_length * np.sin(np.pi - theta)
+
+ if atoms.info['output']['linewidth_Kikuchi'] < 0:
+ if len(intensity[ZOLZ]) > 0:
+ intensity_kikuchi = intensity * 4. / intensity[ZOLZ].max()
+ else:
+ intensity_kikuchi = intensity
+ else:
+ intensity_kikuchi = np.ones(len(intensity)) * atoms.info['output']['linewidth_Kikuchi']
+
+ if atoms.info['output']['linewidth_HOLZ'] < 0:
+ intensity_holz = np.log(intensity + 1)
+
+ if tags_out['HOLZ']['HOLZ'].any():
+ pass # intensity_holz = intensity/intensity[tags_out['HOLZ']['HOLZ']].max()*4.
+ else:
+ intensity_holz = np.ones(len(intensity)) * atoms.info['output']['linewidth_HOLZ']
+
+ # #######
+ # Plot #
+ # #######
+ # cms = mpl.cm
+ # cm = cms.plasma#jet#, cms.gray, cms.autumn]
+ cm = plt.get_cmap(atoms.info['output']['color_map'])
+
+ # fig = plt.figure()
+ fig = plt.figure()
+
+ ax = plt.gca()
+ if 'background' not in atoms.info['output']:
+ atoms.info['output']['background'] = None
+ if atoms.info['output']['background'] is not None:
+ ax.set_facecolor(atoms.info['output']['background'])
+
+ if diffraction_pattern is not None:
+ plt.imshow(diffraction_pattern, extent=diffraction_pattern.get_extent([0, 1]), cmap='gray')
+
+ ix = np.argsort((points ** 2).sum(axis=1))
+ p = points[ix]
+ inten = intensity[ix]
+ reflection = hkl_label[ix]
+ laue_color = []
+
+ labelP = ''
+ lineLabel = []
+
+ def onpick(event):
+ if isinstance(event.artist, Line2D):
+ thisline = event.artist
+ ind = ax.lines.index(thisline)
+ print(ind, len(points), ind - len(points))
+ # ind = ind- len(points)
+ h, k, l = lineLabel[ind]
+
+ if Laue_Zone[ind] > 0:
+ labelP = 'Laue Zone %1d; HOLZ line: [%1d,%1d,%1d]' % (Laue_Zone[ind], h, k, l)
+ else:
+ labelP = 'Kikuchi line: [%1d,%1d,%1d]' % (h, k, l)
+ # print(labelP)
+
+ elif isinstance(event.artist, Circle):
+ print('Circle')
+
+ else:
+ ind = event.ind[0]
+ h, k, l = reflection[ind]
+
+ print('Reflection: [%1d,%1d,%1d]' % (h, k, l))
+
+ for i in range(int(Laue_Zone.max()) + 1):
+ if i < len(atoms.info['output']['color_Laue_Zones']):
+ laue_color.append(atoms.info['output']['color_Laue_Zones'][i])
+ else:
+ laue_color.append(atoms.info['output']['color_Laue_Zones'][-1])
+
+ if 'plot_labels' not in atoms.info['output']:
+ atoms.info['output']['plot_labels'] = True
+ if atoms.info['output']['plot_reflections']:
+ if radius < 0.01:
+ if atoms.info['output']['color_reflections'] == 'intensity':
+ for i in range(len(points)):
+ ax.scatter(points[i, 0], points[i, 1], c=np.log(intensity[i] + 1), cmap=cm, s=100)
+
+ if atoms.info['output']['plot_labels']:
+ plt.text(points[i, 0], points[i, 1], label[i], fontsize=10)
+ else:
+ for i in range(len(Laue_Zone)):
+ color = laue_color[int(Laue_Zone[i])]
+ ax.scatter(points[i, 0], points[i, 1], c=color, cmap=cm, s=100)
+ if atoms.info['output']['plot_labels']:
+ plt.text(points[i, 0], points[i, 1], label[i], fontsize=8)
+
+ ax.scatter(LC[0], LC[1], c=atoms.info['output']['color_zero'], s=100)
+ radius = .2
+ else:
+ ix = np.argsort((points ** 2).sum(axis=1))
+ p = points[ix]
+ inten = intensity[ix]
+ if atoms.info['output']['color_reflections'] == 'intensity':
+ circles(p[:, 0], p[:, 1], s=radius, c=np.log(inten + 1), cmap=cm, alpha=0.9, edgecolor=None, picker=5)
+ else:
+ for i in range(len(Laue_Zone)):
+ color = laue_color[int(Laue_Zone[i])]
+ circles(p[i, 0], p[i, 1], s=radius, c=color, cmap=cm, alpha=0.9, edgecolor='', picker=5) #
+ plt.text(points[i, 0], points[i, 1], label[i], fontsize=8)
+
+ if 'plot_dynamically_allowed' not in atoms.info['output']:
+ atoms.info['output']['plot_dynamically_allowed'] = False
+ if 'plot_forbidden' not in atoms.info['output']:
+ atoms.info['output']['plot_forbidden'] = False
+
+ if atoms.info['output']['plot_dynamically_allowed']:
+ if 'dynamically_allowed' not in atoms.info['diffraction']['forbidden']:
+ print('To plot dynamically allowed reflections you must run the get_dynamically_allowed function of '
+ 'kinematic_scattering library first!')
+ else:
+ points = atoms.info['diffraction']['forbidden']['g']
+ dynamically_allowed = atoms.info['diffraction']['forbidden']['dynamically_allowed']
+ dyn_allowed = atoms.info['diffraction']['forbidden']['g'][dynamically_allowed, :]
+ dyn_label = atoms.info['diffraction']['forbidden']['hkl'][dynamically_allowed, :]
+
+ color = laue_color[0]
+ ax.scatter(dyn_allowed[:, 0], dyn_allowed[:, 1], c='blue', alpha=0.4, s=70)
+ if atoms.info['output']['plot_labels']:
+ for i in range(len(dyn_allowed)):
+ plt.text(dyn_allowed[i, 0], dyn_allowed[i, 1], dyn_label[i], fontsize=8)
+ if atoms.info['output']['plot_forbidden']:
+ forbidden_g = atoms.info['diffraction']['forbidden']['g'][np.logical_not(dynamically_allowed), :]
+ forbidden_hkl = atoms.info['diffraction']['forbidden']['hkl'][np.logical_not(dynamically_allowed), :]
+ ax.scatter(forbidden_g[:, 0], forbidden_g[:, 1], c='orange', alpha=0.4, s=70)
+ if atoms.info['output']['plot_labels']:
+ for i in range(len(forbidden_g)):
+ plt.text(forbidden_g[i, 0], forbidden_g[i, 1], forbidden_hkl[i], fontsize=8)
+ elif atoms.info['output']['plot_forbidden']:
+ forbidden_g = atoms.info['diffraction']['forbidden']['g']
+ forbidden_hkl = atoms.info['diffraction']['forbidden']['hkl']
+ ax.scatter(forbidden_g[:, 0], forbidden_g[:, 1], c='orange', alpha=0.4, s=70)
+ if atoms.info['output']['plot_labels']:
+ for i in range(len(forbidden_g)):
+ plt.text(forbidden_g[i, 0], forbidden_g[i, 1], forbidden_hkl[i], fontsize=8)
+
+ k = 0
+ if atoms.info['output']['plot_HOLZ']:
+ for i in range(len(h_xp)):
+ if tags_out['HOLZ']['HOLZ'][i]:
+ color = laue_color[int(Laue_Zone[i])]
+ if atoms.info['output']['plot_HOLZ']:
+ # plot HOLZ lines
+ line, = plt.plot((h_xp[i], h_xm[i]), (h_yp[i], h_ym[i]), c=color, linewidth=intensity_holz[i],
+ picker=5)
+ if atoms.info['output']['label_HOLZ']: # Add indices
+ plt.text(h_xp[i], h_yp[i], label[i], fontsize=8)
+ lineLabel.append(hkl_label[i])
+ # print(i, hkl_label[i], intensity_holz[i])
+
+ if atoms.info['output']['plot_HOLZ_excess']:
+ line, = plt.plot((e_xp[i], e_xm[i]), (e_yp[i], e_ym[i]), c=color, linewidth=intensity_holz[i])
+ lineLabel.append(hkl_label[i])
+
+ if atoms.info['output']['label_HOLZ']: # Add indices
+ plt.text(e_xp[i], e_yp[i], label[i], fontsize=8)
+
+ elif atoms.info['output']['label_Kikuchi']: # Add indices
+ if ZOLZ[i]:
+ plt.text(k_xp[i], k_yp[i], label[i], fontsize=atoms.info['output']['label_size'],
+ color=atoms.info['output']['label_color'])
+ lineLabel.append(hkl_label[i])
+ if atoms.info['output']['plot_Kikuchi']:
+ # Beginning and ends of Kikuchi lines
+ if atoms.info['output']['label_Kikuchi']:
+ label_kikuchi = []
+ for i in range(len(label)):
+ if ZOLZ[i]:
+ label_kikuchi.append(label[i])
+ for i in range(len(k_xp)):
+ line, = plt.plot((k_xp[i], k_xm[i]), (k_yp[i], k_ym[i]), c=atoms.info['output']['color_Kikuchi'],
+ linewidth=2)
+ if atoms.info['output']['label_Kikuchi']: # Add indices
+ plt.text(k_xp[i], k_yp[i], label[i], fontsize=atoms.info['output']['label_size'],
+ color=atoms.info['output']['label_color'])
+
+ def format_coord(x, y):
+ return labelP + 'x=%1.4f, y=%1.4f' % (x, y)
+
+ ax.format_coord = format_coord
+
+ if atoms.info['output']['color_ring_zero'] != 'None':
+ ring = plt.Circle(LC, radius, color=atoms.info['output']['color_ring_zero'], fill=False, linewidth=2)
+ ax.add_artist(ring)
+ # print(ring)
+ if atoms.info['output']['color_zero'] != 'None':
+ circle = plt.Circle(LC, radius, color=atoms.info['output']['color_zero'], linewidth=2)
+ ax.add_artist(circle)
+
+ plt.axis('equal')
+ if 'plot_FOV' in tags_out:
+ l = -tags_out['plot_FOV'] / 2
+ r = tags_out['plot_FOV'] / 2
+ t = -tags_out['plot_FOV'] / 2
+ b = tags_out['plot_FOV'] / 2
+ plt.xlim(l, r)
+ plt.ylim(t, b)
+
+ fig.canvas.mpl_connect('pick_event', onpick)
+ # texfig.savefig("HOLZ")
+
+ # plt.title( tags_out['crystal'])
+ plt.show()
+
+"""
+Dynamic Scattering Library for Multi-Slice Calculations
+
+author: Gerd Duscher
+"""
+
+import numpy as np
+import scipy.constants
+import scipy.special
+
+import pyTEMlib.kinematic_scattering as ks # kinematic scattering Library
+
+
+[docs]def potential_1dim(element, r):
+ """ Calculates the projected potential of an atom of element
+
+ The projected potential will be in units of V nm^2,
+ however, internally we will use Angstrom instead of nm!
+ The basis for these calculations are the atomic form factors of Kirkland 2𝑛𝑑 edition
+ following the equation in Appendix C page 252.
+
+ Parameter
+ ---------
+ element: str
+ name of 'element
+ r: numpy array [nxn]
+ impact parameters (distances from atom position) in nm
+
+ Returns
+ -------
+ numpy array (nxn)
+ projected potential in units of V nm^2
+ """
+
+ # get elementary constants
+ a0 = scipy.constants.value('Bohr radius') * 1e10 # in Angstrom
+ rydberg_div_e = scipy.constants.value('Rydberg constant times hc in eV') # in V
+ e0 = 2 * rydberg_div_e * scipy.constants.value('Bohr radius') * 1e10 # now in V A
+
+ pre_factor = 2 * np.pi ** 2 * a0 * e0
+
+ param = ks.electronFF[element] # parametrized form factors
+ f_lorentz = r * 0 # Lorentzian term
+ f_gauss = r * 0 # Gaussian term
+ for i in range(3):
+ f_lorentz += param['fa'][i] * scipy.special.k0(2 * np.pi * r * np.sqrt(param['fb'][i]))
+ f_gauss += param['fc'][i] / param['fd'][i] * np.exp(-np.pi ** 2 * r ** 2 / param['fd'][i])
+ f_lorentz[0, 0] = f_lorentz[0, 1]
+ # / 100 is conversion from V Angstrom^2 to V nm^2
+ return pre_factor * (2 * f_lorentz + f_gauss) # V Angstrom^2
+
+
+[docs]def potential_2dim(element, nx, ny, n_cell_x, n_cell_y, lattice_parameter, base):
+ """Make a super-cell with potentials
+
+ Limitation is that we only place atom potential with single pixel resolution
+ """
+ n_cell_x = int(2 ** np.log2(n_cell_x))
+ n_cell_y = int(2 ** np.log2(n_cell_y))
+
+ pixel_size = lattice_parameter / (nx / n_cell_x)
+
+ a_nx = a_ny = int(1 / pixel_size)
+ x, y = np.mgrid[0:a_nx, 0:a_ny] * pixel_size
+ a = int(nx / n_cell_x)
+ r = x ** 2 + y ** 2
+
+ atom_potential = potential_1dim(element, r)
+
+ potential = np.zeros([nx, ny])
+
+ atom_potential_corner = np.zeros([nx, ny])
+ atom_potential_corner[0:a_nx, 0:a_ny] = atom_potential
+ atom_potential_corner[nx - a_nx:, 0:a_ny] = np.flip(atom_potential, axis=0)
+ atom_potential_corner[0:a_nx, ny - a_ny:] = np.flip(atom_potential, axis=1)
+ atom_potential_corner[nx - a_nx:, ny - a_ny:] = np.flip(np.flip(atom_potential, axis=0), axis=1)
+
+ unit_cell_base = np.array(base) * a
+ unit_cell_base = np.array(unit_cell_base, dtype=int)
+
+ for pos in unit_cell_base:
+ potential = potential + np.roll(atom_potential_corner, shift=np.array(pos), axis=[0, 1])
+
+ for column in range(int(np.log2(n_cell_x))):
+ potential = potential + np.roll(potential, shift=2 ** column * a, axis=1)
+ for row in range(int(np.log2(n_cell_y))):
+ potential = potential + np.roll(potential, shift=2 ** row * a, axis=0)
+
+ return potential
+
+
+[docs]def interaction_parameter(acceleration_voltage):
+ """Calculates interaction parameter sigma
+
+ Parameter
+ ---------
+ acceleration_voltage: float
+ acceleration voltage in volt
+
+ Returns
+ -------
+ interaction parameter: float
+ interaction parameter (dimensionless)
+ """
+ e0 = 510998.95 # m_0 c^2 in eV
+
+ wavelength = ks.get_wavelength(acceleration_voltage)
+ e = acceleration_voltage
+
+ return 2. * np.pi / (wavelength * e) * (e0 + e) / (2. * e0 + e)
+
+
+[docs]def get_transmission(potential, acceleration_voltage):
+ """ Get transmission function
+
+ has to be multiplied in real space with wave function
+
+ Parameter
+ ---------
+ potential: numpy array (nxn)
+ potential of a layer
+ acceleration_voltage: float
+ acceleration voltage in V
+
+ Returns
+ -------
+ complex numpy array (nxn)
+ """
+
+ sigma = interaction_parameter(acceleration_voltage)
+
+ return np.exp(1j * sigma * potential)
+
+
+[docs]def get_propagator(size_in_pixel, delta_z, number_layers, wavelength, field_of_view, bandwidth_factor, verbose=True):
+ """Get propagator function
+
+ has to be convoluted with wave function after transmission
+
+ Parameter
+ ---------
+ size_in_pixel: int
+ number of pixels of one axis in square image
+ delta_z: float
+ distance between layers
+ number_layers: int
+ number of layers to make a propagator
+ wavelength: float
+ wavelength of incident electrons
+ field_of_view: float
+ field of view of image
+ bandwidth_factor: float
+ relative bandwidth to avoid anti-aliasing
+
+ Returns
+ -------
+ propagator: complex numpy array (layers x size_in_pixel x size_in_pixel)
+
+ """
+
+ k2max = size_in_pixel / field_of_view / 2. * bandwidth_factor
+ print(k2max)
+ if verbose:
+ print(f"Bandwidth limited to a real space resolution of {1.0 / k2max * 1000} pm")
+ print(f" (= {wavelength * k2max * 1000.0:.2f} mrad) for symmetrical anti-aliasing.")
+ k2max = k2max * k2max
+
+ kx, ky = np.mgrid[-size_in_pixel / 2:size_in_pixel / 2, -size_in_pixel / 2:size_in_pixel / 2] / field_of_view
+ k_square = kx ** 2 + ky ** 2
+ k_square[k_square > k2max] = 0 # bandwidth limiting
+
+ if verbose:
+ temp = np.zeros([size_in_pixel, size_in_pixel])
+ temp[k_square > 0] = 1
+ print(f"Number of symmetrical non-aliasing beams = {temp.sum():.0f}")
+
+ propagator = np.zeros([number_layers, size_in_pixel, size_in_pixel], dtype=complex)
+ for i in range(number_layers):
+ propagator[i] = np.exp(-1j * np.pi * wavelength * k_square * delta_z[i])
+
+ return propagator
+
+
+[docs]def multi_slice(wave, number_of_unit_cell_z, number_layers, transmission, propagator):
+ """Multi-Slice Calculation
+
+ The wave function will be changed iteratively
+
+ Parameters
+ ----------
+ wave: complex numpy array (nxn)
+ starting wave function
+ number_of_unit_cell_z: int
+ this gives the thickness in multiples of c lattice parameter
+ number_layers: int
+ number of layers per unit cell
+ transmission: complex numpy array
+ transmission function
+ propagator: complex numpy array
+ propagator function
+
+ Returns
+ -------
+ complex numpy array
+ """
+
+ for i in range(number_of_unit_cell_z):
+ for layer in range(number_layers):
+ wave = wave * transmission[layer] # transmission - real space
+ wave = np.fft.fft2(wave)
+ wave = wave * propagator[layer] # propagation; propagator is defined in reciprocal space
+ wave = np.fft.ifft2(wave) # back to real space
+ return wave
+
+
+[docs]def make_chi(theta, phi, aberrations):
+ """
+ ###
+ # Aberration function chi
+ ###
+ phi and theta are meshgrids of the angles in polar coordinates.
+ aberrations is a dictionary with the aberrations coefficients
+ Attention: an empty aberration dictionary will give you a perfect aberration
+ """
+
+ chi = np.zeros(theta.shape)
+ for n in range(6): # First Sum up to fifth order
+ term_first_sum = np.power(theta, n + 1) / (n + 1) # term in first sum
+
+ second_sum = np.zeros(theta.shape) # second Sum initialized with zeros
+ for m in range((n + 1) % 2, n + 2, 2):
+ # print(n, m)
+
+ if m > 0:
+ if f'C{n}{m}a' not in aberrations: # Set non existent aberrations coefficient to zero
+ aberrations[f'C{n}{m}a'] = 0.
+ if f'C{n}{m}b' not in aberrations:
+ aberrations[f'C{n}{m}b'] = 0.
+
+ # term in second sum
+ second_sum = second_sum + aberrations[f'C{n}{m}a'] * np.cos(m * phi) + aberrations[
+ f'C{n}{m}b'] * np.sin(m * phi)
+ else:
+ if f'C{n}{m}' not in aberrations: # Set non existent aberrations coefficient to zero
+ aberrations[f'C{n}{m}'] = 0.
+
+ # term in second sum
+ second_sum = second_sum + aberrations[f'C{n}{m}']
+ chi = chi + term_first_sum * second_sum * 2 * np.pi / aberrations['wavelength']
+
+ return chi
+
+
+[docs]def objective_lens_function(ab, nx, ny, field_of_view, aperture_size=10):
+ """Objective len function to be convoluted with exit wave to derive image function
+
+ Parameter:
+ ----------
+ ab: dict
+ aberrations in nm should at least contain defocus (C10), and spherical aberration (C30)
+ nx: int
+ number of pixel in x direction
+ ny: int
+ number of pixel in y direction
+ field_of_view: float
+ field of view of potential
+ wavelength: float
+ wavelength in nm
+ aperture_size: float
+ aperture size in 1/nm
+
+ Returns:
+ --------
+ object function: numpy array (nx x ny)
+ extent: list
+ """
+
+ wavelength = ab['wavelength']
+ # Reciprocal plane in 1/nm
+ dk = 1 / field_of_view
+ t_xv, t_yv = np.mgrid[int(-nx / 2):int(nx / 2), int(-ny / 2):int(ny / 2)] * dk
+
+ # define reciprocal plane in angles
+ phi = np.arctan2(t_yv, t_xv)
+ theta = np.arctan2(np.sqrt(t_xv ** 2 + t_yv ** 2), 1 / wavelength)
+
+ mask = theta < aperture_size * wavelength
+
+ # calculate chi
+ chi = make_chi(theta, phi, ab)
+
+ extent = [-nx / 2 * dk, nx / 2 * dk, -nx / 2 * dk, nx / 2 * dk]
+ return np.exp(-1j * chi) * mask, extent
+
+"""
+eds_tools
+Model based quantification of energy-dispersive X-ray spectroscopy data
+Copyright by Gerd Duscher
+
+The University of Tennessee, Knoxville
+Department of Materials Science & Engineering
+
+Sources:
+
+Units:
+ everything is in SI units, except length is given in nm and angles in mrad.
+
+Usage:
+ See the notebooks for examples of these routines
+
+All the input and output is done through a dictionary which is to be found in the meta_data
+attribute of the sidpy.Dataset
+"""
+import numpy as np
+
+import scipy
+from scipy.interpolate import interp1d, splrep # splev, splint
+from scipy import interpolate
+from scipy.signal import peak_prominences
+from scipy.ndimage import gaussian_filter
+
+import scipy.constants as const
+
+from scipy import constants
+import matplotlib.pyplot as plt
+# import matplotlib.patches as patches
+
+# from matplotlib.widgets import SpanSelector
+# import ipywidgets as widgets
+# from IPython.display import display
+
+import requests
+
+from scipy.optimize import leastsq # least square fitting routine fo scipy
+
+import sidpy
+
+import pickle # pkg_resources
+import pyTEMlib.eels_tools as eels
+from pyTEMlib.xrpa_x_sections import x_sections
+
+elements_list = eels.elements
+
+shell_occupancy={'K1':2, 'L1':2, 'L2':2, 'L3':4, 'M1':2, 'M2':2, 'M3':4,'M4':4,'M5':6,
+ 'N1':2, 'N2':2,' N3':4,'N4':4,'N5':6, 'N6':6,'N7':8,
+ 'O1':2, 'O2':2,' O3':4,'O4':4,'O5':6, 'O6':6,'O7':8, 'O8':8, 'O9': 10 }
+
+[docs]def detector_response(detector_definition, energy_scale):
+ """
+ Parameters
+ ----------
+
+
+ Example
+ -------
+
+ tags = {}
+
+ tags['acceleration_voltage_V'] = 30000
+
+ tags['detector'] ={}
+ tags['detector']['layers'] ={}
+
+ ## layer thicknesses of commen materials in EDS detectors in m
+ tags['detector']['layers']['alLayer'] = {}
+ tags['detector']['layers']['alLayer']['thickness'] = 30 *1e-9 # in m
+ tags['detector']['layers']['alLayer']['Z'] = 13
+
+ tags['detector']['layers']['deadLayer'] = {}
+ tags['detector']['layers']['deadLayer']['thickness'] = 100 *1e-9 # in m
+ tags['detector']['layers']['deadLayer']['Z'] = 14
+
+ tags['detector']['layers']['window'] = {}
+ tags['detector']['layers']['window']['thickness'] = 100 *1e-9 # in m
+ tags['detector']['layers']['window']['Z'] = 6
+
+ tags['detector']['detector'] = {}
+ tags['detector']['detector']['thickness'] = 45 * 1e-3 # in m
+ tags['detector']['detector']['Z'] = 14
+ tags['detector']['detector']['area'] = 30 * 1e-6 #in m2
+
+ energy_scale = np.linspace(.1,60,1199)*1000 i eV
+ detector_response(tags, energy_scale)
+ """
+ response = np.ones(len(energy_scale))
+ x_sections = eels.get_x_sections()
+
+ for key in detector_definition['layers']:
+ Z = detector_definition['layers'][key]['Z']
+ t = detector_definition['layers'][key]['thickness']
+ photoabsorption = x_sections[str(Z)]['dat']/1e10/x_sections[str(Z)]['photoabs_to_sigma']
+ lin = interp1d(x_sections[str(Z)]['ene'], photoabsorption,kind='linear')
+ mu = lin(energy_scale) * x_sections[str(Z)]['nominal_density']*100. #1/cm -> 1/m
+
+ absorption = np.exp(-mu * t)
+ response = response*absorption
+ Z = detector_definition['detector']['Z']
+ t = detector_definition['detector']['thickness']
+ photoabsorption = x_sections[str(Z)]['dat']/1e10/x_sections[str(Z)]['photoabs_to_sigma']
+ lin = interp1d(x_sections[str(Z)]['ene']/1000., photoabsorption,kind='linear')
+ mu = lin(energy_scale) * x_sections[str(Z)]['nominal_density']*100. #1/cm -> 1/m
+ response = response*(1.0 - np.exp(-mu * t))# * oo4pi;
+ return(response)
+
+
+[docs]def detect_peaks(dataset, minimum_number_of_peaks=30):
+ if not isinstance(dataset, sidpy.Dataset):
+ raise TypeError('Needs an sidpy dataset')
+ if not dataset.data_type.name == 'SPECTRUM':
+ raise TypeError('Need a spectrum')
+ resolution = 138
+ if 'EDS' in dataset.metadata:
+ if 'energy_resolution' in dataset.metadata['EDS']:
+ resolution = dataset.metadata['EDS']['energy_resolution']
+ start = np.searchsorted(dataset.energy_scale, 125)
+ ## we use half the width of the resolution for smearing
+ width = int(np.ceil(125/(dataset.energy_scale[1]-dataset.energy_scale[0])/2)+1)
+ new_spectrum = scipy.signal.savgol_filter(dataset[start:], width, 2) ## we use half the width of the resolution for smearing
+ #new_energy_scale = dataset.energy_scale[start:]
+ prominence = 10
+ minor_peaks, _ = scipy.signal.find_peaks(new_spectrum, prominence=prominence)
+
+ while len(minor_peaks) > minimum_number_of_peaks:
+ prominence+=10
+ minor_peaks, _ = scipy.signal.find_peaks(new_spectrum, prominence=prominence)
+ return np.array(minor_peaks)+start
+
+[docs]def find_elements(spectrum, minor_peaks):
+ if not isinstance(spectrum, sidpy.Dataset):
+ raise TypeError(' Need a sidpy dataset')
+ energy_scale = spectrum.energy_scale
+ elements = []
+ for peak in minor_peaks:
+ found = False
+ for element in range(3,82):
+ if 'lines' in x_sections[str(element)]:
+ if 'K-L3' in x_sections[str(element)]['lines']:
+ if abs(x_sections[str(element)]['lines']['K-L3']['position']- energy_scale[peak]) <10:
+ found = True
+ if x_sections[str(element)]['name'] not in elements:
+ elements.append( x_sections[str(element)]['name'])
+ if not found:
+ if 'K-L2' in x_sections[str(element)]['lines']:
+ if abs(x_sections[str(element)]['lines']['K-L2']['position']- energy_scale[peak]) <10:
+ found = True
+ if x_sections[str(element)]['name'] not in elements:
+ elements.append( x_sections[str(element)]['name'])
+ if not found:
+ if 'L3-M5' in x_sections[str(element)]['lines']:
+ if abs(x_sections[str(element)]['lines']['L3-M5']['position']- energy_scale[peak]) <30:
+ if x_sections[str(element)]['name'] not in elements:
+ elements.append( x_sections[str(element)]['name'])
+ return elements
+
+[docs]def get_x_ray_lines(spectrum, elements):
+ out_tags = {}
+ alpha_K = 1e6
+ alpha_L = 6.5e7
+ alpha_M = 8*1e8#2.2e10
+ # My Fit
+ alpha_K = .9e6
+ alpha_L = 6.e7
+ alpha_M = 6*1e8#2.2e10
+ # omega_K = Z**4/(alpha_K+Z**4)
+ # omega_L = Z**4/(alpha_L+Z**4)
+ # omega_M = Z**4/(alpha_M+Z**4)
+ for element in elements:
+
+ atomic_number = elements_list.index(element)
+ out_tags[element] ={'Z': atomic_number}
+ energy_scale = spectrum.energy_scale
+ if 'K-L3' in x_sections[str(atomic_number)]['lines']:
+ if x_sections[str(atomic_number)]['lines']['K-L3']['position'] < 1.9e4:
+ height = spectrum[np.searchsorted(energy_scale, x_sections[str(atomic_number)]['lines']['K-L3']['position'] )].compute()
+ out_tags[element]['K-family'] = {'height': height}
+ out_tags[element]['K-family']['yield'] = atomic_number**4/(alpha_K+atomic_number**4)/4/1.4
+
+ if 'L3-M5' in x_sections[str(atomic_number)]['lines']:
+ if x_sections[str(atomic_number)]['lines']['L3-M5']['position'] < 1.9e4:
+ height = spectrum[np.searchsorted(energy_scale, x_sections[str(atomic_number)]['lines']['L3-M5']['position'] )].compute()
+ out_tags[element]['L-family'] = {'height': height}
+ out_tags[element]['L-family']['yield'] = (atomic_number**4/(alpha_L+atomic_number**4))**2
+
+ if 'M5-N6' in x_sections[str(atomic_number)]['lines']:
+ if x_sections[str(atomic_number)]['lines']['M5-N6']['position'] < 1.9e4:
+ height = spectrum[np.searchsorted(energy_scale, x_sections[str(atomic_number)]['lines']['M5-N7']['position'] )].compute()
+ out_tags[element]['M-family'] = {'height': height}
+ out_tags[element]['M-family']['yield'] = (atomic_number**4/(alpha_M+atomic_number**4))**2
+
+ for key, line in x_sections[str(atomic_number)]['lines'].items():
+ other = True
+ if line['weight'] > 0.01 and line['position'] < 3e4:
+ if 'K-family' in out_tags[element]:
+ if key[0] == 'K':
+ other = False
+ out_tags[element]['K-family'][key]=line
+ if 'L-family' in out_tags[element]:
+ if key[:2] in ['L2', 'L3']:
+ other = False
+ out_tags[element]['L-family'][key]=line
+ if 'M-family' in out_tags[element]:
+ if key[:2] in ['M5', 'M4']:
+ other = False
+ out_tags[element]['M-family'][key]=line
+ if other:
+ if 'other' not in out_tags[element]:
+ out_tags[element]['other'] = {}
+ height = spectrum[np.searchsorted(energy_scale, x_sections[str(atomic_number)]['lines'][key]['position'] )].compute()
+ out_tags[element]['other'][key]=line
+ out_tags[element]['other'][key]['height'] = height
+
+ xs = get_eds_cross_sections(atomic_number)
+ if 'K' in xs and 'K-family' in out_tags[element]:
+ out_tags[element]['K-family']['ionization_x_section'] = xs['K']
+ if 'L' in xs and 'L-family' in out_tags[element]:
+ out_tags[element]['L-family']['ionization_x_section'] = xs['L']
+ if 'M' in xs and 'M-family' in out_tags[element]:
+ out_tags[element]['M-family']['ionization_x_section'] = xs['M']
+
+ """
+ for key, x_lines in out_tags.items():
+ if 'K-family' in x_lines:
+ xs = eels.xsec_xrpa(np.arange(100)+x_sections[str(x_lines['Z'])]['K1']['onset'], 200,x_lines['Z'], 100).sum()
+
+ x_lines['K-family']['ionization_x_section'] = xs
+
+ if 'L-family' in x_lines:
+ xs = eels.xsec_xrpa(np.arange(100)+x_sections[str(x_lines['Z'])]['L3']['onset'], 200,x_lines['Z'], 100).sum()
+ x_lines['L-family']['ionization_x_section'] = xs
+ if 'M-family' in x_lines:
+ xs = eels.xsec_xrpa(np.arange(100)+x_sections[str(x_lines['Z'])]['M5']['onset'], 200,x_lines['Z'], 100).sum()
+ x_lines['M-family']['ionization_x_section'] = xs
+ """
+ return out_tags
+
+
+
+
+[docs]def gaussian(enrgy_scale, mu, FWHM):
+ sig = FWHM/2/np.sqrt(2*np.log(2))
+ return np.exp(-np.power(enrgy_scale - mu, 2.) / (2 * np.power(sig, 2.)))
+
+[docs]def get_peak(E, energy_scale):
+ E_ref = 5895.0
+ FWHM_ref = 136 #eV
+ FWHM = getFWHM(E, E_ref, FWHM_ref)
+ gaus = gaussian(energy_scale, E, FWHM)
+
+ return gaus /gaus.sum()
+
+
+[docs]def get_model(tags, spectrum):
+
+ energy_scale = spectrum.energy_scale
+ p = []
+ peaks = []
+ keys = []
+ for element, lines in tags.items():
+ if 'K-family' in lines:
+ model = np.zeros(len(energy_scale))
+ for line, info in lines['K-family'].items():
+ if line[0] == 'K':
+ model += get_peak(info['position'], energy_scale)*info['weight']
+ lines['K-family']['peaks'] = model/model.max()
+ lines['K-family']['height'] /= lines['K-family']['peaks'].max()
+ p.append(lines['K-family']['height'])
+ peaks.append(lines['K-family']['peaks'])
+ keys.append(element+':K-family')
+ if 'L-family' in lines:
+ model = np.zeros(len(energy_scale))
+ for line, info in lines['L-family'].items():
+ if line[0] == 'L':
+ model += get_peak(info['position'], energy_scale)*info['weight']
+ lines['L-family']['peaks'] = model/model.max()
+ lines['L-family']['height'] /= lines['L-family']['peaks'].max()
+ p.append(lines['L-family']['height'])
+ peaks.append(lines['L-family']['peaks'])
+ keys.append(element+':L-family')
+ if 'M-family' in lines:
+ model = np.zeros(len(energy_scale))
+ for line, info in lines['M-family'].items():
+ if line[0] == 'M':
+ model += get_peak(info['position'], energy_scale)*info['weight']
+ lines['M-family']['peaks'] = model/model.max()
+ lines['M-family']['height'] /= lines['M-family']['peaks'].max()
+ p.append(lines['M-family']['height'])
+ peaks.append(lines['M-family']['peaks'])
+ keys.append(element+':M-family')
+
+ if 'other' in lines:
+ for line, info in lines['other'].items():
+ info['peak'] = get_peak(info['position'], energy_scale)
+ peaks.append(info['peak'])
+ p.append(info['height'])
+ keys.append(element+':other:'+line)
+ return np.array(peaks), np.array(p), keys
+
+[docs]def fit_model(spectrum, elements):
+ out_tags = get_x_ray_lines(spectrum, elements)
+
+ peaks, pin, keys = get_model(out_tags, spectrum)
+
+ def residuals(pp, yy):
+ model = np.zeros(len(yy))
+ for i in range(len(pp)):
+ model += peaks[i]*pp[i]
+ err = np.abs((yy - model)[75:]) / np.sqrt(np.abs(yy[75:]))
+ return err
+
+ y = spectrum.compute()
+ [p, _] = leastsq(residuals, pin, args=(y))
+ update_fit_values(out_tags, p)
+
+ if 'EDS' not in spectrum.metadata:
+ spectrum.metadata['EDS'] = {}
+ spectrum.metadata['EDS']['lines'] = out_tags
+
+ return np.array(peaks), np.array(p)
+
+
+[docs]def update_fit_values(out_tags, p):
+ index = 0
+ for element, lines in out_tags.items():
+ if 'K-family' in lines:
+ lines['K-family']['height'] = p[index]
+ index += 1
+ if 'L-family' in lines:
+ lines['L-family']['height'] = p[index]
+ index += 1
+ if 'M-family' in lines:
+ lines['M-family']['height'] =p[index]
+ index += 1
+ if 'other' in lines:
+ for line, info in lines['other'].items():
+ info['height'] = p[index]
+ index += 1
+
+
+[docs]def get_eds_xsection(Xsection, energy_scale, start_bgd, end_bgd):
+ background = eels.power_law_background(Xsection, energy_scale, [start_bgd, end_bgd], verbose=False)
+ cross_section_core = Xsection- background[0]
+ cross_section_core[cross_section_core < 0] = 0.0
+ cross_section_core[energy_scale < end_bgd] = 0.0
+ return cross_section_core
+
+
+[docs]def get_eds_cross_sections(z):
+ energy_scale = np.arange(10, 20000)
+ Xsection = eels.xsec_xrpa(energy_scale, 200, z, 400.)
+ edge_info = eels.get_x_sections(z)
+ eds_cross_sections = {}
+ if 'K1' in edge_info:
+ start_bgd = edge_info['K1']['onset'] * 0.8
+ end_bgd = edge_info['K1']['onset'] - 5
+ if start_bgd > end_bgd:
+ start_bgd = end_bgd-100
+ if start_bgd > energy_scale[0] and end_bgd< energy_scale[-1]-100:
+ eds_xsection = get_eds_xsection(Xsection, energy_scale, start_bgd, end_bgd)
+ eds_xsection = Xsection - eds_xsection
+ eds_xsection[eds_xsection<0] = 0.
+ start_sum = np.searchsorted(energy_scale, edge_info['K1']['onset'])
+ eds_cross_sections['K'] = eds_xsection[start_sum:start_sum+200].sum()
+ if 'L3' in edge_info:
+ start_bgd = edge_info['L3']['onset'] * 0.8
+ end_bgd = edge_info['L3']['onset'] - 5
+ if start_bgd > end_bgd:
+ start_bgd = end_bgd-100
+ if start_bgd > energy_scale[0] and end_bgd< energy_scale[-1]-100:
+ eds_xsection = get_eds_xsection(Xsection, energy_scale, start_bgd, end_bgd)
+ eds_xsection = Xsection - eds_xsection
+ eds_xsection[eds_xsection<0] = 0.
+ start_sum = np.searchsorted(energy_scale, edge_info['L3']['onset'])
+ eds_cross_sections['L'] = eds_xsection[start_sum:start_sum+200].sum()
+ if 'M5' in edge_info:
+ start_bgd = edge_info['M5']['onset'] * 0.8
+ end_bgd = edge_info['M5']['onset'] - 5
+ if start_bgd > end_bgd:
+ start_bgd = end_bgd-100
+ if start_bgd > energy_scale[0] and end_bgd< energy_scale[-1]-100:
+ eds_xsection = get_eds_xsection(Xsection, energy_scale, start_bgd, end_bgd)
+ eds_xsection = Xsection - eds_xsection
+ eds_xsection[eds_xsection<0] = 0.
+ start_sum = np.searchsorted(energy_scale, edge_info['M5']['onset'])
+ eds_cross_sections['M'] = eds_xsection[start_sum:start_sum+200].sum()
+ return eds_cross_sections
+
+"""
+QT dialog window for EELS compositional analysis
+
+Author: Gerd Duscher
+"""
+Qt_available = True
+try:
+ from PyQt5 import QtCore, QtWidgets
+except:
+ Qt_available = False
+ # print('Qt dialogs are not available')
+
+
+import numpy as np
+
+import ipywidgets
+import IPython.display
+from IPython.display import display
+import matplotlib
+import matplotlib.pylab as plt
+import matplotlib.patches as patches
+
+from pyTEMlib import file_tools as ft
+from pyTEMlib import eels_tools as eels
+from pyTEMlib import eels_dialog_utilities
+
+import sidpy
+
+if Qt_available:
+ from pyTEMlib import eels_dlg
+
+ class EELSDialog(QtWidgets.QDialog):
+ """
+ EELS Input Dialog for Chemical Analysis
+ """
+
+ def __init__(self, datasets=None):
+ super().__init__(None, QtCore.Qt.WindowStaysOnTopHint)
+ # Create an instance of the GUI
+ if datasets is None:
+ # make a dummy dataset
+ datasets = {'Channel_000':ft.make_dummy_dataset(sidpy.DataType.SPECTRUM)}
+ elif isinstance(datasets, sidpy.Dataset):
+ datasets = {'Channel_000': datasets}
+ elif isinstance(datasets, dict):
+ pass
+ else:
+ raise TypeError('dataset or first item inhas to be a sidpy dataset')
+ self.datasets = datasets
+ self.dataset = datasets[list(datasets)[0]]
+
+ if not isinstance(self.dataset, sidpy.Dataset):
+ raise TypeError('dataset or first item inhas to be a sidpy dataset')
+
+ self.spec_dim = ft.get_dimensions_by_type('spectral', self.dataset)
+ if len(self.spec_dim) != 1:
+ raise TypeError('We need exactly one SPECTRAL dimension')
+ self.spec_dim = self.spec_dim[0]
+
+ self.ui = eels_dlg.UiDialog(self)
+ # Run the .setup_ui() method to show the GUI
+ # self.ui.setup_ui(self)
+
+ self.set_action()
+
+ self.energy_scale = np.array([])
+ self.model = np.array([])
+ self.y_scale = 1.0
+ self.change_y_scale = 1.0
+ self.spectrum_ll = None
+ self.low_loss_key = None
+
+ self.edges = {}
+
+ self.show_regions = False
+ self.show()
+
+ self.set_dataset(self.dataset)
+ initial_elements = []
+
+ for key in self.edges:
+ if key.isdigit():
+ if 'element' in self.edges[key]:
+ initial_elements.append(self.edges[key]['element'])
+
+ self.pt_dialog = eels_dialog_utilities.PeriodicTableDialog(energy_scale=self.energy_scale,
+ initial_elements=initial_elements)
+ self.pt_dialog.signal_selected[list].connect(self.set_elements)
+
+ self.dataset.plot()
+
+ if hasattr(self.dataset.view, 'axes'):
+ self.axis = self.dataset.view.axes[-1]
+ elif hasattr(self.dataset.view, 'axis'):
+ self.axis = self.dataset.view.axis
+
+ self.figure = self.axis.figure
+ self.updY = 0
+ self.figure.canvas.mpl_connect('button_press_event', self.plot)
+
+ self.ui.do_fit_button.setFocus()
+ self.plot()
+ self.ui.do_fit_button.setFocus()
+
+ def set_dataset(self, dataset):
+
+ self.dataset = dataset
+ if 'edges' not in self.dataset.metadata or self.dataset.metadata['edges'] == {}:
+ self.dataset.metadata['edges'] = {'0': {}, 'model': {}, 'use_low_loss': False}
+ self.edges = self.dataset.metadata['edges']
+
+ spec_dim = ft.get_dimensions_by_type('spectral', dataset)[0]
+
+ if len(spec_dim) == 0:
+ raise TypeError('We need at least one SPECTRAL dimension')
+
+ self.spec_dim = spec_dim[0]
+ self.energy_scale = dataset._axes[self.spec_dim].values
+ self.ui.edit2.setText(f"{self.energy_scale[-2]:.3f}")
+
+ if 'fit_area' not in self.edges:
+ self.edges['fit_area'] = {}
+ if 'fit_start' not in self.edges['fit_area']:
+ self.ui.edit1.setText(f"{self.energy_scale[50]:.3f}")
+ self.edges['fit_area']['fit_start'] = float(self.ui.edit1.displayText())
+ else:
+ self.ui.edit1.setText(f"{self.edges['fit_area']['fit_start']:.3f}")
+ if 'fit_end' not in self.edges['fit_area']:
+ self.ui.edit2.setText(f"{self.energy_scale[-2]:.3f}")
+ self.edges['fit_area']['fit_end'] = float(self.ui.edit2.displayText())
+ else:
+ self.ui.edit2.setText(f"{self.edges['fit_area']['fit_end']:.3f}")
+
+ if self.dataset.data_type.name == 'SPECTRAL_IMAGE':
+ if 'SI_bin_x' not in self.dataset.metadata['experiment']:
+ self.dataset.metadata['experiment']['SI_bin_x'] = 1
+ self.dataset.metadata['experiment']['SI_bin_y'] = 1
+
+ bin_x = self.dataset.metadata['experiment']['SI_bin_x']
+ bin_y = self.dataset.metadata['experiment']['SI_bin_y']
+ self.dataset.view.set_bin([bin_x, bin_y])
+ self.update()
+
+ def update(self):
+ index = self.ui.list3.currentIndex() # which edge
+ edge = self.edges[str(index)]
+
+ if 'z' in edge:
+ self.ui.list5.setCurrentIndex(self.ui.edge_sym.index(edge['symmetry']))
+ self.ui.edit4.setText(str(edge['z']))
+ self.ui.unit4.setText(edge['element'])
+ self.ui.edit6.setText(f"{edge['onset']:.2f}")
+ self.ui.edit7.setText(f"{edge['start_exclude']:.2f}")
+ self.ui.edit8.setText(f"{edge['end_exclude']:.2f}")
+ if self.y_scale == 1.0:
+ self.ui.edit9.setText(f"{edge['areal_density']:.2e}")
+ self.ui.unit9.setText('a.u.')
+ else:
+ dispersion = self.energy_scale[1]-self.energy_scale[0]
+ self.ui.edit9.setText(f"{edge['areal_density']*self.y_scale*1e-6/dispersion:.2f}")
+ self.ui.unit9.setText('atoms/nm²')
+ else:
+ self.ui.list3.setCurrentIndex(0)
+ self.ui.edit4.setText(str(0))
+ self.ui.unit4.setText(' ')
+ self.ui.edit6.setText(f"{0:.2f}")
+ self.ui.edit7.setText(f"{0:.2f}")
+ self.ui.edit8.setText(f"{0:.2f}")
+ self.ui.edit9.setText(f"{0:.2e}")
+
+ def update_element(self, z):
+ # We check whether this element is already in the
+ zz = eels.get_z(z)
+ for key, edge in self.edges.items():
+ if key.isdigit():
+ if 'z' in edge:
+ if zz == edge['z']:
+ return False
+
+ major_edge = ''
+ minor_edge = ''
+ all_edges = {}
+ x_section = eels.get_x_sections(zz)
+ edge_start = 10 # int(15./ft.get_slope(self.energy_scale)+0.5)
+ for key in x_section:
+ if len(key) == 2 and key[0] in ['K', 'L', 'M', 'N', 'O'] and key[1].isdigit():
+ if self.energy_scale[edge_start] < x_section[key]['onset'] < self.energy_scale[-edge_start]:
+ if key in ['K1', 'L3', 'M5']:
+ major_edge = key
+ elif key in self.ui.edge_sym:
+ if minor_edge == '':
+ minor_edge = key
+ if int(key[-1]) % 2 > 0:
+ if int(minor_edge[-1]) % 2 == 0 or key[-1] > minor_edge[-1]:
+ minor_edge = key
+
+ all_edges[key] = {'onset': x_section[key]['onset'], 'original_onset': x_section[key]['onset']}
+
+
+ if major_edge != '':
+ key = major_edge
+ elif minor_edge != '':
+ key = minor_edge
+ else:
+ print(f'Could not find no edge of {zz} in spectrum')
+ return False
+
+ index = self.ui.list3.currentIndex()
+ # self.ui.dialog.setWindowTitle(f'{index}, {zz}')
+
+ if str(index) not in self.edges:
+ self.edges[str(index)] = {}
+
+ start_exclude = x_section[key]['onset'] - x_section[key]['excl before']
+ end_exclude = x_section[key]['onset'] + x_section[key]['excl after']
+
+ self.edges[str(index)] = {'z': zz, 'symmetry': key, 'element': eels.elements[zz],
+ 'onset': x_section[key]['onset'], 'end_exclude': end_exclude,
+ 'start_exclude': start_exclude}
+ self.edges[str(index)]['all_edges'] = all_edges
+ self.edges[str(index)]['chemical_shift'] = 0.0
+ self.edges[str(index)]['areal_density'] = 0.0
+ self.edges[str(index)]['original_onset'] = self.edges[str(index)]['onset']
+ return True
+
+ def on_enter(self):
+ sender = self.sender()
+ edge_list = self.ui.list3
+ # self.ui.dialog.setWindowTitle(f"{sender.objectName()}")
+
+
+ if sender.objectName() == 'fit_start_edit':
+ value = float(str(sender.displayText()).strip())
+ if value < self.energy_scale[0]:
+ value = self.energy_scale[0]
+ if value > self.energy_scale[-5]:
+ value = self.energy_scale[-5]
+ self.edges['fit_area']['fit_start'] = value
+ sender.setText(str(self.edges['fit_area']['fit_start']))
+ elif sender.objectName() == 'fit_end_edit':
+ value = float(str(sender.displayText()).strip())
+ if value < self.energy_scale[5]:
+ value = self.energy_scale[5]
+ if value > self.energy_scale[-1]:
+ value = self.energy_scale[-1]
+ self.edges['fit_area']['fit_end'] = value
+ sender.setText(str(self.edges['fit_area']['fit_end']))
+ elif sender.objectName() == 'element_edit':
+ if str(sender.displayText()).strip() == '0':
+ # sender.setText('PT')
+ self.pt_dialog.energy_scale = self.energy_scale
+ self.pt_dialog.show()
+ pass
+ else:
+ self.update_element(str(sender.displayText()).strip())
+ self.update()
+ elif sender.objectName() in ['onset_edit', 'excl_start_edit', 'excl_end_edit']:
+ self.check_area_consistency()
+
+ elif sender.objectName() == 'multiplier_edit':
+ index = edge_list.currentIndex()
+ self.edges[str(index)]['areal_density'] = float(self.ui.edit9.displayText())
+ if self.y_scale != 1.0:
+ dispersion = self.energy_scale[1]-self.energy_scale[0]
+ self.edges[str(index)]['areal_density'] /= self.y_scale * 1e-6 *dispersion
+ if 'background' not in self.edges['model']:
+ print(' no background')
+ return
+ self.model = self.edges['model']['background']
+ for key in self.edges:
+ if key.isdigit():
+ self.model = self.model + self.edges[key]['areal_density'] * self.edges[key]['data']
+ self.plot()
+ else:
+ return
+ if self.show_regions:
+ self.plot()
+
+
+
+ def sort_elements(self):
+ onsets = []
+ for index, edge in self.edges.items():
+ if index.isdigit():
+ onsets.append(float(edge['onset']))
+
+ arg_sorted = np.argsort(onsets)
+ edges = self.edges.copy()
+ for index, i_sorted in enumerate(arg_sorted):
+ self.edges[str(index)] = edges[str(i_sorted)].copy()
+
+ index = 0
+ edge = self.edges['0']
+ dispersion = self.energy_scale[1]-self.energy_scale[0]
+
+ while str(index + 1) in self.edges:
+ next_edge = self.edges[str(index + 1)]
+ if edge['end_exclude'] > next_edge['start_exclude'] - 5 * dispersion:
+ edge['end_exclude'] = next_edge['start_exclude'] - 5 * dispersion
+ edge = next_edge
+ index += 1
+
+ if edge['end_exclude'] > self.energy_scale[-3]:
+ edge['end_exclude'] = self.energy_scale[-3]
+
+ def set_elements(self, selected_elements):
+ edge_list = self.ui.list3
+
+ for index, elem in enumerate(selected_elements):
+ edge_list.setCurrentIndex(index)
+ self.update_element(elem)
+
+ self.sort_elements()
+ self.update()
+
+ def plot(self, event=None):
+ self.energy_scale = self.dataset._axes[self.spec_dim].values
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ spectrum = self.dataset.view.get_spectrum()
+ self.axis = self.dataset.view.axes[1]
+ else:
+ spectrum = np.array(self.dataset)
+ self.axis = self.dataset.view.axis
+
+ if self.ui.select10.isChecked():
+ if 'experiment' in self.dataset.metadata:
+ exp = self.dataset.metadata['experiment']
+ if 'convergence_angle' not in exp:
+ raise ValueError('need a convergence_angle in experiment of metadata dictionary ')
+ alpha = exp['convergence_angle']
+ beta = exp['collection_angle']
+ beam_kv = exp['acceleration_voltage']
+
+ eff_beta = eels.effective_collection_angle(self.energy_scale, alpha, beta, beam_kv)
+ edges = eels.make_cross_sections(self.edges, np.array(self.energy_scale), beam_kv, eff_beta)
+ self.edges = eels.fit_edges2(spectrum, self.energy_scale, edges)
+ areal_density = []
+ elements = []
+ for key in edges:
+ if key.isdigit(): # only edges have numbers in that dictionary
+ elements.append(edges[key]['element'])
+ areal_density.append(edges[key]['areal_density'])
+ areal_density = np.array(areal_density)
+ out_string = '\nRelative composition: \n'
+ for i, element in enumerate(elements):
+ out_string += f'{element}: {areal_density[i] / areal_density.sum() * 100:.1f}% '
+
+ self.model = self.edges['model']['spectrum']
+ self.update()
+
+ x_limit = self.axis.get_xlim()
+ y_limit = np.array(self.axis.get_ylim())*self.change_y_scale
+ self.change_y_scale = 1.0
+
+ self.axis.clear()
+
+ line1, = self.axis.plot(self.energy_scale, spectrum*self.y_scale, label='spectrum')
+ lines = [line1]
+
+ def onpick(event):
+ # on the pick event, find the orig line corresponding to the
+ # legend proxy line, and toggle the visibility
+ leg_line = event.artist
+ orig_line = lined[legline]
+ vis = not origline.get_visible()
+ orig_line.set_visible(vis)
+ # Change the alpha on the line in the legend, so we can see what lines
+ # have been toggled
+ if vis:
+ leg_line.set_alpha(1.0)
+ else:
+ leg_line.set_alpha(0.2)
+ self.figure.canvas.draw()
+
+ if len(self.model) > 1:
+ line2, = self.axis.plot(self.energy_scale, self.model*self.y_scale, label='model')
+ line3, = self.axis.plot(self.energy_scale, (spectrum - self.model)*self.y_scale, label='difference')
+ line4, = self.axis.plot(self.energy_scale, (spectrum - self.model) / np.sqrt(spectrum)*self.y_scale, label='Poisson')
+ lines = [line1, line2, line3, line4]
+ lined = dict()
+
+ legend = self.axis.legend(loc='upper right', fancybox=True, shadow=True)
+
+ legend.get_frame().set_alpha(0.4)
+ for legline, origline in zip(legend.get_lines(), lines):
+ legline.set_picker(5) # 5 pts tolerance
+ lined[legline] = origline
+ self.figure.canvas.mpl_connect('pick_event', onpick)
+ self.axis.set_xlim(x_limit)
+ self.axis.set_ylim(y_limit)
+
+ if self.y_scale != 1.:
+ self.axis.set_ylabel('scattering intensity (ppm)')
+ else:
+ self.axis.set_ylabel('intensity (counts)')
+ self.axis.set_xlabel('energy_loss (eV)')
+
+
+ if self.ui.show_edges.isChecked():
+ self.show_edges()
+ if self.show_regions:
+ self.plot_regions()
+ self.figure.canvas.draw_idle()
+
+ def plot_regions(self):
+ y_min, y_max = self.axis.get_ylim()
+ height = y_max - y_min
+
+ rect = []
+ if 'fit_area' in self.edges:
+ color = 'blue'
+ alpha = 0.2
+ x_min = self.edges['fit_area']['fit_start']
+ width = self.edges['fit_area']['fit_end'] - x_min
+ rect.append(patches.Rectangle((x_min, y_min), width, height,
+ edgecolor=color, alpha=alpha, facecolor=color))
+ self.axis.add_patch(rect[0])
+ self.axis.text(x_min, y_max, 'fit region', verticalalignment='top')
+ color = 'red'
+ alpha = 0.5
+ for key in self.edges:
+ if key.isdigit():
+ x_min = self.edges[key]['start_exclude']
+ width = self.edges[key]['end_exclude']-x_min
+ rect.append(patches.Rectangle((x_min, y_min), width, height,
+ edgecolor=color, alpha=alpha, facecolor=color))
+ self.axis.add_patch(rect[-1])
+ self.axis.text(x_min, y_max, f"exclude\n edge {int(key)+1}", verticalalignment='top')
+
+ def show_edges(self):
+ x_min, x_max = self.axis.get_xlim()
+ y_min, y_max = self.axis.get_ylim()
+
+ for key, edge in self.edges.items():
+ i = 0
+ if key.isdigit():
+ element = edge['element']
+ for sym in edge['all_edges']:
+ x = edge['all_edges'][sym]['onset'] + edge['chemical_shift']
+ if x_min < x < x_max:
+ self.axis.text(x, y_max, '\n' * i + f"{element}-{sym}",
+ verticalalignment='top', color='black')
+ self.axis.axvline(x, ymin=0, ymax=1, color='gray')
+ i += 1
+
+ def check_area_consistency(self):
+ if self.dataset is None:
+ return
+ onset = float(self.ui.edit6.displayText())
+ excl_start = float(self.ui.edit7.displayText())
+ excl_end = float(self.ui.edit8.displayText())
+ if onset < self.energy_scale[2]:
+ onset = self.energy_scale[2]
+ excl_start = self.energy_scale[2]
+ if onset > self.energy_scale[-2]:
+ onset = self.energy_scale[-2]
+ excl_end = self.energy_scale[-2]
+ if excl_start > onset:
+ excl_start = onset
+ if excl_end < onset:
+ excl_end = onset
+
+ index = self.ui.list3.currentIndex()
+ self.edges[str(index)]['chemical_shift'] = onset - self.edges[str(index)]['original_onset']
+ self.edges[str(index)]['onset'] = onset
+ self.edges[str(index)]['end_exclude'] = excl_end
+ self.edges[str(index)]['start_exclude'] = excl_start
+
+ self.update()
+
+ def on_list_enter(self):
+ sender = self.sender()
+ # self.ui.dialog.setWindowTitle(f"on list eneter {sender.objectName()}")
+
+ if sender.objectName() == 'edge_list':
+ index = self.ui.list3.currentIndex()
+
+ number_of_edges = 0
+ for key in self.edges:
+ if key.isdigit():
+ if int(key) > number_of_edges:
+ number_of_edges = int(key)
+ number_of_edges += 1
+ if index > number_of_edges:
+ index = number_of_edges
+ self.ui.list3.setCurrentIndex(index)
+ if str(index) not in self.edges:
+ self.edges[str(index)] = {'z': 0, 'symmetry': 'K1', 'element': 'H', 'onset': 0, 'end_exclude': 0,
+ 'start_exclude': 0, 'areal_density': 0}
+
+ self.update()
+ elif sender.objectName() == 'symmetry_list':
+ sym = self.ui.list5.currentText()
+ index = self.ui.list3.currentIndex()
+ zz = self.edges[str(index)]['z']
+ if zz > 1:
+ x_section = eels.get_x_sections(zz)
+ if sym in x_section:
+ start_exclude = x_section[sym]['onset'] - x_section[sym]['excl before']
+ end_exclude = x_section[sym]['onset'] + x_section[sym]['excl after']
+ self.edges[str(index)].update({'symmetry': sym, 'onset': x_section[sym]['onset'],
+ 'end_exclude': end_exclude, 'start_exclude': start_exclude})
+ self.edges[str(index)]['chemical_shift'] = 0.0
+ self.edges[str(index)]['areal_density'] = 0.0
+ self.edges[str(index)]['original_onset'] = self.edges[index]['onset']
+ self.update()
+ elif sender.objectName() == 'symmetry_method':
+ self.ui.select5.setCurrentIndex(0)
+
+ def on_check(self):
+ sender = self.sender()
+ # self.ui.dialog.setWindowTitle(f"on_check {sender.objectName()}")
+
+
+ if sender.objectName() == 'edge_check':
+ self.show_regions = sender.isChecked()
+ elif sender.objectName() == 'conv_ll':
+ self.edges['use_low_loss'] = self.ui.check10.isChecked()
+ if self.ui.check10.isChecked():
+ self.low_loss()
+ elif sender.objectName() == 'probability':
+ dispersion = self.energy_scale[1]-self.energy_scale[0]
+ old_y_scale = self.y_scale *1.
+ if sender.isChecked():
+ flux_key = None
+ spectrum_key = None
+
+ for key in self.datasets.keys():
+ if 'Reference' in key:
+ if self.datasets[key].data_type.name == 'IMAGE': # Prefer Ronchigrams
+ flux_key = key
+ self.dataset.metadata['experiment']['flux_reference_key'] = flux_key
+ elif self.datasets[key].data_type.name == 'SPECTRUM':
+ spectrum_key = key
+ self.dataset.metadata['experiment']['low_loss_key'] = spectrum_key
+ if flux_key is None:
+ flux_key = spectrum_key
+
+ # self.ui.dialog.setWindowTitle(f"2nd {self.dataset.metadata['experiment']['flux_ppm']:.2f}")
+ if self.dataset.metadata['experiment']['flux_ppm'] > 0:
+ # self.ui.dialog.setWindowTitle(f"3rD {self.dataset.metadata['experiment']['flux_ppm']:.2f}")
+ self.y_scale = 1/self.dataset.metadata['experiment']['flux_ppm']*dispersion
+ elif flux_key is not None:
+ self.dataset.metadata['experiment']['flux_ppm'] = (np.array(self.datasets[flux_key])/1e6).sum()
+ self.dataset.metadata['experiment']['flux_ppm'] /= self.datasets[flux_key].metadata['experiment']['exposure_time']
+ self.dataset.metadata['experiment']['flux_ppm'] *= self.dataset.metadata['experiment']['exposure_time']
+ self.y_scale = 1/self.dataset.metadata['experiment']['flux_ppm']*dispersion
+ else:
+ self.y_scale = 1.0
+ else:
+ self.y_scale = 1.0
+
+ self.change_y_scale = self.y_scale/old_y_scale
+ self.update()
+ self.plot()
+
+ def low_loss(self):
+ self.edges['use_low_loss'] = self.ui.check10.isChecked()
+ if self.low_loss_key is None:
+ for key in self.datasets.keys():
+ if 'Reference' in key:
+ if self.datasets[key].data_type.name == 'SPECTRUM':
+ self.low_loss_key = key
+ self.dataset.metadata['experiment']['low_loss_key'] = self.low_loss_key
+
+ if self.low_loss_key is None:
+ self.low_loss_key = ft.add_dataset_from_file(self.datasets, key_name='Reference')
+ self.spectrum_ll = self.datasets[self.low_loss_key]
+ if self.spectrum_ll.data_type.name != 'SPECTRUM':
+ self.spectrum_ll = None
+ self.low_loss_key = None
+
+ if self.low_loss_key is not None:
+ self.spectrum_ll = self.datasets[self.low_loss_key]
+ if 'number_of_frames' in self.spectrum_ll.metadata['experiment']:
+ self.spectrum_ll.metadata['experiment']['exposure_time'] = \
+ self.spectrum_ll.metadata['experiment']['single_exposure_time'] * \
+ self.spectrum_ll.metadata['experiment']['number_of_frames']
+
+ def do_all_button_click(self):
+
+ if self.dataset.data_type.name != 'SPECTRAL_IMAGE':
+ self.do_fit_button_click()
+ return
+
+ if 'experiment' in self.dataset.metadata:
+ exp = self.dataset.metadata['experiment']
+ if 'convergence_angle' not in exp:
+ raise ValueError('need a convergence_angle in experiment of metadata dictionary ')
+ alpha = exp['convergence_angle']
+ beta = exp['collection_angle']
+ beam_kv = exp['acceleration_voltage']
+ else:
+ raise ValueError('need a experiment parameter in metadata dictionary')
+
+ self.energy_scale = self.dataset._axes[self.spec_dim].values
+ eff_beta = eels.effective_collection_angle(self.energy_scale, alpha, beta, beam_kv)
+ if self.edges['use_low_loss']:
+ low_loss = np.array(self.spectrum_ll)/self.spectrum_ll.sum()
+ else:
+ low_loss = None
+
+ edges = eels.make_cross_sections(self.edges, np.array(self.energy_scale), beam_kv, eff_beta,
+ low_loss=low_loss)
+
+ view = self.dataset.view
+ bin_x = view.bin_x
+ bin_y = view.bin_y
+
+ start_x = view.x
+ start_y = view.y
+
+ number_of_edges = 0
+ for key in self.edges:
+ if key.isdigit():
+ number_of_edges += 1
+
+ results = np.zeros([int(self.dataset.shape[0]/bin_x), int(self.dataset.shape[1]/bin_y), number_of_edges])
+ total_spec = int(self.dataset.shape[0]/bin_x)*int(self.dataset.shape[1]/bin_y)
+ self.ui.progress.setMaximum(total_spec)
+ self.ui.progress.setValue(0)
+ ind = 0
+ for x in range(int(self.dataset.shape[0]/bin_x)):
+
+ for y in range(int(self.dataset.shape[1]/bin_y)):
+ ind += 1
+ self.ui.progress.setValue(ind)
+ view.x = x*bin_x
+ view.y = y*bin_y
+ spectrum = view.get_spectrum()
+
+ edges = eels.fit_edges2(spectrum, self.energy_scale, edges)
+ for key, edge in edges.items():
+ if key.isdigit():
+ # element.append(edge['element'])
+ results[x, y, int(key)] = edge['areal_density']
+ edges['spectrum_image_quantification'] = results
+ self.ui.progress.setValue(total_spec)
+ view.x = start_x
+ view.y = start_y
+
+ def do_fit_button_click(self):
+ if 'experiment' in self.dataset.metadata:
+ exp = self.dataset.metadata['experiment']
+ if 'convergence_angle' not in exp:
+ raise ValueError('need a convergence_angle in experiment of metadata dictionary ')
+ alpha = exp['convergence_angle']
+ beta = exp['collection_angle']
+ beam_kv = exp['acceleration_voltage']
+
+ else:
+ raise ValueError('need a experiment parameter in metadata dictionary')
+ self.energy_scale = self.dataset._axes[self.spec_dim].values
+ eff_beta = eels.effective_collection_angle(self.energy_scale, alpha, beta, beam_kv)
+
+ if self.edges['use_low_loss']:
+ low_loss = self.spectrum_ll / self.spectrum_ll.sum()
+ else:
+ low_loss = None
+ edges = eels.make_cross_sections(self.edges, np.array(self.energy_scale), beam_kv, eff_beta, low_loss)
+
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ spectrum = self.dataset.view.get_spectrum()
+ else:
+ spectrum = self.dataset
+ self.edges = eels.fit_edges2(spectrum, self.energy_scale, edges)
+ areal_density = []
+ elements = []
+ for key in edges:
+ if key.isdigit(): # only edges have numbers in that dictionary
+ elements.append(edges[key]['element'])
+ areal_density.append(edges[key]['areal_density'])
+ areal_density = np.array(areal_density)
+ out_string = '\nRelative composition: \n'
+ for i, element in enumerate(elements):
+ out_string += f'{element}: {areal_density[i] / areal_density.sum() * 100:.1f}% '
+
+ self.model = self.edges['model']['spectrum']
+ self.update()
+ self.plot()
+
+ def do_auto_id_button_click(self):
+ # self.ui.dialog.setWindowTitle(f"auto id ")
+ self.ui.do_fit_button.setFocus()
+
+ if '0' not in self.edges:
+ self.edges['0'] ={}
+ found_edges = eels.auto_id_edges(self.dataset)
+
+ to_delete = []
+ if len(found_edges) >0:
+ for key in self.edges:
+ if key.isdigit():
+ to_delete.append(key)
+ for key in to_delete:
+ del self.edges[key]
+ if len(to_delete) == 0:
+ self.edges['0'] = {}
+
+ selected_elements = []
+ for key in found_edges:
+ selected_elements.append(key)
+ self.set_elements(selected_elements)
+
+ for button in self.pt_dialog.button:
+ if button.text() in selected_elements:
+ button.setChecked(True)
+ else:
+ button.setChecked(False)
+ self.update()
+
+ def do_select_button_click(self):
+ self.pt_dialog.energy_scale = self.energy_scale
+ self.pt_dialog.show()
+ self.update()
+
+ def set_action(self):
+ self.ui.edit1.editingFinished.connect(self.on_enter)
+ self.ui.edit2.editingFinished.connect(self.on_enter)
+ self.ui.list3.activated[str].connect(self.on_list_enter)
+ self.ui.check3.clicked.connect(self.on_check)
+ self.ui.edit4.editingFinished.connect(self.on_enter)
+ self.ui.list5.activated[str].connect(self.on_list_enter)
+ self.ui.select5.activated[str].connect(self.on_list_enter)
+
+ self.ui.edit6.editingFinished.connect(self.on_enter)
+ self.ui.edit7.editingFinished.connect(self.on_enter)
+ self.ui.edit8.editingFinished.connect(self.on_enter)
+ self.ui.edit9.editingFinished.connect(self.on_enter)
+
+ self.ui.check10.clicked.connect(self.on_check)
+ self.ui.select10.clicked.connect(self.on_check)
+ self.ui.show_edges.clicked.connect(self.on_check)
+ self.ui.check_probability.clicked.connect(self.on_check)
+
+ self.ui.do_all_button.clicked.connect(self.do_all_button_click)
+ self.ui.do_fit_button.clicked.connect(self.do_fit_button_click)
+ self.ui.auto_id_button.clicked.connect(self.do_auto_id_button_click)
+ self.ui.select_button.clicked.connect(self.do_select_button_click)
+
+
+ class CurveVisualizer(object):
+ """Plots a sidpy.Dataset with spectral dimension-type
+
+ """
+ def __init__(self, dset, spectrum_number=None, axis=None, leg=None, **kwargs):
+ if not isinstance(dset, sidpy.Dataset):
+ raise TypeError('dset should be a sidpy.Dataset object')
+ if axis is None:
+ self.fig = plt.figure()
+ self.axis = self.fig.add_subplot(1, 1, 1)
+ else:
+ self.axis = axis
+ self.fig = axis.figure
+
+ self.dset = dset
+ self.selection = []
+ [self.spec_dim, self.energy_scale] = ft.get_dimensions_by_type('spectral', self.dset)[0]
+
+ self.lined = dict()
+ self.plot(**kwargs)
+
+ def plot(self, **kwargs):
+ if self.dset.data_type.name == 'IMAGE_STACK':
+ line1, = self.axis.plot(self.energy_scale.values, self.dset[0, 0], label='spectrum', **kwargs)
+ else:
+ line1, = self.axis.plot(self.energy_scale.values, self.dset, label='spectrum', **kwargs)
+ lines = [line1]
+ if 'add2plot' in self.dset.metadata:
+ data = self.dset.metadata['add2plot']
+ for key, line in data.items():
+ line_add, = self.axis.plot(self.energy_scale.values, line['data'], label=line['legend'])
+ lines.append(line_add)
+
+ legend = self.axis.legend(loc='upper right', fancybox=True, shadow=True)
+ legend.get_frame().set_alpha(0.4)
+
+ for legline, origline in zip(legend.get_lines(), lines):
+ legline.set_picker(True)
+ legline.set_pickradius(5) # 5 pts tolerance
+ self.lined[legline] = origline
+ self.fig.canvas.mpl_connect('pick_event', self.onpick)
+
+ self.axis.axhline(0, color='gray', alpha=0.6)
+ self.axis.set_xlabel(self.dset.labels[0])
+ self.axis.set_ylabel(self.dset.data_descriptor)
+ self.axis.ticklabel_format(style='sci', scilimits=(-2, 3))
+ self.fig.canvas.draw_idle()
+
+ def update(self, **kwargs):
+ x_limit = self.axis.get_xlim()
+ y_limit = self.axis.get_ylim()
+ self.axis.clear()
+ self.plot(**kwargs)
+ self.axis.set_xlim(x_limit)
+ self.axis.set_ylim(y_limit)
+
+ def onpick(self, event):
+ # on the pick event, find the orig line corresponding to the
+ # legend proxy line, and toggle the visibility
+ legline = event.artist
+ origline = self.lined[legline]
+ vis = not origline.get_visible()
+ origline.set_visible(vis)
+ # Change the alpha on the line in the legend, so we can see what lines
+ # have been toggled
+ if vis:
+ legline.set_alpha(1.0)
+ else:
+ legline.set_alpha(0.2)
+ self.fig.canvas.draw()
+
+[docs]def get_sidebar():
+ side_bar = ipywidgets.GridspecLayout(13, 3,width='auto', grid_gap="0px")
+
+
+ row = 0
+ side_bar[row, :3] = ipywidgets.ToggleButton(description='Fit Area',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ tooltip='Shows fit regions and regions excluded from fit',
+ button_style='info') #ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Fit Start:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Fit End:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+
+ row += 1
+
+ side_bar[row, :3] = ipywidgets.Button(description='Elements',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=[('Edge 1', 0), ('Edge 2', 1), ('Edge 3', 2), ('Edge 4', 3),('Add Edge', -1)],
+ value=0,
+ description='Edges:',
+ disabled=False,
+ layout=ipywidgets.Layout(width='200px'))
+ """side_bar[row,2] = ipywidgets.ToggleButton(
+ description='Regions',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Shows fit regions and regions excluded from fit',
+ layout=ipywidgets.Layout(width='100px')
+ )
+ """
+ row += 1
+ side_bar[row, :2] = ipywidgets.IntText(value=7.5,description='Z:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=['K1','L3', 'M5', 'M3', 'M1', 'N7', 'N5', 'N3', 'N1'],
+ value='K1',
+ description='Symmetry:',
+ disabled=False,
+ layout=ipywidgets.Layout(width='200px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Onset:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Excl.Start:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Excl.End:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Mutliplier:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="a.u.", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+
+ side_bar[row, :3] = ipywidgets.Button(description='Quantification',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+
+ row += 1
+ side_bar[row,0] = ipywidgets.ToggleButton(
+ description='Probabiity',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Changes y-axis to probability of flux is given',
+ layout=ipywidgets.Layout(width='100px')
+ )
+ side_bar[row,1] = ipywidgets.ToggleButton(
+ description='Conv.LL',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Changes y-axis to probability of flux is given',
+ layout=ipywidgets.Layout(width='100px')
+ )
+ side_bar[row,2] = ipywidgets.ToggleButton(
+ description='Show Edges',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Changes y-axis to probability of flux is given',
+ layout=ipywidgets.Layout(width='100px')
+ )
+ return side_bar
+
+
+import ipywidgets
+
+
+[docs]class CompositionWidget(object):
+ def __init__(self, datasets=None, index=0):
+
+ if not isinstance(datasets, dict):
+ raise TypeError('dataset or first item has to be a sidpy dataset')
+ self.datasets = datasets
+ self.dataset = datasets[list(datasets)[0]]
+ self.model = []
+ self.sidebar = get_sidebar()
+
+ self.set_dataset()
+
+ self.periodic_table = eels_dialog_utilities.PeriodicTableWidget(self.energy_scale)
+ self.elements_cancel_button = ipywidgets.Button(description='Cancel')
+ self.elements_select_button = ipywidgets.Button(description='Select')
+ self.elements_auto_button = ipywidgets.Button(description='Auto ID')
+
+ self.periodic_table_panel = ipywidgets.VBox([self.periodic_table.periodic_table,
+ ipywidgets.HBox([self.elements_cancel_button, self.elements_auto_button, self.elements_select_button])])
+
+
+ self.app_layout = ipywidgets.AppLayout(
+ left_sidebar=self.sidebar,
+ center=self.view.panel,
+ footer=None,#message_bar,
+ pane_heights=[0, 10, 0],
+ pane_widths=[4, 10, 0],
+ )
+ self.set_action()
+ display(self.app_layout)
+
+
+ def line_select_callback(self, x_min, x_max):
+ self.start_cursor.value = np.round(x_min,3)
+ self.end_cursor.value = np.round(x_max, 3)
+ self.start_channel = np.searchsorted(self.datasets[self.key].energy_loss, self.start_cursor.value)
+ self.end_channel = np.searchsorted(self.datasets[self.key].energy_loss, self.end_cursor.value)
+
+
+ def plot(self, scale=True):
+ self.view.change_y_scale = self.change_y_scale
+ self.view.y_scale = self.y_scale
+ self.energy_scale = self.dataset.energy_loss.values
+
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ spectrum = self.dataset.view.get_spectrum()
+ else:
+ spectrum = self.dataset
+ if len(self.model) > 1:
+ additional_spectra = {'model': self.model,
+ 'difference': spectrum-self.model}
+ else:
+ additional_spectra = None
+ self.view.plot(scale=True, additional_spectra=additional_spectra )
+ self.change_y_scale = 1.
+
+ if self.sidebar[12, 2].value:
+ self.show_edges()
+ if self.sidebar[0, 0].value:
+ self.plot_regions()
+ self.view.figure.canvas.draw_idle()
+
+
+ def plot_regions(self):
+ axis = self.view.figure.gca()
+ y_min, y_max = axis.get_ylim()
+ height = y_max - y_min
+
+ rect = []
+ if 'fit_area' in self.edges:
+ color = 'blue'
+ alpha = 0.2
+ x_min = self.edges['fit_area']['fit_start']
+ width = self.edges['fit_area']['fit_end'] - x_min
+ rect.append(patches.Rectangle((x_min, y_min), width, height,
+ edgecolor=color, alpha=alpha, facecolor=color))
+ axis.add_patch(rect[0])
+ axis.text(x_min, y_max, 'fit region', verticalalignment='top')
+ color = 'red'
+ alpha = 0.5
+
+ for key in self.edges:
+ if key.isdigit():
+ x_min = self.edges[key]['start_exclude']
+ width = self.edges[key]['end_exclude']-x_min
+ rect.append(patches.Rectangle((x_min, y_min), width, height,
+ edgecolor=color, alpha=alpha, facecolor=color))
+ axis.add_patch(rect[-1])
+ axis.text(x_min, y_max, f"exclude\n edge {int(key)+1}", verticalalignment='top')
+
+ def show_edges(self):
+ axis = self.view.figure.gca()
+ x_min, x_max = axis.get_xlim()
+ y_min, y_max = axis.get_ylim()
+
+ for key, edge in self.edges.items():
+ i = 0
+ if key.isdigit():
+ element = edge['element']
+ for sym in edge['all_edges']:
+ x = edge['all_edges'][sym]['onset'] + edge['chemical_shift']
+ if x_min < x < x_max:
+ axis.text(x, y_max, '\n' * i + f"{element}-{sym}",
+ verticalalignment='top', color='black')
+ axis.axvline(x, ymin=0, ymax=1, color='gray')
+ i += 1
+
+
+
+
+ def set_dataset(self, index=0):
+ spec_dim = self.dataset.get_dimensions_by_type(sidpy.DimensionType.SPECTRAL)
+ self.spec_dim = self.dataset._axes[spec_dim[0]]
+
+ self.energy_scale = self.spec_dim.values
+ self.dataset.metadata['experiment']['offset'] = self.energy_scale[0]
+ self.dataset.metadata['experiment']['dispersion'] = self.energy_scale[1] - self.energy_scale[0]
+ if 'edges' not in self.dataset.metadata or self.dataset.metadata['edges'] == {}:
+ self.dataset.metadata['edges'] = {'0': {}, 'model': {}, 'use_low_loss': False}
+
+ self.edges = self.dataset.metadata['edges']
+ if '0' not in self.edges:
+ self.edges['0'] = {}
+
+ if 'fit_area' not in self.edges:
+ self.edges['fit_area'] = {}
+ if 'fit_start' not in self.edges['fit_area']:
+ self.sidebar[1,0].value = np.round(self.energy_scale[50], 3)
+ self.edges['fit_area']['fit_start'] = self.sidebar[1,0].value
+ else:
+ self.sidebar[1,0].value = np.round(self.edges['fit_area']['fit_start'],3)
+ if 'fit_end' not in self.edges['fit_area']:
+ self.sidebar[2,0].value = np.round(self.energy_scale[-2], 3)
+ self.edges['fit_area']['fit_end'] = self.sidebar[2,0].value
+ else:
+ self.sidebar[2,0].value = np.round(self.edges['fit_area']['fit_end'],3)
+
+ if self.dataset.data_type.name == 'SPECTRAL_IMAGE':
+ if 'SI_bin_x' not in self.dataset.metadata['experiment']:
+ self.dataset.metadata['experiment']['SI_bin_x'] = 1
+ self.dataset.metadata['experiment']['SI_bin_y'] = 1
+
+ bin_x = self.dataset.metadata['experiment']['SI_bin_x']
+ bin_y = self.dataset.metadata['experiment']['SI_bin_y']
+ # self.dataset.view.set_bin([bin_x, bin_y])
+ if self.dataset.data_type.name =='SPECTRAL_IMAGE':
+ self.view = eels_dialog_utilities.SIPlot(self.dataset)
+ else:
+ self.view = eels_dialog_utilities.SpectrumPlot(self.dataset)
+ self.y_scale = 1.0
+ self.change_y_scale = 1.0
+
+ self.update()
+
+ def update_element(self, z=0, index=-1):
+ # We check whether this element is already in the
+ if z == 0:
+ z = self.sidebar[5,0].value
+
+ zz = eels.get_z(z)
+ for key, edge in self.edges.items():
+ if key.isdigit():
+ if 'z' in edge:
+ if zz == edge['z']:
+ return False
+
+ major_edge = ''
+ minor_edge = ''
+ all_edges = {}
+ x_section = eels.get_x_sections(zz)
+ edge_start = 10 # int(15./ft.get_slope(self.energy_scale)+0.5)
+ for key in x_section:
+ if len(key) == 2 and key[0] in ['K', 'L', 'M', 'N', 'O'] and key[1].isdigit():
+ if self.energy_scale[edge_start] < x_section[key]['onset'] < self.energy_scale[-edge_start]:
+ if key in ['K1', 'L3', 'M5']:
+ major_edge = key
+ elif key in self.sidebar[6,0].options:
+ if minor_edge == '':
+ minor_edge = key
+ if int(key[-1]) % 2 > 0:
+ if int(minor_edge[-1]) % 2 == 0 or key[-1] > minor_edge[-1]:
+ minor_edge = key
+
+ all_edges[key] = {'onset': x_section[key]['onset']}
+
+ if major_edge != '':
+ key = major_edge
+ elif minor_edge != '':
+ key = minor_edge
+ else:
+ print(f'Could not find no edge of {zz} in spectrum')
+ return False
+ if index == -1:
+ index = self.sidebar[4, 0].value
+ # self.ui.dialog.setWindowTitle(f'{index}, {zz}')
+
+ if str(index) not in self.edges:
+ self.edges[str(index)] = {}
+
+ start_exclude = x_section[key]['onset'] - x_section[key]['excl before']
+ end_exclude = x_section[key]['onset'] + x_section[key]['excl after']
+
+ self.edges[str(index)] = {'z': zz, 'symmetry': key, 'element': eels.elements[zz],
+ 'onset': x_section[key]['onset'], 'end_exclude': end_exclude,
+ 'start_exclude': start_exclude}
+ self.edges[str(index)]['all_edges'] = all_edges
+ self.edges[str(index)]['chemical_shift'] = 0.0
+ self.edges[str(index)]['areal_density'] = 0.0
+ self.edges[str(index)]['original_onset'] = self.edges[str(index)]['onset']
+ return True
+
+ def sort_elements(self):
+ onsets = []
+ for index, edge in self.edges.items():
+ if index.isdigit():
+ onsets.append(float(edge['onset']))
+
+ arg_sorted = np.argsort(onsets)
+ edges = self.edges.copy()
+ for index, i_sorted in enumerate(arg_sorted):
+ self.edges[str(index)] = edges[str(i_sorted)].copy()
+
+ index = 0
+ edge = self.edges['0']
+ dispersion = self.energy_scale[1]-self.energy_scale[0]
+
+ while str(index + 1) in self.edges:
+ next_edge = self.edges[str(index + 1)]
+ if edge['end_exclude'] > next_edge['start_exclude'] - 5 * dispersion:
+ edge['end_exclude'] = next_edge['start_exclude'] - 5 * dispersion
+ edge = next_edge
+ index += 1
+
+ if edge['end_exclude'] > self.energy_scale[-3]:
+ edge['end_exclude'] = self.energy_scale[-3]
+
+ def set_elements(self, value=0):
+ selected_elements = self.periodic_table.get_output()
+ edges = self.edges.copy()
+ to_delete = []
+ old_elements = []
+ if len(selected_elements) > 0:
+ for key in self.edges:
+ if key.isdigit():
+ to_delete.append(key)
+ old_elements.append(self.edges[key]['element'])
+
+ for key in to_delete:
+ edges[key] = self.edges[key]
+ del self.edges[key]
+
+ for index, elem in enumerate(selected_elements):
+ if elem in old_elements:
+ self.edges[str(index)] = edges[str(old_elements.index(elem))]
+ else:
+ self.update_element(elem, index=index)
+ self.sort_elements()
+ self.update()
+ self.set_figure_pane()
+
+ def set_element(self, elem):
+ self.update_element(self.sidebar[5, 0].value)
+ # self.sort_elements()
+ self.update()
+
+ def cursor2energy_scale(self, value):
+ dispersion = (self.end_cursor.value - self.start_cursor.value) / (self.end_channel - self.start_channel)
+ self.datasets[self.key].energy_loss *= (self.sidebar[3, 0].value/dispersion)
+ self.sidebar[3, 0].value = dispersion
+ offset = self.start_cursor.value - self.start_channel * dispersion
+ self.datasets[self.key].energy_loss += (self.sidebar[2, 0].value-self.datasets[self.key].energy_loss[0])
+ self.sidebar[2, 0].value = offset
+ self.plot()
+
+ def set_fit_area(self, value):
+ if self.sidebar[1,0].value > self.sidebar[2,0].value:
+ self.sidebar[1,0].value = self.sidebar[2,0].value -1
+ if self.sidebar[1,0].value < self.energy_scale[0]:
+ self.sidebar[1,0].value = self.energy_scale[0]
+ if self.sidebar[2,0].value > self.energy_scale[-1]:
+ self.sidebar[2,0].value = self.energy_scale[-1]
+ self.edges['fit_area']['fit_start'] = self.sidebar[1,0].value
+ self.edges['fit_area']['fit_end'] = self.sidebar[2,0].value
+
+ self.plot()
+
+ def set_y_scale(self, value):
+ self.change_y_scale = 1/self.y_scale
+ self.y_scale = 1.0
+ if self.dataset.metadata['experiment']['flux_ppm'] > 0:
+ if self.sidebar[12, 0].value:
+ dispersion = self.energy_scale[1] - self.energy_scale[0]
+ self.y_scale = 1/self.dataset.metadata['experiment']['flux_ppm'] * dispersion
+
+ self.change_y_scale *= self.y_scale
+ self.update()
+ self.plot()
+
+ def auto_id(self, value=0):
+ found_edges = eels.auto_id_edges(self.dataset)
+ if len(found_edges) > 0:
+ self.periodic_table.elements_selected = found_edges
+ self.periodic_table.update()
+
+ def find_elements(self, value=0):
+
+ if '0' not in self.edges:
+ self.edges['0'] = {}
+ # found_edges = eels.auto_id_edges(self.dataset)
+ found_edges = {}
+
+ selected_elements = []
+ elements = self.edges.copy()
+
+ for key in self.edges:
+ if key.isdigit():
+ if 'element' in self.edges[key]:
+ selected_elements.append(self.edges[key]['element'])
+ self.periodic_table.elements_selected = selected_elements
+ self.periodic_table.update()
+ self.app_layout.center = self.periodic_table_panel # self.periodic_table.periodic_table
+
+ def set_figure_pane(self, value=0):
+
+ self.app_layout.center = self.view.panel
+
+ def update(self, index=0):
+
+ index = self.sidebar[4,0].value # which edge
+ if index < 0:
+ options = list(self.sidebar[4,0].options)
+ options.insert(-1, (f'Edge {len(self.sidebar[4,0].options)}', len(self.sidebar[4,0].options)-1))
+ self.sidebar[4,0].options= options
+ self.sidebar[4,0].value = len(self.sidebar[4,0].options)-2
+ if str(index) not in self.edges:
+ self.edges[str(index)] = {'z': 0, 'element': 'x', 'symmetry': 'K1', 'onset': 0, 'start_exclude': 0, 'end_exclude':0,
+ 'areal_density': 0, 'chemical_shift':0}
+ if 'z' not in self.edges[str(index)]:
+ self.edges[str(index)] = {'z': 0, 'element': 'x', 'symmetry': 'K1', 'onset': 0, 'start_exclude': 0, 'end_exclude':0,
+ 'areal_density': 0, 'chemical_shift':0}
+ edge = self.edges[str(index)]
+
+ self.sidebar[5,0].value = edge['z']
+ self.sidebar[5,2].value = edge['element']
+ self.sidebar[6,0].value = edge['symmetry']
+ self.sidebar[7,0].value = edge['onset']
+ self.sidebar[8,0].value = edge['start_exclude']
+ self.sidebar[9,0].value = edge['end_exclude']
+ if self.y_scale == 1.0:
+ self.sidebar[10, 0].value = edge['areal_density']
+ self.sidebar[10, 2].value = 'a.u.'
+ else:
+ dispersion = self.energy_scale[1]-self.energy_scale[0]
+ self.sidebar[10, 0].value = np.round(edge['areal_density']/self.dataset.metadata['experiment']['flux_ppm']*1e-6, 2)
+ self.sidebar[10, 2].value = 'atoms/nm²'
+
+
+ def do_fit(self, value=0):
+ if 'experiment' in self.dataset.metadata:
+ exp = self.dataset.metadata['experiment']
+ if 'convergence_angle' not in exp:
+ raise ValueError('need a convergence_angle in experiment of metadata dictionary ')
+ alpha = exp['convergence_angle']
+ beta = exp['collection_angle']
+ beam_kv = exp['acceleration_voltage']
+
+ else:
+ raise ValueError('need a experiment parameter in metadata dictionary')
+
+ eff_beta = eels.effective_collection_angle(self.energy_scale, alpha, beta, beam_kv)
+
+ self.low_loss = None
+ if self.sidebar[12, 1].value:
+ for key in self.datasets.keys():
+ if key != self.key:
+ if isinstance(self.datasets[key], sidpy.Dataset):
+ if self.datasets[key].data_type.name == 'SPECTRUM':
+ if self.datasets[key].energy_loss[0] < 0:
+ self.low_loss = self.datasets[key]/self.datasets[key].sum()
+
+ edges = eels.make_cross_sections(self.edges, np.array(self.energy_scale), beam_kv, eff_beta, self.low_loss)
+
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ spectrum = self.dataset.view.get_spectrum()
+ else:
+ spectrum = self.dataset
+ self.edges = eels.fit_edges2(spectrum, self.energy_scale, edges)
+ areal_density = []
+ elements = []
+ for key in edges:
+ if key.isdigit(): # only edges have numbers in that dictionary
+ elements.append(edges[key]['element'])
+ areal_density.append(edges[key]['areal_density'])
+ areal_density = np.array(areal_density)
+ out_string = '\nRelative composition: \n'
+ for i, element in enumerate(elements):
+ out_string += f'{element}: {areal_density[i] / areal_density.sum() * 100:.1f}% '
+
+ self.model = self.edges['model']['spectrum']
+ self.update()
+ self.plot()
+
+ def modify_onset(self, value=-1):
+ edge_index = self.sidebar[4, 0].value
+ edge = self.edges[str(edge_index)]
+ edge['onset'] = self.sidebar[7,0].value
+ if 'original_onset' not in edge:
+ edge['original_onset'] = edge['onset']
+ edge['chemical_shift'] = edge['onset'] - edge['original_onset']
+ self.update()
+
+
+ def modify_start_exclude(self, value=-1):
+ edge_index = self.sidebar[4, 0].value
+ edge = self.edges[str(edge_index)]
+ edge['start_exclude'] = self.sidebar[8,0].value
+ self.plot()
+
+ def modify_end_exclude(self, value=-1):
+ edge_index = self.sidebar[4, 0].value
+ edge = self.edges[str(edge_index)]
+ edge['end_exclude'] = self.sidebar[9,0].value
+ self.plot()
+
+ def modify_areal_density(self, value=-1):
+ edge_index = self.sidebar[4, 0].value
+ edge = self.edges[str(edge_index)]
+
+ edge['areal_density'] = self.sidebar[10, 0].value
+ if self.y_scale != 1.0:
+ dispersion = self.energy_scale[1]-self.energy_scale[0]
+ edge['areal_density'] = self.sidebar[10, 0].value *self.dataset.metadata['experiment']['flux_ppm']/1e-6
+
+ self.model = self.edges['model']['background']
+ for key in self.edges:
+ if key.isdigit():
+ if 'data' in self.edges[key]:
+
+ self.model = self.model + self.edges[key]['areal_density'] * self.edges[key]['data']
+ self.plot()
+
+ def set_action(self):
+ self.sidebar[1, 0].observe(self.set_fit_area, names='value')
+ self.sidebar[2, 0].observe(self.set_fit_area, names='value')
+
+ self.sidebar[3, 0].on_click(self.find_elements)
+ self.sidebar[4, 0].observe(self.update)
+ self.sidebar[5, 0].observe(self.set_element, names='value')
+
+ self.sidebar[7, 0].observe(self.modify_onset, names='value')
+ self.sidebar[8, 0].observe(self.modify_start_exclude, names='value')
+ self.sidebar[9, 0].observe(self.modify_end_exclude, names='value')
+ self.sidebar[10, 0].observe(self.modify_areal_density, names='value')
+
+ self.sidebar[11, 0].on_click(self.do_fit)
+ self.sidebar[12, 2].observe(self.plot)
+ self.sidebar[0, 0].observe(self.plot)
+
+ self.sidebar[12,0].observe(self.set_y_scale)
+
+ self.elements_cancel_button.on_click(self.set_figure_pane)
+ self.elements_auto_button.on_click(self.auto_id)
+ self.elements_select_button.on_click(self.set_elements)
+
+""" Interactive routines for EELS analysis
+
+this file provides additional dialogs for EELS quantification
+
+Author: Gerd Duscher
+"""
+
+import numpy as np
+Qt_available = True
+try:
+ from PyQt5 import QtCore, QtGui, QtWidgets
+
+except:
+ Qt_available = False
+ # print('Qt dialogs are not available')
+
+import sidpy
+import matplotlib
+import matplotlib.pyplot as plt
+
+import matplotlib.patches as patches
+from matplotlib.widgets import RectangleSelector, SpanSelector
+
+import h5py # TODO: needs to go
+
+from IPython.display import display
+import ipywidgets
+
+from pyTEMlib import eels_tools as eels
+from pyTEMlib import file_tools as ft
+
+major_edges = ['K1', 'L3', 'M5', 'N5']
+all_edges = ['K1', 'L1', 'L2', 'L3', 'M1', 'M2', 'M3', 'M4', 'M5', 'N1', 'N2', 'N3', 'N4', 'N5', 'N6', 'N7', 'O1', 'O2',
+ 'O3', 'O4', 'O5', 'O6', 'O7', 'P1', 'P2', 'P3']
+first_close_edges = ['K1', 'L3', 'M5', 'M3', 'N5', 'N3']
+
+if Qt_available:
+
+ class PeriodicTableDialog(QtWidgets.QDialog):
+ """ Modal dialog to get a selection of elements.
+
+ Elements that are not having a valid cross-sections are disabled.
+
+ Parameters
+ ----------
+ initial_elements: list of str
+ the elements that are already selected
+ energy_scale: list or numpy array
+ energy-scale of spectrum/spectra to determine likely edges
+
+ Returns
+ -------
+ list of strings: elements.
+
+ Example
+ -------
+ >> PT_dialog = periodic_table_dialog(None, ['Mn', 'O'])
+ >> if PT_dialog.exec_() == periodic_table_dialog.Accepted:
+ >> selected_elements = PT_dialog.get_output()
+ >> print(selected_elements)
+ """
+
+ signal_selected = QtCore.pyqtSignal(list)
+
+ def __init__(self, initial_elements=None, energy_scale=None, parent=None):
+ super(PeriodicTableDialog, self).__init__(None, QtCore.Qt.WindowStaysOnTopHint)
+
+ if initial_elements is None:
+ initial_elements = [' ']
+ self.initial_elements = initial_elements
+ if energy_scale is None:
+ energy_scale = [100., 150., 200.]
+ self.parent = parent
+ self._output = []
+ self.elements_selected = initial_elements
+ self.energy_scale = np.array(energy_scale)
+
+ self.setWindowTitle("Periodic Table")
+ likely_edges = get_likely_edges(self.energy_scale)
+ self.likely_edges = likely_edges
+
+ # GD:font = wx.Font(10, wx.MODERN, wx.NORMAL, wx.BOLD)
+ self.buttons1 = []
+ self.button = []
+ self.pt_info = get_periodic_table_info()
+ self.init_ui()
+
+ for button in self.button:
+ if button.text() in initial_elements:
+ button.toggle()
+ pass
+
+ def on_close(self):
+ self.get_output()
+ self.signal_selected[list].emit(self._output)
+ self.accept()
+
+ def get_output(self):
+ self._output = []
+ for btn in self.button:
+ if btn.isChecked():
+ self._output.append(btn.text())
+
+ def exec_(self):
+ super(PeriodicTableDialog, self).exec_()
+ return self._output
+
+ def init_ui(self):
+
+ v_sizer = QtWidgets.QVBoxLayout()
+ g_sizer = QtWidgets.QGridLayout()
+
+ main_group = QtWidgets.QWidget()
+
+ color1 = "background-color: lightblue;\n"
+ color1l = "background-color: dodgerblue;\n"
+ color2 = "background-color: coral;\n"
+
+ for symbol, parameter in self.pt_info.items():
+ self.button.append(QtWidgets.QPushButton(symbol))
+ if parameter['PT_row'] > 7:
+ self.button[-1].setStyleSheet(color2)
+ elif '*' in symbol:
+ self.button[-1].setStyleSheet(color2)
+ else:
+ if symbol in self.likely_edges:
+ self.button[-1].setStyleSheet(color1l)
+ else:
+ self.button[-1].setStyleSheet(color1)
+ if parameter['Z'] == 0:
+ self.button[-1].setEnabled(False)
+ self.button[-1].setFixedWidth(50)
+ self.button[-1].setCheckable(True)
+ g_sizer.addWidget(self.button[-1], parameter['PT_row'], parameter['PT_col'])
+ main_group.setLayout(g_sizer)
+
+ v_sizer.addWidget(main_group)
+ self.setLayout(v_sizer)
+
+ ok_button = QtWidgets.QPushButton('OK')
+ ok_button.clicked.connect(self.on_close)
+
+ v_sizer.addWidget(ok_button)
+ self.setLayout(v_sizer)
+
+
+ class EnergySelector(QtWidgets.QDialog):
+ """Dialog and cursor to set energy scale"""
+
+ signal_selected = QtCore.pyqtSignal(bool)
+
+ def __init__(self, dset=None):
+ super(EnergySelector, self).__init__(None, QtCore.Qt.WindowStaysOnTopHint)
+
+ if not isinstance(dset, sidpy.Dataset):
+ return
+ if dset is None:
+ return
+ if dset.view is None:
+ return
+ self.dataset = dset
+
+ if hasattr(dset.view, 'axis'):
+ self.axis = dset.view.axis
+ # self.setWindowTitle('p')
+ elif hasattr(dset.view, 'axes'):
+ self.axis = dset.view.axes[1]
+ else:
+ return
+
+ self.spec_dim = -1
+ for dim, axis in self.dataset._axes.items():
+ if axis.dimension_type == sidpy.DimensionType.SPECTRAL:
+ self.spec_dim = dim
+ if self.spec_dim < 0:
+ raise TypeError('We need at least one SPECTRAL dimension')
+
+ self.energy_scale = self.dataset._axes[self.spec_dim].values
+ self.dispersion = self.energy_scale[1] - self.energy_scale[0]
+ self.offset = self.energy_scale[0]
+ self.spectrum = np.zeros(2)
+
+ self.change = 0
+
+ self.x_min = self.energy_scale[int(len(self.energy_scale)/4)]
+ self.x_max = self.energy_scale[int(len(self.energy_scale) / 4*3)]
+ self.setWindowTitle("Select Energy")
+
+ valid_float = QtGui.QDoubleValidator()
+
+ layout = QtWidgets.QGridLayout()
+ layout.setVerticalSpacing(2)
+ self.label1 = QtWidgets.QLabel('Start:')
+ self.edit1 = QtWidgets.QLineEdit('0')
+ self.edit1.setValidator(valid_float)
+ self.unit1 = QtWidgets.QLabel('eV')
+
+ self.label2 = QtWidgets.QLabel('End:')
+ self.edit2 = QtWidgets.QLineEdit('0')
+ self.edit2.setValidator(valid_float)
+ self.unit2 = QtWidgets.QLabel('eV')
+
+ self.label3 = QtWidgets.QLabel('Dispersion:')
+ self.edit3 = QtWidgets.QLineEdit('0')
+ self.edit3.setValidator(valid_float)
+ self.unit3 = QtWidgets.QLabel('eV')
+
+ self.edit1.editingFinished.connect(self.on_enter)
+ self.edit2.editingFinished.connect(self.on_enter)
+ self.edit3.editingFinished.connect(self.on_enter)
+
+ layout.addWidget(self.label1, 0, 0)
+ layout.addWidget(self.edit1, 0, 1)
+ layout.addWidget(self.unit1, 0, 2)
+
+ layout.addWidget(self.label2, 1, 0)
+ layout.addWidget(self.edit2, 1, 1)
+ layout.addWidget(self.unit2, 1, 2)
+
+ layout.addWidget(self.label3, 2, 0)
+ layout.addWidget(self.edit3, 2, 1)
+ layout.addWidget(self.unit3, 2, 2)
+
+ self.ok_button = QtWidgets.QPushButton('OK')
+ self.ok_button.clicked.connect(self.on_close)
+ self.cancel_button = QtWidgets.QPushButton('Cancel')
+ self.cancel_button.clicked.connect(self.on_close)
+
+ layout.addWidget(self.ok_button, 3, 0)
+ layout.addWidget(self.cancel_button, 3, 2)
+
+ self.setLayout(layout)
+ self.edit1.setFocus()
+ self.plot()
+
+ self.selector = SpanSelector(self.axis, self.line_select_callback,
+ direction="horizontal",
+ interactive=True,
+ props=dict(facecolor='blue', alpha=0.2))
+ self.edit1.setText(f'{self.x_min:.3f}')
+ self.edit2.setText(f'{self.x_max:.3f}')
+ self.edit3.setText(f'{self.dispersion:.4f}')
+ self.update()
+
+ def line_select_callback(self, eclick, erelease):
+ y_min, y_max = self.axis.get_ylim()
+ self.x_min = self.selector.extents[0]
+ self.x_max = self.selector.extents[1]
+ # self.selector.extents = (self.x_min, self.x_max, y_min, y_max)
+
+ self.edit1.setText(f'{self.x_min:.3f}')
+ self.edit2.setText(f'{self.x_max:.3f}')
+
+ def on_enter(self):
+ sender = self.sender()
+
+ if sender == self.edit1:
+ value = float(str(sender.displayText()).strip())
+ if value == self.x_min:
+ return
+ self.change = value - self.x_min
+ self.x_min += self.change
+ self.x_max += self.change
+ self.offset += self.change
+
+ self.edit1.setText(f"{self.x_min:.2f}")
+ self.edit2.setText(f"{self.x_max:.2f}")
+
+ self.energy_scale = np.arange(len(self.energy_scale)) * self.dispersion + self.offset
+
+ self.update()
+ # self.axis.draw()
+ # self.setWindowTitle(f'shift, {self.change}, {self.x_min}')
+
+ elif sender == self.edit2:
+ value = float(str(sender.displayText()).strip())
+ if value == self.x_max:
+ return
+ start_channel = np.searchsorted(self.energy_scale, self.x_min)
+ end_channel = np.searchsorted(self.energy_scale, self.x_max)
+
+ self.x_max = value
+
+ if end_channel - start_channel != 0:
+ self.dispersion = (self.x_max - self.x_min) / (end_channel - start_channel)
+ self.offset = self.x_min - start_channel * self.dispersion
+ self.edit2.setText(f"{self.x_max:.3f}")
+ self.edit3.setText(f"{self.dispersion:.4f}")
+ self.energy_scale = np.arange(len(self.energy_scale)) * self.dispersion + self.offset
+
+ self.update()
+ # self.axis.draw()
+ # self.setWindowTitle(f'range, {self.change}, {self.dispersion}')
+
+ elif sender == self.edit3:
+ value = float(str(sender.displayText()).strip())
+ if self.dispersion == value:
+ return
+
+ start_channel = np.searchsorted(self.energy_scale, self.x_min)
+ end_channel = np.searchsorted(self.energy_scale, self.x_max)
+ self.dispersion = value
+ self.energy_scale = np.arange(len(self.energy_scale)) * self.dispersion + self.offset
+ self.x_min = self.energy_scale[start_channel]
+ self.x_max = self.energy_scale[end_channel]
+ self.update()
+ # self.axis.draw()
+ self.edit3.setText(f"{self.dispersion:.3f}")
+ self.change = 0
+
+ def on_close(self):
+ sender = self.sender()
+ if sender == self.ok_button:
+ pass
+ self.dataset.set_dimension(self.spec_dim, sidpy.Dimension(self.energy_scale, name='energy_scale',
+ units='eV', quantity='energy loss',
+ dimension_type='spectral'))
+ else:
+ pass
+ self.selector.set_visible(False)
+ self.signal_selected[bool].emit(True)
+ self.accept()
+
+ def plot(self):
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ self.spectrum = self.dataset.view.get_spectrum()
+ else:
+ self.spectrum = np.array(self.dataset)
+ x_limit = self.axis.get_xlim()
+ y_limit = self.axis.get_ylim()
+
+ self.axis.clear()
+ self.cplot = self.axis.plot(self.energy_scale, self.spectrum, label='spectrum')
+ self.axis.set_xlim(x_limit)
+ self.axis.set_ylim(y_limit)
+
+ self.axis.figure.canvas.draw()
+
+ def update(self):
+ x_limit = self.axis.get_xlim()
+ y_limit = self.axis.get_ylim()
+ self.selector.extents = (self.x_min, self.x_max)
+
+ x_limit = np.array(x_limit) + self.change
+
+ self.cplot[0].set_data(self.energy_scale, self.spectrum)
+ self.axis.set_xlim(x_limit)
+ self.axis.set_ylim(y_limit)
+ self.axis.figure.canvas.draw()
+
+
+
+
+
+[docs]class RegionSelector(object):
+ """Selects fitting region and the regions that are excluded for each edge.
+
+ Select a region with a spanSelector and then type 'a' for all the fitting region or a number for the edge
+ you want to define the region excluded from the fit (solid state effects).
+
+ see Chapter4 'CH4-Working_with_X-Sections,ipynb' notebook
+
+ """
+
+ def __init__(self, ax):
+ self.ax = ax
+ self.regions = {}
+ self.rect = None
+ self.xmin = 0
+ self.width = 0
+
+ self.span = SpanSelector(ax, self.on_select1,
+ direction="horizontal",
+ interactive=True,
+ props=dict(facecolor='blue', alpha=0.2))
+ self.cid = ax.figure.canvas.mpl_connect('key_press_event', self.click)
+ self.draw = ax.figure.canvas.mpl_connect('draw_event', self.onresize)
+
+ def on_select1(self, xmin, xmax):
+ self.xmin = xmin
+ self.width = xmax - xmin
+
+ def onresize(self, event):
+ self.update()
+
+ def delete_region(self, key):
+ if key in self.regions:
+ if 'Rect' in self.regions[key]:
+ self.regions[key]['Rect'].remove()
+ self.regions[key]['Text'].remove()
+ del (self.regions[key])
+
+ def update(self):
+
+ y_min, y_max = self.ax.get_ylim()
+ for key in self.regions:
+ if 'Rect' in self.regions[key]:
+ self.regions[key]['Rect'].remove()
+ self.regions[key]['Text'].remove()
+
+ xmin = self.regions[key]['xmin']
+ width = self.regions[key]['width']
+ height = y_max - y_min
+ alpha = self.regions[key]['alpha']
+ color = self.regions[key]['color']
+ self.regions[key]['Rect'] = patches.Rectangle((xmin, y_min), width, height,
+ edgecolor=color, alpha=alpha, facecolor=color)
+ self.ax.add_patch(self.regions[key]['Rect'])
+
+ self.regions[key]['Text'] = self.ax.text(xmin, y_max, self.regions[key]['text'], verticalalignment='top')
+
+ def click(self, event):
+ if str(event.key) in ['1', '2', '3', '4', '5', '6']:
+ key = str(event.key)
+ text = 'exclude \nedge ' + key
+ alpha = 0.5
+ color = 'red'
+ elif str(event.key) in ['a', 'A', 'B', 'b', 'f', 'F']:
+ key = '0'
+ color = 'blue'
+ alpha = 0.2
+ text = 'fit region'
+ else:
+ return
+
+ if key not in self.regions:
+ self.regions[key] = {}
+
+ self.regions[key]['xmin'] = self.xmin
+ self.regions[key]['width'] = self.width
+ self.regions[key]['color'] = color
+ self.regions[key]['alpha'] = alpha
+ self.regions[key]['text'] = text
+
+ self.update()
+
+ def set_regions(self, region, start_x, width):
+ key = ''
+ if 'fit' in str(region):
+ key = '0'
+ if region in ['0', '1', '2', '3', '4', '5', '6']:
+ key = region
+ if region in [0, 1, 2, 3, 4, 5, 6]:
+ key = str(region)
+
+ if key not in self.regions:
+ self.regions[key] = {}
+ if key in ['1', '2', '3', '4', '5', '6']:
+ self.regions[key]['text'] = 'exclude \nedge ' + key
+ self.regions[key]['alpha'] = 0.5
+ self.regions[key]['color'] = 'red'
+ elif key == '0':
+ self.regions[key]['text'] = 'fit region'
+ self.regions[key]['alpha'] = 0.2
+ self.regions[key]['color'] = 'blue'
+
+ self.regions[key]['xmin'] = start_x
+ self.regions[key]['width'] = width
+
+ self.update()
+
+ def get_regions(self):
+ tags = {}
+ for key in self.regions:
+ if key == '0':
+ area = 'fit_area'
+ else:
+ area = key
+ tags[area] = {}
+ tags[area]['start_x'] = self.regions[key]['xmin']
+ tags[area]['width_x'] = self.regions[key]['width']
+
+ return tags
+
+ def disconnect(self):
+ for key in self.regions:
+ if 'Rect' in self.regions[key]:
+ self.regions[key]['Rect'].remove()
+ self.regions[key]['Text'].remove()
+ del self.span
+ self.ax.figure.canvas.mpl_disconnect(self.cid)
+ # self.ax.figure.canvas.mpl_disconnect(self.draw)
+ pass
+
+
+[docs]class RangeSelector(RectangleSelector):
+ """Select ranges of edge fitting interactively"""
+ def __init__(self, ax, on_select):
+ drawtype = 'box'
+ spancoords = 'data'
+ rectprops = dict(facecolor="blue", edgecolor="black", alpha=0.2, fill=True)
+
+ super().__init__(ax, on_select, drawtype=drawtype,
+ minspanx=0, minspany=0, useblit=False,
+ lineprops=None, rectprops=rectprops, spancoords=spancoords,
+ button=None, maxdist=10, marker_props=None,
+ interactive=True, state_modifier_keys=None)
+
+ self.artists = [self.to_draw, self._center_handle.artist,
+ self._edge_handles.artist]
+
+ def draw_shape(self, extents):
+ x0, x1, y0, y1 = extents
+ xmin, xmax = sorted([x0, x1])
+ # ymin, ymax = sorted([y0, y1])
+ xlim = sorted(self.ax.get_xlim())
+ ylim = sorted(self.ax.get_ylim())
+
+ xmin = max(xlim[0], xmin)
+ ymin = ylim[0]
+ xmax = min(xmax, xlim[1])
+ ymax = ylim[1]
+
+ self.to_draw.set_x(xmin)
+ self.to_draw.set_y(ymin)
+ self.to_draw.set_width(xmax - xmin)
+ self.to_draw.set_height(ymax - ymin)
+
+
+[docs]def get_likely_edges(energy_scale):
+ """get likely ionization edges within energy_scale"""
+ x_sections = eels.get_x_sections()
+ # print(energy_scale)
+ energy_origin = energy_scale[0]
+ energy_window = energy_scale[-1] - energy_origin
+ selected_edges_unsorted = {}
+ likely_edges = []
+ selected_elements = []
+ for element in range(1, 83):
+ # print(element)
+ element_z = str(eels.get_z(element))
+
+ for key in x_sections[element_z]:
+ if key in all_edges:
+ onset = x_sections[element_z][key]['onset']
+ if onset > energy_origin:
+ if onset - energy_origin < energy_window:
+ if element not in selected_edges_unsorted:
+ selected_edges_unsorted[element] = {}
+ # print(element, x_sections[element]['name'], key, x_sections[element][key]['onset'])
+ # text = f"\n {x_sections[element_z]['name']:2s}-{key}: " \
+ # f"{x_sections[element_z][key]['onset']:8.1f} eV "
+ # print(text)
+
+ selected_edges_unsorted[element][key] = {}
+ selected_edges_unsorted[element][key]['onset'] = x_sections[element_z][key]['onset']
+
+ if key in major_edges:
+ selected_edges_unsorted[element][key]['intensity'] = 'major'
+ selected_elements.append(x_sections[element_z]['name'])
+ else:
+ selected_edges_unsorted[element][key]['intensity'] = 'minor'
+
+ if element in selected_edges_unsorted:
+ for key in selected_edges_unsorted[element]:
+ if selected_edges_unsorted[element][key]['intensity'] == 'major':
+ likely_edges.append(x_sections[str(element)]['name']) # = {'z':element, 'symmetry': key}
+
+ return likely_edges
+
+
+[docs]class SpectrumPlot(sidpy.viz.dataset_viz.CurveVisualizer):
+ def __init__(self, dset, spectrum_number=0, figure=None, **kwargs):
+ with plt.ioff():
+ self.figure = plt.figure()
+ self.figure.canvas.toolbar_position = 'right'
+ self.figure.canvas.toolbar_visible = True
+
+ super().__init__(dset, spectrum_number=spectrum_number, figure=self.figure, **kwargs)
+
+ self.start_cursor = ipywidgets.FloatText(value=0, description='Start:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ self.end_cursor = ipywidgets.FloatText(value=0, description='End:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ self.panel = ipywidgets.VBox([ipywidgets.HBox([ipywidgets.Label('',layout=ipywidgets.Layout(width='100px')), ipywidgets.Label('Cursor:'),
+ self.start_cursor,ipywidgets.Label('eV'),
+ self.end_cursor, ipywidgets.Label('eV')]),
+ self.figure.canvas])
+
+ self.selector = matplotlib.widgets.SpanSelector(self.axis, self.line_select_callback,
+ direction="horizontal",
+ interactive=True,
+ props=dict(facecolor='blue', alpha=0.2))
+
+ def line_select_callback(self, x_min, x_max):
+ self.start_cursor.value = np.round(x_min, 3)
+ self.end_cursor.value = np.round(x_max, 3)
+ self.start_channel = np.searchsorted(self.dset.energy_loss, self.start_cursor.value)
+ self.end_channel = np.searchsorted(self.dset.energy_loss, self.end_cursor.value)
+
+ def plot(self, scale=True, additional_spectra=None):
+
+ self.energy_scale = self.dset.energy_loss.values
+ x_limit = self.axis.get_xlim()
+ y_limit = np.array(self.axis.get_ylim())
+
+ self.axis.clear()
+
+ self.axis.plot(self.energy_scale, self.dset*self.y_scale, label='spectrum')
+
+ if additional_spectra is not None:
+ if isinstance(additional_spectra, dict):
+ for key, spectrum in additional_spectra.items():
+ self.axis.plot(self.energy_scale, spectrum*self.y_scale, label=key)
+
+ self.axis.set_xlabel(self.dset.labels[0])
+ self.axis.set_ylabel(self.dset.data_descriptor)
+ self.axis.ticklabel_format(style='sci', scilimits=(-2, 3))
+ if scale:
+ self.axis.set_ylim(np.array(y_limit)*self.change_y_scale)
+
+ self.change_y_scale = 1.0
+ if self.y_scale != 1.:
+ self.axis.set_ylabel('scattering probability (ppm/eV)')
+ self.selector = matplotlib.widgets.SpanSelector(self.axis, self.line_select_callback,
+ direction="horizontal",
+ interactive=True,
+ props=dict(facecolor='blue', alpha=0.2))
+ self.axis.legend()
+ self.figure.canvas.draw_idle()
+
+
+[docs]class SIPlot(sidpy.viz.dataset_viz.SpectralImageVisualizer):
+ def __init__(self, dset, figure=None, horizontal=True, **kwargs):
+ if figure is None:
+ with plt.ioff():
+ self.figure = plt.figure()
+ else:
+ self.figure = figure
+ self.figure.canvas.toolbar_position = 'right'
+ self.figure.canvas.toolbar_visible = True
+
+ super().__init__(dset, figure= self.figure, horizontal=horizontal, **kwargs)
+
+ self.start_cursor = ipywidgets.FloatText(value=0, description='Start:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ self.end_cursor = ipywidgets.FloatText(value=0, description='End:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ self.panel = ipywidgets.VBox([ipywidgets.HBox([ipywidgets.Label('',layout=ipywidgets.Layout(width='100px')), ipywidgets.Label('Cursor:'),
+ self.start_cursor,ipywidgets.Label('eV'),
+ self.end_cursor, ipywidgets.Label('eV')]),
+ self.figure.canvas])
+ self.axis = self.axes[-1]
+ self.selector = matplotlib.widgets.SpanSelector(self.axis, self.line_select_callback,
+ direction="horizontal",
+ interactive=True,
+ props=dict(facecolor='blue', alpha=0.2))
+
+ def line_select_callback(self, x_min, x_max):
+ self.start_cursor.value = np.round(x_min, 3)
+ self.end_cursor.value = np.round(x_max, 3)
+ self.start_channel = np.searchsorted(self.dset.energy_loss, self.start_cursor.value)
+ self.end_channel = np.searchsorted(self.dset.energy_loss, self.end_cursor.value)
+
+ def plot(self, scale=True, additional_spectra=None):
+
+ xlim = self.axes[1].get_xlim()
+ ylim = self.axes[1].get_ylim()
+ self.axes[1].clear()
+ self.get_spectrum()
+ if len(self.energy_scale)!=self.spectrum.shape[0]:
+ self.spectrum = self.spectrum.T
+ self.axes[1].plot(self.energy_scale, self.spectrum.compute(), label='experiment')
+ if additional_spectra is not None:
+ if isinstance(additional_spectra, dict):
+ for key, spectrum in additional_spectra.items():
+ self.axes[1].plot(self.energy_scale, spectrum, label=key)
+
+ if self.set_title:
+ self.axes[1].set_title('spectrum {}, {}'.format(self.x, self.y))
+ self.fig.tight_layout()
+ self.selector = matplotlib.widgets.SpanSelector(self.axes[1], self.line_select_callback,
+ direction="horizontal",
+ interactive=True,
+ props=dict(facecolor='blue', alpha=0.2))
+
+ self.axes[1].set_xlim(xlim)
+ self.axes[1].set_ylim(ylim)
+ self.axes[1].set_xlabel(self.xlabel)
+ self.axes[1].set_ylabel(self.ylabel)
+
+ self.fig.canvas.draw_idle()
+
+
+
+
+[docs]def get_periodic_table_widget(energy_scale=None):
+
+ if energy_scale is None:
+ energy_scale = [100., 150., 200.]
+
+ likely_edges = get_likely_edges(energy_scale)
+
+ pt_info = get_periodic_table_info()
+ table = ipywidgets.GridspecLayout(10, 18,width= '60%', grid_gap="0px")
+ for symbol, parameter in pt_info.items():
+ #print(parameter['PT_row'], parameter['PT_col'])
+ if parameter['PT_row'] > 7:
+ color = 'warning'
+ elif '*' in symbol:
+ color = 'warning'
+ else:
+ if symbol in likely_edges:
+ color = 'primary'
+ else:
+ color = 'info'
+ table[parameter['PT_row'], parameter['PT_col']] = ipywidgets.ToggleButton(description=symbol,
+ value=False,
+ button_style=color,
+ layout=ipywidgets.Layout(width='auto'),
+ style={"button_width": "30px"})
+ return table
+
+
+[docs]class PeriodicTableWidget(object):
+ """ ipywidget to get a selection of elements.
+
+ Elements that are not having a valid cross-sections are disabled.
+
+ Parameters
+ ----------
+ initial_elements: list of str
+ the elements that are already selected
+ energy_scale: list or numpy array
+ energy-scale of spectrum/spectra to determine likely edges
+
+ Returns
+ -------
+ list of strings: elements.
+ use get_output() function
+ """
+
+ def __init__(self, initial_elements=None, energy_scale=None):
+
+ if initial_elements is None:
+ initial_elements = [' ']
+ self.elements_selected = initial_elements
+ if energy_scale is None:
+ energy_scale = [100., 150., 200.]
+ self._output = []
+ self.energy_scale = np.array(energy_scale)
+ self.pt_info = get_periodic_table_info()
+
+ self.periodic_table = get_periodic_table_widget(energy_scale)
+ self.update()
+
+ def get_output(self):
+ self.elements_selected = []
+ for symbol, parameter in self.pt_info.items():
+ if self.periodic_table[parameter['PT_row'], parameter['PT_col']].value == True: # [parameter['PT_row'], parameter['PT_col']]
+ self.elements_selected.append(self.periodic_table[parameter['PT_row'], parameter['PT_col']].description)
+ return self.elements_selected
+
+ def update(self):
+ for symbol, parameter in self.pt_info.items():
+ if str(self.periodic_table[parameter['PT_row'], parameter['PT_col']].description) in list(self.elements_selected):
+ self.periodic_table[parameter['PT_row'], parameter['PT_col']].value = True
+
+
+
+
+[docs]def get_periodic_table_info():
+ """Info for periodic table dialog"""
+ pt_info = \
+ {'H': {'PT_row': 0, 'PT_col': 0, 'Z': 0},
+ 'He': {'PT_row': 0, 'PT_col': 17, 'Z': 2}, 'Li': {'PT_row': 1, 'PT_col': 0, 'Z': 3},
+ 'Be': {'PT_row': 1, 'PT_col': 1, 'Z': 4}, 'B': {'PT_row': 1, 'PT_col': 12, 'Z': 5},
+ 'C': {'PT_row': 1, 'PT_col': 13, 'Z': 6}, 'N': {'PT_row': 1, 'PT_col': 14, 'Z': 7},
+ 'O': {'PT_row': 1, 'PT_col': 15, 'Z': 8}, 'F': {'PT_row': 1, 'PT_col': 16, 'Z': 9},
+ 'Ne': {'PT_row': 1, 'PT_col': 17, 'Z': 10}, 'Na': {'PT_row': 2, 'PT_col': 0, 'Z': 11},
+ 'Mg': {'PT_row': 2, 'PT_col': 1, 'Z': 12}, 'Al': {'PT_row': 2, 'PT_col': 12, 'Z': 13},
+ 'Si': {'PT_row': 2, 'PT_col': 13, 'Z': 14}, 'P': {'PT_row': 2, 'PT_col': 14, 'Z': 15},
+ 'S': {'PT_row': 2, 'PT_col': 15, 'Z': 16}, 'Cl': {'PT_row': 2, 'PT_col': 16, 'Z': 17},
+ 'Ar': {'PT_row': 2, 'PT_col': 17, 'Z': 18}, 'K': {'PT_row': 3, 'PT_col': 0, 'Z': 19},
+ 'Ca': {'PT_row': 3, 'PT_col': 1, 'Z': 20}, 'Sc': {'PT_row': 3, 'PT_col': 2, 'Z': 21},
+ 'Ti': {'PT_row': 3, 'PT_col': 3, 'Z': 22}, 'V ': {'PT_row': 3, 'PT_col': 4, 'Z': 23},
+ 'Cr': {'PT_row': 3, 'PT_col': 5, 'Z': 24}, 'Mn': {'PT_row': 3, 'PT_col': 6, 'Z': 25},
+ 'Fe': {'PT_row': 3, 'PT_col': 7, 'Z': 26}, 'Co': {'PT_row': 3, 'PT_col': 8, 'Z': 27},
+ 'Ni': {'PT_row': 3, 'PT_col': 9, 'Z': 28}, 'Cu': {'PT_row': 3, 'PT_col': 10, 'Z': 29},
+ 'Zn': {'PT_row': 3, 'PT_col': 11, 'Z': 30}, 'Ga': {'PT_row': 3, 'PT_col': 12, 'Z': 31},
+ 'Ge': {'PT_row': 3, 'PT_col': 13, 'Z': 32}, 'As': {'PT_row': 3, 'PT_col': 14, 'Z': 33},
+ 'Se': {'PT_row': 3, 'PT_col': 15, 'Z': 34}, 'Br': {'PT_row': 3, 'PT_col': 16, 'Z': 35},
+ 'Kr': {'PT_row': 3, 'PT_col': 17, 'Z': 36}, 'Rb': {'PT_row': 4, 'PT_col': 0, 'Z': 37},
+ 'Sr': {'PT_row': 4, 'PT_col': 1, 'Z': 38}, 'Y': {'PT_row': 4, 'PT_col': 2, 'Z': 39},
+ 'Zr': {'PT_row': 4, 'PT_col': 3, 'Z': 40}, 'Nb': {'PT_row': 4, 'PT_col': 4, 'Z': 41},
+ 'Mo': {'PT_row': 4, 'PT_col': 5, 'Z': 42}, 'Tc': {'PT_row': 4, 'PT_col': 6, 'Z': 43},
+ 'Ru': {'PT_row': 4, 'PT_col': 7, 'Z': 44}, 'Rh': {'PT_row': 4, 'PT_col': 8, 'Z': 45},
+ 'Pd': {'PT_row': 4, 'PT_col': 9, 'Z': 46}, 'Ag': {'PT_row': 4, 'PT_col': 10, 'Z': 47},
+ 'Cd': {'PT_row': 4, 'PT_col': 11, 'Z': 48}, 'In': {'PT_row': 4, 'PT_col': 12, 'Z': 49},
+ 'Sn': {'PT_row': 4, 'PT_col': 13, 'Z': 50}, 'Sb': {'PT_row': 4, 'PT_col': 14, 'Z': 51},
+ 'Te': {'PT_row': 4, 'PT_col': 15, 'Z': 52}, 'I': {'PT_row': 4, 'PT_col': 16, 'Z': 53},
+ 'Xe': {'PT_row': 4, 'PT_col': 17, 'Z': 54}, 'Cs': {'PT_row': 5, 'PT_col': 0, 'Z': 55},
+ 'Ba': {'PT_row': 5, 'PT_col': 1, 'Z': 56}, 'Hf': {'PT_row': 5, 'PT_col': 3, 'Z': 72},
+ 'Ta': {'PT_row': 5, 'PT_col': 4, 'Z': 73}, 'W': {'PT_row': 5, 'PT_col': 5, 'Z': 74},
+ 'Re': {'PT_row': 5, 'PT_col': 6, 'Z': 75}, 'Os': {'PT_row': 5, 'PT_col': 7, 'Z': 76},
+ 'Ir': {'PT_row': 5, 'PT_col': 8, 'Z': 77}, 'Pt': {'PT_row': 5, 'PT_col': 9, 'Z': 78},
+ 'Au': {'PT_row': 5, 'PT_col': 10, 'Z': 79}, 'Hg': {'PT_row': 5, 'PT_col': 11, 'Z': 80},
+ 'Pb': {'PT_row': 5, 'PT_col': 13, 'Z': 82}, 'Bi': {'PT_row': 5, 'PT_col': 14, 'Z': 0},
+ 'Po': {'PT_row': 5, 'PT_col': 15, 'Z': 0}, 'At': {'PT_row': 5, 'PT_col': 16, 'Z': 0},
+ 'Rn': {'PT_row': 5, 'PT_col': 17, 'Z': 0}, 'Fr': {'PT_row': 6, 'PT_col': 0, 'Z': 0},
+ 'Ra': {'PT_row': 6, 'PT_col': 1, 'Z': 0}, 'Rf': {'PT_row': 6, 'PT_col': 3, 'Z': 0},
+ 'Db': {'PT_row': 6, 'PT_col': 4, 'Z': 0}, 'Sg': {'PT_row': 6, 'PT_col': 5, 'Z': 0},
+ 'Bh': {'PT_row': 6, 'PT_col': 6, 'Z': 0}, 'Hs': {'PT_row': 6, 'PT_col': 7, 'Z': 0},
+ 'Mt': {'PT_row': 6, 'PT_col': 8, 'Z': 0}, 'Ds': {'PT_row': 6, 'PT_col': 9, 'Z': 0},
+ 'Rg': {'PT_row': 6, 'PT_col': 10, 'Z': 0}, 'La': {'PT_row': 8, 'PT_col': 3, 'Z': 57},
+ 'Ce': {'PT_row': 8, 'PT_col': 4, 'Z': 58}, 'Pr': {'PT_row': 8, 'PT_col': 5, 'Z': 59},
+ 'Nd': {'PT_row': 8, 'PT_col': 6, 'Z': 60}, 'Pm': {'PT_row': 8, 'PT_col': 7, 'Z': 61},
+ 'Sm': {'PT_row': 8, 'PT_col': 8, 'Z': 62}, 'Eu': {'PT_row': 8, 'PT_col': 9, 'Z': 63},
+ 'Gd': {'PT_row': 8, 'PT_col': 10, 'Z': 64}, 'Tb': {'PT_row': 8, 'PT_col': 11, 'Z': 65},
+ 'Dy': {'PT_row': 8, 'PT_col': 12, 'Z': 66}, 'Ho': {'PT_row': 8, 'PT_col': 13, 'Z': 67},
+ 'Er': {'PT_row': 8, 'PT_col': 14, 'Z': 68}, 'Tm': {'PT_row': 8, 'PT_col': 15, 'Z': 69},
+ 'Yb': {'PT_row': 8, 'PT_col': 16, 'Z': 70}, 'Lu': {'PT_row': 8, 'PT_col': 17, 'Z': 71},
+ 'Ac': {'PT_row': 9, 'PT_col': 3, 'Z': 0}, 'Th': {'PT_row': 9, 'PT_col': 4, 'Z': 0},
+ 'Pa': {'PT_row': 9, 'PT_col': 5, 'Z': 0}, 'U': {'PT_row': 9, 'PT_col': 6, 'Z': 0},
+ 'Np': {'PT_row': 9, 'PT_col': 7, 'Z': 0}, 'Pu': {'PT_row': 9, 'PT_col': 8, 'Z': 0},
+ 'Am': {'PT_row': 9, 'PT_col': 9, 'Z': 0}, 'Cm': {'PT_row': 9, 'PT_col': 10, 'Z': 0},
+ 'Bk': {'PT_row': 9, 'PT_col': 11, 'Z': 0}, 'Cf': {'PT_row': 9, 'PT_col': 12, 'Z': 0},
+ 'Es': {'PT_row': 9, 'PT_col': 13, 'Z': 0}, 'Fm': {'PT_row': 9, 'PT_col': 14, 'Z': 0},
+ 'Md': {'PT_row': 9, 'PT_col': 15, 'Z': 0}, 'No': {'PT_row': 9, 'PT_col': 16, 'Z': 0},
+ 'Lr': {'PT_row': 9, 'PT_col': 17, 'Z': 0},
+ '*': {'PT_row': 5, 'PT_col': 2, 'PT_col2': 8, 'PT_row2': 2, 'Z': 0},
+ '**': {'PT_row': 6, 'PT_col': 2, 'PT_col2': 9, 'PT_row2': 2, 'Z': 0}}
+
+ return pt_info
+
+
+[docs]class InteractiveSpectrumImage(object):
+ """Interactive spectrum imaging plot
+
+ Attributes:
+ -----------
+ dictionary with a minimum of the following keys:
+ ['image']: displayed image
+ ['data']: data cube
+ ['intensity_scale_ppm']: intensity scale
+ ['ylabel']: intensity label
+ ['spectra'] dictionary which contains dictionaries for each spectrum style ['1-2']:
+ ['spectrum'] = tags['cube'][y,x,:]
+ ['spectra'][f'{x}-{y}']['energy_scale'] = tags['energy_scale']
+ ['intensity_scale'] = 1/tags['cube'][y,x,:].sum()*1e6
+
+ Please note the possibility to load any image for the selection of the spectrum
+ Also there is the possibility to display the survey image.
+
+ For analysis, we have the following options:
+ 'fix_energy': set zero-loss peak maximum to zero !! Low loss spectra only!!
+ 'fit_zero_loss': fit zero-loss peak with model function !! Low loss spectra only!!
+ 'fit_low_loss': fit low-loss spectrum with model peaks !! Low loss spectra only!!
+
+
+ 'fit_composition': fit core-loss spectrum with background and cross-sections!! Core loss spectra only!!
+ 'fit_ELNES': fit core-loss edge with model peaks !! Core loss spectra only!!
+ """
+
+ def __init__(self, data_source, horizontal=True):
+
+ box_layout = ipywidgets.Layout(display='flex',
+ flex_flow='row',
+ align_items='stretch',
+ width='100%')
+
+ words = ['fix_energy', 'fit_zero_loss', 'fit_low_loss', 'fit_composition', 'fit_ELNES']
+
+ self.buttons = [ipywidgets.ToggleButton(value=False, description=word, disabled=False) for word in words]
+ box = ipywidgets.Box(children=self.buttons, layout=box_layout)
+ display(box)
+
+ # MAKE Dictionary
+
+ if isinstance(data_source, dict):
+ self.tags = data_source
+ elif isinstance(data_source, h5py.Group):
+ self.tags = self.set_tags(data_source)
+ else:
+ print('Data source must be a dictionary or channel')
+ return
+
+ # Button(description='edge_quantification')
+ for button in self.buttons:
+ button.observe(self.on_button_clicked, 'value') # on_click(self.on_button_clicked)
+
+ self.figure = plt.figure()
+ self.horizontal = horizontal
+ self.x = 0
+ self.y = 0
+
+ self.extent = [0, self.tags['cube'].shape[1], self.tags['cube'].shape[0], 0]
+ self.rectangle = [0, self.tags['cube'].shape[1], 0, self.tags['cube'].shape[0]]
+ self.scaleX = 1.0
+ self.scaleY = 1.0
+ self.analysis = []
+ self.plot_legend = False
+ if 'ylabel' not in self.tags:
+ self.tags['ylabel'] = 'intensity [a.u.]'
+ self.SI = False
+
+ if horizontal:
+ self.ax1 = plt.subplot(1, 2, 1)
+ self.ax2 = plt.subplot(1, 2, 2)
+ else:
+ self.ax1 = plt.subplot(2, 1, 1)
+ self.ax2 = plt.subplot(2, 1, 2)
+
+ self.cube = self.tags['cube']
+ self.image = self.tags['cube'].sum(axis=2)
+
+ self.ax1.imshow(self.image, extent=self.extent)
+ if horizontal:
+ self.ax1.set_xlabel('distance [pixels]')
+ else:
+ self.ax1.set_ylabel('distance [pixels]')
+ self.ax1.set_aspect('equal')
+
+ self.rect = patches.Rectangle((0, 0), 1, 1, linewidth=1, edgecolor='r', facecolor='red', alpha=0.2)
+ self.ax1.add_patch(self.rect)
+ self.intensity_scale = self.tags['spectra'][f'{self.x}-{self.y}']['intensity_scale']
+ self.spectrum = self.tags['spectra'][f'{self.x}-{self.y}']['spectrum'] * self.intensity_scale
+ self.energy_scale = self.tags['spectra'][f'{self.x}-{self.y}']['energy_scale']
+
+ self.ax2.plot(self.energy_scale, self.spectrum)
+ self.ax2.set_title(f' spectrum {self.x},{self.y} ')
+ self.ax2.set_xlabel('energy loss [eV]')
+ self.ax2.set_ylabel(self.tags['ylabel'])
+ self.cid = self.figure.canvas.mpl_connect('button_press_event', self.onclick)
+
+ plt.tight_layout()
+
+ def on_button_clicked(self, b):
+ # print(b['owner'].description)
+ selection = b['owner'].description
+ if b['new']:
+ if selection == 'fit_composition':
+ if 'region_tags' in self.tags and 'edges_present' in self.tags \
+ and 'acceleration_voltage' in self.tags \
+ and 'collection_angle' in self.tags:
+ pass
+ else:
+ self.buttons[3].value = False
+ return
+ elif selection in ['fix_energy', 'fit_zero_loss']:
+ if self.energy_scale[0] > 0:
+ button_index = ['fix_energy', 'fit_zero_loss'].index(selection)
+ self.buttons[button_index].value = False
+ return
+ self.analysis.append(selection)
+ self.update()
+ else:
+
+ if selection in self.analysis:
+ self.analysis.remove(selection)
+
+ def do_all(self, selection=None, verbose=True):
+ x = self.x
+ y = self.y
+ if selection is None:
+ selection = self.analysis
+ for self.x in range(self.cube.shape[1]):
+ if verbose:
+ print(f' row: {self.x}')
+ for self.y in range(self.cube.shape[0]):
+
+ if 'fit_zero_loss' in selection:
+ title = self.fit_zero_loss(plot_this=False)
+
+ elif 'fix_energy' in selection:
+ self.ax2.set_title('bn')
+ title = self.fix_energy()
+
+ elif 'fit_composition' in selection:
+ title = self.fit_quantification(plot_this=False)
+
+ self.x = x
+ self.y = y
+
+ def onclick(self, event):
+ x = int(event.xdata)
+ y = int(event.ydata)
+
+ # print(x,y)
+ if self.rectangle[0] <= x < self.rectangle[0] + self.rectangle[1]:
+ if self.rectangle[2] <= y < self.rectangle[2] + self.rectangle[3]:
+ self.x = int((x - self.rectangle[0]) / self.rectangle[1] * self.cube.shape[1])
+ self.y = int((y - self.rectangle[2]) / self.rectangle[3] * self.cube.shape[0])
+ else:
+ return
+ else:
+ return
+
+ if event.inaxes in [self.ax1]:
+ x = (self.x * self.rectangle[1] / self.cube.shape[1] + self.rectangle[0])
+ y = (self.y * self.rectangle[3] / self.cube.shape[0] + self.rectangle[2])
+
+ self.rect.set_xy([x, y])
+ self.update()
+
+ def update(self):
+ xlim = self.ax2.get_xlim()
+ ylim = self.ax2.get_ylim()
+ self.ax2.clear()
+ self.intensity_scale = self.tags['spectra'][f'{self.x}-{self.y}']['intensity_scale']
+ self.spectrum = self.tags['spectra'][f'{self.x}-{self.y}']['spectrum'] * self.intensity_scale
+ self.energy_scale = self.tags['spectra'][f'{self.x}-{self.y}']['energy_scale']
+
+ if 'fit_zero_loss' in self.analysis:
+ title = self.fit_zero_loss()
+ self.ax2.set_title(title)
+ elif 'fix_energy' in self.analysis:
+ self.ax2.set_title('bn')
+ title = self.fix_energy()
+ self.ax2.set_title(title)
+
+ elif 'fit_composition' in self.analysis:
+ title = self.fit_quantification()
+ self.ax2.set_title(title)
+
+ else:
+ self.ax2.set_title(f' spectrum {self.x},{self.y} ')
+ self.ax2.plot(self.energy_scale, self.spectrum, color='#1f77b4', label='experiment')
+
+ if self.plot_legend:
+ self.ax2.legend(shadow=True)
+ self.ax2.set_xlim(xlim)
+ self.ax2.set_ylim(ylim)
+ self.ax2.set_xlabel('energy loss [eV]')
+ self.ax2.set_ylabel(self.tags['ylabel'])
+ self.ax2.set_xlim(xlim)
+
+ # self.ax2.draw()
+
+ def set_tags(self, channel):
+ # TODO: change to sidpy dataset tags = ft.h5_get_dictionary(channel)
+ tags = {}
+ if tags['data_type'] == 'spectrum_image':
+ tags['image'] = tags['data']
+ tags['data'] = tags['cube'][0, 0, :]
+ if 'intensity_scale_ppm' not in channel:
+ channel['intensity_scale_ppm'] = 1
+
+ tags['ylabel'] = 'intensity [a.u.]'
+ tags['spectra'] = {}
+ for x in range(tags['spatial_size_y']):
+ for y in range(tags['spatial_size_x']):
+ tags['spectra'][f'{x}-{y}'] = {}
+ tags['spectra'][f'{x}-{y}']['spectrum'] = tags['cube'][y, x, :]
+ tags['spectra'][f'{x}-{y}']['energy_scale'] = tags['energy_scale']
+ tags['spectra'][f'{x}-{y}']['intensity_scale'] = 1 / tags['cube'][y, x, :].sum() * 1e6
+ tags['ylabel'] = 'inel. scat. int. [ppm]'
+
+ return tags
+
+ def fix_energy(self):
+
+ energy_scale = self.tags['spectra'][f'{self.x}-{self.y}']['energy_scale']
+ spectrum = self.tags['spectra'][f'{self.x}-{self.y}']['spectrum'] * self.intensity_scale
+ fwhm, delta_e = eels.fix_energy_scale(spectrum, energy_scale)
+ self.tags['spectra'][f'{self.x}-{self.y}']['delta_e'] = delta_e
+ self.tags['spectra'][f'{self.x}-{self.y}']['fwhm'] = fwhm
+ self.energy_scale = energy_scale - delta_e
+ title = f'spectrum {self.x},{self.y} fwhm: {fwhm:.2f}, dE: {delta_e:.3f}'
+ return title
+
+ def fit_zero_loss(self, plot_this=True):
+
+ energy_scale = self.tags['spectra'][f'{self.x}-{self.y}']['energy_scale']
+ spectrum = self.tags['spectra'][f'{self.x}-{self.y}']['spectrum'] * self.intensity_scale
+ if 'zero_loss_fit_width' not in self.tags:
+ self.tags['zero_loss_fit_width'] = .5
+ if self.tags['zero_loss_fit_width'] / (energy_scale[1] - energy_scale[0]) < 6:
+ self.tags['zero_loss_fit_width'] = (energy_scale[1] - energy_scale[0]) * 6
+ fwhm, delta_e = eels.fix_energy_scale(spectrum, energy_scale)
+ energy_scale = energy_scale - delta_e
+ z_oss, p_zl = eels.resolution_function(energy_scale, spectrum, self.tags['zero_loss_fit_width'])
+ fwhm2, delta_e2 = eels.fix_energy_scale(z_oss, energy_scale)
+
+ self.tags['spectra'][f'{self.x}-{self.y}']['resolution_function'] = z_oss
+ self.tags['spectra'][f'{self.x}-{self.y}']['p_zl'] = p_zl
+ self.tags['spectra'][f'{self.x}-{self.y}']['delta_e'] = delta_e
+ self.tags['spectra'][f'{self.x}-{self.y}']['fwhm_resolution'] = fwhm2
+ self.tags['spectra'][f'{self.x}-{self.y}']['fwhm'] = fwhm
+
+ if plot_this:
+ self.ax2.plot(energy_scale, z_oss, label='resolution function', color='black')
+ self.ax2.plot(energy_scale, self.spectrum - z_oss, label='difference', color='orange')
+ self.ax2.axhline(linewidth=0.5, color='black')
+ self.energy_scale = energy_scale
+ title = f'spectrum {self.x},{self.y} fwhm: {fwhm:.2f}' # ', dE: {delta_e2:.5e}'
+ return title
+
+ def fit_quantification(self, plot_this=True):
+ energy_scale = self.tags['spectra'][f'{self.x}-{self.y}']['energy_scale']
+ spectrum = self.tags['spectra'][f'{self.x}-{self.y}']['spectrum'] * self.intensity_scale
+ edges = eels.make_edges(self.tags['edges_present'], energy_scale, self.tags['acceleration_voltage'],
+ self.tags['collection_angle'])
+ edges = eels.fit_edges(spectrum, self.tags['spectra'][f'{self.x}-{self.y}']['energy_scale'],
+ self.tags['region_tags'], edges)
+ self.tags['spectra'][f'{self.x}-{self.y}']['edges'] = edges.copy()
+ if plot_this:
+ self.ax2.plot(energy_scale, edges['model']['spectrum'], label='model')
+ self.ax2.plot(energy_scale, self.spectrum - edges['model']['spectrum'], label='difference')
+ self.ax2.axhline(linewidth=0.5, color='black')
+ else:
+ self.tags['spectra'][f'{self.x}-{self.y}']['do_all'] = 'done'
+ title = f'spectrum {self.x},{self.y} '
+
+ for key in edges:
+ if key.isdigit():
+ title = title + f"{edges[key]['element']}: {edges[key]['areal_density']:.2e}; "
+
+ return title
+
+ def set_legend(self, set_legend):
+ self.plot_legend = set_legend
+
+ def get_xy(self):
+ return [self.x, self.y]
+
+ def get_current_spectrum(self):
+ return self.cube[self.y, self.x, :]
+
+ def set_z_contrast_image(self, z_channel=None):
+ if z_channel is not None:
+ self.tags['Z_contrast_channel'] = z_channel
+ if 'Z_contrast_channel' not in self.tags:
+ print('add Z contrast channel group to dictionary first!')
+ return
+
+ z_tags = {} # TODO change to sidpy dataset ft.h5_get_dictionary(z_channel)
+ extent = [self.rectangle[0], self.rectangle[0] + self.rectangle[1],
+ self.rectangle[2] + self.rectangle[3], self.rectangle[2]]
+ self.ax1.imshow(z_tags['data'], extent=extent, cmap='gray')
+
+ def overlay_z_contrast_image(self, z_channel=None):
+
+ if self.SI:
+ if z_channel is not None:
+ self.tags['Z_contrast_channel'] = z_channel
+ if 'Z_contrast_channel' not in self.tags:
+ print('add survey channel group to dictionary first!')
+ return
+
+ z_tags = {} # TODO: change to sidpy ft.h5_get_dictionary(self.tags['Z_contrast_channel'])
+
+ xlim = self.ax1.get_xlim()
+ ylim = self.ax1.get_ylim()
+ extent = [self.rectangle[0], self.rectangle[0] + self.rectangle[1],
+ self.rectangle[2] + self.rectangle[3], self.rectangle[2]]
+ self.ax1.imshow(z_tags['data'], extent=extent, cmap='viridis', alpha=0.5)
+ self.ax1.set_ylim(ylim)
+ self.ax1.set_xlim(xlim)
+
+ def overlay_data(self, data=None):
+
+ if self.SI:
+ if data is None:
+ data = self.cube.sum(axis=2)
+
+ xlim = self.ax1.get_xlim()
+ ylim = self.ax1.get_ylim()
+ extent = [self.rectangle[0], self.rectangle[0] + self.rectangle[1],
+ self.rectangle[2] + self.rectangle[3], self.rectangle[2]]
+ self.ax1.imshow(data, extent=extent, alpha=0.7, cmap='viridis')
+ self.ax1.set_ylim(ylim)
+ self.ax1.set_xlim(xlim)
+
+ def set_survey_image(self, si_channel=None):
+
+ if si_channel is not None:
+ self.tags['survey_channel'] = si_channel
+ if 'survey_channel' not in self.tags:
+ print('add survey channel group to dictionary first!')
+ return
+ si_channel = self.tags['survey_channel']
+ si_tags = {} # TODO: change to sidpy ft.h5_get_dictionary(si_channel)
+ tags2 = dict(si_channel.attrs)
+
+ self.ax1.set_aspect('equal')
+ self.scaleX = si_channel['spatial_scale_x'][()]
+ self.scaleY = si_channel['spatial_scale_y'][()]
+
+ self.ax1.imshow(si_tags['data'], extent=si_tags['extent'], cmap='gray')
+ if self.horizontal:
+ self.ax1.set_xlabel('distance [nm]')
+ else:
+ self.ax1.set_ylabel('distance [nm]')
+
+ annotation_done = []
+ for key in tags2:
+ if 'annotations' in key:
+ annotation_number = key[12]
+ if annotation_number not in annotation_done:
+ annotation_done.append(annotation_number)
+
+ if tags2['annotations_' + annotation_number + '_type'] == 'text':
+ x = tags2['annotations_' + annotation_number + '_x']
+ y = tags2['annotations_' + annotation_number + '_y']
+ text = tags2['annotations_' + annotation_number + '_text']
+ self.ax1.text(x, y, text, color='r')
+
+ elif tags2['annotations_' + annotation_number + '_type'] == 'circle':
+ radius = 20 * self.scaleX # tags['annotations'][key]['radius']
+ xy = tags2['annotations_' + annotation_number + '_position']
+ circle = patches.Circle(xy, radius, color='r', fill=False)
+ self.ax1.add_artist(circle)
+
+ elif tags2['annotations_' + annotation_number + '_type'] == 'spectrum image':
+ width = tags2['annotations_' + annotation_number + '_width']
+ height = tags2['annotations_' + annotation_number + '_height']
+ position = tags2['annotations_' + annotation_number + '_position']
+ rectangle = patches.Rectangle(position, width, height, color='r', fill=False)
+ self.rectangle = [position[0], width, position[1], height]
+ self.ax1.add_artist(rectangle)
+ self.ax1.text(position[0], position[1], 'Spectrum Image', color='r')
+ self.rect.set_width(width / self.cube.shape[1])
+ self.rect.set_height(height / self.cube.shape[0])
+ self.SI = True
+
+
+[docs]class ElementalEdges(object):
+ """ Adds ionization edges of element z to plot with axis ax
+
+ There is an optional parameter maximum_chemical_shift which allows to change
+ the energy range in which the edges are searched.
+
+ available functions:
+ - update(): updates the drawing of ionization edges
+ - set_edge(Z) : changes atomic number and updates everything accordingly
+ - disconnect: makes everything invisible and stops drawing
+ - reconnect: undo of disconnect
+
+ usage:
+ >> fig, ax = plt.subplots()
+ >> ax.plot(energy_scale, spectrum)
+ >> Z= 42
+ >> cursor = ElementalEdges(ax, Z)
+
+
+ see Chapter4 'CH4-Working_with_X-Sections' notebook
+ """
+
+ def __init__(self, ax, z):
+ self.ax = ax
+ self.labels = None
+ self.lines = None
+ self.Z = eels.get_z(z)
+ self.color = 'black'
+ self.x_sections = eels.get_x_sections()
+ self.cid = ax.figure.canvas.mpl_connect('draw_event', self.onresize)
+ # self.update() is not necessary because of a drawing event is issued
+
+ def set_edge(self, z):
+ self.Z = eels.get_z(z)
+ if self.cid is None:
+ self.cid = self.ax.figure.canvas.mpl_connect('draw_event', self.onresize)
+ self.update()
+
+ def onresize(self, event):
+ self.update()
+
+ def update(self):
+ if self.labels is not None:
+ for label in self.labels:
+ label.remove()
+ if self.lines is not None:
+ for line in self.lines:
+ line.remove()
+ self.labels = []
+ self.lines = []
+ x_min, x_max = self.ax.get_xlim()
+ y_min, y_max = self.ax.get_ylim()
+
+ element = str(self.Z)
+ x_sections = self.x_sections
+ for key in all_edges:
+ if key in x_sections[element] and 'onset' in x_sections[element][key]:
+ x = x_sections[element][key]['onset']
+ if x_min < x < x_max:
+ if key in first_close_edges:
+ label2 = self.ax.text(x, y_max, f"{x_sections[element]['name']}-{key}",
+ verticalalignment='top', rotation=0, color=self.color)
+ else:
+ label2 = self.ax.text(x, y_max, f"\n{x_sections[element]['name']}-{key}",
+ verticalalignment='top', color=self.color)
+ line2 = self.ax.axvline(x, ymin=0, ymax=1, color=self.color)
+
+ self.labels.append(label2)
+ self.lines.append(line2)
+
+ def reconnect(self):
+ self.cid = self.ax.figure.canvas.mpl_connect('draw_event', self.onresize)
+ self.update()
+
+ def disconnect(self):
+ if self.labels is not None:
+ for label in self.labels:
+ label.remove()
+ if self.lines is not None:
+ for line in self.lines:
+ line.remove()
+ self.labels = None
+ self.lines = None
+ self.ax.figure.canvas.mpl_disconnect(self.cid)
+
+
+[docs]class EdgesAtCursor(object):
+ """
+ Adds a Cursor to a plot, which plots all major (possible) ionization edges at
+ the cursor location if left (right) mouse button is clicked.
+
+ Attributes
+ ----------
+ ax: matplotlib axis
+ x: numpy array
+ energy_scale of spectrum
+ y: numpy array
+ intensities of spectrum
+ maximal_chemical_shift: float
+ optional parameter maximum_chemical_shift which allows to change the energy range in which the edges
+ are searched.
+
+ Example
+ -------
+ fig, ax = plt.subplots()
+ ax.plot(energy_scale, spectrum)
+ cursor = EdgesAtCursor(ax, energy_scale, spectrum)
+
+ see Chapter4 'CH4-Working_with_X-Sections' notebook
+
+ """
+
+ def __init__(self, ax, x, y, maximal_chemical_shift=5):
+ self.ax = ax
+ self.ly = ax.axvline(x[0], color='k', alpha=0.2) # the vert line
+ self.marker, = ax.plot(x[0], y[0], marker="o", color="crimson", zorder=3)
+ self.x = x
+ self.y = y
+ self.txt = ax.text(0.7, 0.9, '', verticalalignment='bottom')
+ self.select = 0
+ self.label = None
+ self.line = None
+ self.cid = ax.figure.canvas.mpl_connect('button_press_event', self.click)
+ self.mouse_cid = ax.figure.canvas.mpl_connect('motion_notify_event', self.mouse_move)
+ self.maximal_chemical_shift = maximal_chemical_shift
+
+ def click(self, event):
+
+ # print('click', event)
+ if not event.inaxes:
+ return
+ x, y = event.xdata, event.ydata
+
+ index = np.searchsorted(self.x, [x])[0]
+ x = self.x[index]
+ y = self.y[index]
+ self.select = x
+
+ y_min, y_max = self.ax.get_ylim()
+
+ if self.label is not None:
+ self.label.remove()
+ self.line.remove()
+ if event.button == 1:
+ self.label = self.ax.text(x, y_max, eels.find_major_edges(event.xdata, self.maximal_chemical_shift),
+ verticalalignment='top')
+ self.line, = self.ax.plot([x, x], [y_min, y_max], color='black')
+ if event.button == 3:
+ self.line, = self.ax.plot([x, x], [y_min, y_max], color='black')
+ self.label = self.ax.text(x, y_max, eels.find_all_edges(event.xdata, self.maximal_chemical_shift),
+ verticalalignment='top')
+ self.ax.set_ylim(y_min, y_max)
+
+ def mouse_move(self, event):
+ if not event.inaxes:
+ return
+
+ x, y = event.xdata, event.ydata
+ index = np.searchsorted(self.x, [x])[0]
+ x = self.x[index]
+ y = self.y[index]
+ self.select = x
+ self.ly.set_xdata(x)
+ self.marker.set_data([x], [y])
+ self.txt.set_text(f'\n x={x:1.2f}, y={y:1.2g}\n')
+
+ # self.ax.text(x, y*2,find_major_edges(x))
+ self.txt.set_position((x, y))
+ self.ax.figure.canvas.draw_idle()
+
+ def del_edges(self):
+ if self.label is not None:
+ self.label.remove()
+ self.line.remove()
+ self.label = None
+
+ def disconnect(self):
+ self.ly.remove()
+ self.marker.remove()
+ self.txt.remove()
+
+ self.ax.figure.canvas.mpl_disconnect(self.cid)
+ self.ax.figure.canvas.mpl_disconnect(self.mouse_cid)
+
+
+[docs]def make_box_layout():
+ return ipywidgets.Layout(border='solid 1px black', margin='0px 10px 10px 0px', padding='5px 5px 5px 5px')
+
+
+[docs]class plot_EELS(ipywidgets.HBox):
+ def __init__(self, dataset):
+ super().__init__()
+ output = ipywidgets.Output()
+ self.dataset = dataset
+ self.spec_dim = 0
+ initial_color = '#FF00DD'
+
+ with output:
+ self.fig, self.axis = plt.subplots(constrained_layout=True, figsize=(5, 3.5))
+
+ self.axis.set_title(dataset.title.split('/')[-1])
+ self.line, = self.axis.plot(dataset.dim_0.values, dataset, lw=2, label='spectrum')
+ legend = self.axis.legend(fancybox=True, shadow=True)
+
+ lines = [self.line]
+ self.line_dictionary = {} # Will map legend lines to original lines.
+ for legend_line, original_line in zip(legend.get_lines(), lines):
+ legend_line.set_picker(True) # Enable picking on the legend line.
+ self.line_dictionary[legend_line] = original_line
+ self.ax = self.axis
+ self.fig.canvas.toolbar_position = 'bottom'
+ self.fig.canvas.mpl_connect('pick_event', self.on_legend_pick)
+
+ # define widgets
+ int_slider = ipywidgets.IntSlider(
+ value=1,
+ min=0,
+ max=10,
+ step=1,
+ description='freq'
+ )
+ self.offset = ipywidgets.Text(
+ value='0',
+ width=5,
+ description='offset',
+ continuous_update=False
+ )
+ self.dispersion = ipywidgets.Text(
+ value='0',
+ width=5,
+ description='dispersion',
+ continuous_update=False
+ )
+
+ self.exposure = ipywidgets.Text(
+ value='0',
+ width=5,
+ description='exposure',
+ continuous_update=False
+ )
+
+ button_energy_scale = ipywidgets.Button(description='Cursor')
+ button_elements_at_cursor = ipywidgets.Button(description='Elements Cursor')
+ button_main_elements = ipywidgets.Button(description='Main Elements')
+
+ controls = ipywidgets.VBox([
+ ipywidgets.HBox([self.offset, ipywidgets.Label('eV')]),
+ ipywidgets.HBox([self.dispersion, ipywidgets.Label('eV/channel')]),
+ ipywidgets.HBox([self.exposure, ipywidgets.Label('s')]),
+ button_energy_scale,
+ ipywidgets.HBox([button_elements_at_cursor, button_main_elements])
+ ])
+
+ controls.layout = make_box_layout()
+
+ out_box = ipywidgets.Box([output])
+ output.layout = make_box_layout()
+
+ # observe stuff
+ int_slider.observe(self.update, 'value')
+
+ self.offset.value = f'{self.dataset.dim_0.values[0]}'
+ self.offset.observe(self.set_dimension, 'value')
+ self.offset.value = f'{self.dataset.dim_0.values[0]}'
+
+ self.dispersion.observe(self.set_dimension, 'value')
+ self.dispersion.value = f'{self.dataset.dim_0.values[1] - self.dataset.dim_0.values[0]}'
+ self.dispersion.value = '0'
+ self.exposure.observe(self.update_exposure, 'value')
+ self.exposure.value = '0'
+
+ # add to children
+ self.children = [controls, output]
+
+[docs] def update(self):
+ """Draw line in plot"""
+ self.line.set_ydata(self.dataset)
+ self.line.set_xdata(self.dataset.dim_0.values)
+ # self.axis.plot(self.dataset.energy_loss, self.dataset)
+ self.fig.canvas.draw()
+
+ def line_color(self, change):
+ self.line.set_color(change.new)
+
+ def update_exposure(self):
+ pass
+
+ def update_ylabel(self, change):
+ self.ax.set_ylabel(change.new)
+
+ def set_dimension(self, change):
+ self.spec_dim = ft.get_dimensions_by_type('SPECTRAL', self.dataset)
+ self.spec_dim = self.spec_dim[0]
+ old_energy_scale = self.spec_dim[1]
+ energy_scale = np.arange(len(self.dataset.dim_0.values))*float(self.dispersion.value)+float(self.offset.value)
+ self.dataset.set_dimension(self.spec_dim[0], sidpy.Dimension(energy_scale,
+ name=old_energy_scale.name,
+ dimension_type='SPECTRAL',
+ units='eV',
+ quantity='energy loss'))
+ self.update()
+
+ def on_legend_pick(self, event):
+ legend_line = event.artist
+ original_line = self.line_dictionary[legend_line]
+ visible = not original_line.get_visible()
+ original_line.set_visible(visible)
+ legend_line.set_alpha(1.0 if visible else 0.2)
+ self.fig.canvas.draw()
+
+"""
+eels_tools
+Model based quantification of electron energy-loss data
+Copyright by Gerd Duscher
+
+The University of Tennessee, Knoxville
+Department of Materials Science & Engineering
+
+Sources:
+ M. Tian et al.
+
+Units:
+ everything is in SI units, except length is given in nm and angles in mrad.
+
+Usage:
+ See the notebooks for examples of these routines
+
+All the input and output is done through a dictionary which is to be found in the meta_data
+attribute of the sidpy.Dataset
+"""
+import numpy as np
+
+import scipy
+from scipy.interpolate import interp1d, splrep # splev, splint
+from scipy import interpolate
+from scipy.signal import peak_prominences
+from scipy.ndimage import gaussian_filter
+
+from scipy import constants
+import matplotlib.pyplot as plt
+
+import requests
+
+from scipy.optimize import leastsq # least square fitting routine fo scipy
+
+import pickle # pkg_resources,
+
+# ## And we use the image tool library of pyTEMlib
+import pyTEMlib.file_tools as ft
+from pyTEMlib.xrpa_x_sections import x_sections
+
+import sidpy
+from sidpy.base.num_utils import get_slope
+
+major_edges = ['K1', 'L3', 'M5', 'N5']
+all_edges = ['K1', 'L1', 'L2', 'L3', 'M1', 'M2', 'M3', 'M4', 'M5', 'N1', 'N2', 'N3', 'N4', 'N5', 'N6', 'N7', 'O1', 'O2',
+ 'O3', 'O4', 'O5', 'O6', 'O7', 'P1', 'P2', 'P3']
+first_close_edges = ['K1', 'L3', 'M5', 'M3', 'N5', 'N3']
+
+elements = [' ', 'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F', 'Ne', 'Na',
+ 'Mg', 'Al', 'Si', 'P', 'S', 'Cl', 'Ar', 'K', 'Ca', 'Sc', 'Ti', 'V',
+ 'Cr', 'Mn', 'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As', 'Se', 'Br',
+ 'Kr', 'Rb', 'Sr', 'Y', 'Zr', 'Nb', 'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag',
+ 'Cd', 'In', 'Sn', 'Sb', 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La', 'Ce', 'Pr',
+ 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb', 'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu',
+ 'Hf', 'Ta', 'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl', 'Pb', 'Bi']
+
+
+# kroeger_core(e_data,a_data,eps_data,ee,thick, relativistic =True)
+# kroeger_core2(e_data,a_data,eps_data,acceleration_voltage_kev,thickness, relativistic =True)
+# get_wave_length(e0)
+
+# plot_dispersion(plotdata, units, a_data, e_data, title, max_p, ee, ef = 4., ep= 16.8, Es = 0, IBT = [])
+# drude(tags, e, ep, ew, tnm, eb)
+# drude(ep, eb, gamma, e)
+# drude_lorentz(epsInf,leng, ep, eb, gamma, e, Amplitude)
+# zl_func( p, x)
+
+###
+[docs]def set_previous_quantification(current_dataset):
+ """Set previous quantification from a sidpy.Dataset"""
+
+ current_channel = current_dataset.h5_dataset.parent
+ found_metadata = False
+ for key in current_channel:
+ if 'Log' in key:
+ if current_channel[key]['analysis'][()] == 'EELS_quantification':
+ current_dataset.metadata.update(current_channel[key].attrs) # ToDo: find red dictionary
+ found_metadata = True
+ print('found previous quantification')
+
+ if not found_metadata:
+ # setting important experimental parameter
+ current_dataset.metadata['experiment'] = ft.read_dm3_info(current_dataset.original_metadata)
+
+ if 'experiment' not in current_dataset.metadata:
+ current_dataset.metadata['experiment'] = {}
+ if 'convergence_angle' not in current_dataset.metadata['experiment']:
+ current_dataset.metadata['experiment']['convergence_angle'] = 30
+ if 'collection_angle' not in current_dataset.metadata['experiment']:
+ current_dataset.metadata['experiment']['collection_angle'] = 50
+ if 'acceleration_voltage' not in current_dataset.metadata['experiment']:
+ current_dataset.metadata['experiment']['acceleration_voltage'] = 200000
+###
+
+# ###############################################################
+# Peak Fit Functions
+# ################################################################
+
+
+[docs]def residuals_smooth(p, x, y, only_positive_intensity):
+ """part of fit"""
+
+ err = (y - model_smooth(x, p, only_positive_intensity))
+ return err
+
+
+[docs]def model_smooth(x, p, only_positive_intensity=False):
+ """part of fit"""
+
+ y = np.zeros(len(x))
+
+ number_of_peaks = int(len(p) / 3)
+ for i in range(number_of_peaks):
+ if only_positive_intensity:
+ p[i * 3 + 1] = abs(p[i * 3 + 1])
+ p[i * 3 + 2] = abs(p[i * 3 + 2])
+ if p[i * 3 + 2] > abs(p[i * 3]) * 4.29193 / 2.0:
+ p[i * 3 + 2] = abs(p[i * 3]) * 4.29193 / 2. # ## width cannot extend beyond zero, maximum is FWTM/2
+
+ y = y + gauss(x, p[i * 3:])
+
+ return y
+
+
+[docs]def residuals_ll(p, x, y, only_positive_intensity):
+ """part of fit"""
+
+ err = (y - model_ll(x, p, only_positive_intensity)) / np.sqrt(np.abs(y))
+ return err
+
+
+[docs]def residuals_ll2(p, x, y, only_positive_intensity):
+ """part of fit"""
+
+ err = (y - model_ll(x, p, only_positive_intensity))
+ return err
+
+
+[docs]def model_ll(x, p, only_positive_intensity):
+ """part of fit"""
+
+ y = np.zeros(len(x))
+
+ number_of_peaks = int(len(p) / 3)
+ for i in range(number_of_peaks):
+ if only_positive_intensity:
+ p[i * 3 + 1] = abs(p[i * 3 + 1])
+ p[i * 3 + 2] = abs(p[i * 3 + 2])
+ if p[i * 3 + 2] > abs(p[i * 3]) * 4.29193 / 2.0:
+ p[i * 3 + 2] = abs(p[i * 3]) * 4.29193 / 2. # ## width cannot extend beyond zero, maximum is FWTM/2
+
+ y = y + gauss(x, p[i * 3:])
+
+ return y
+
+
+[docs]def fit_peaks(spectrum, energy_scale, pin, start_fit, end_fit, only_positive_intensity=False):
+ """fit peaks to spectrum
+
+ Parameters
+ ----------
+ spectrum: numpy array
+ spectrum to be fitted
+ energy_scale: numpy array
+ energy scale of spectrum
+ pin: list of float
+ intial guess of peaks position amplitude width
+ start_fit: int
+ channel where fit starts
+ end_fit: int
+ channel where fit starts
+ only_positive_intensity: boolean
+ allows only for positive amplitudes if True; default = False
+
+ Returns
+ -------
+ p: list of float
+ fitting parameters
+ """
+
+ # TODO: remove zero_loss_fit_width add absolute
+
+ fit_energy = energy_scale[start_fit:end_fit]
+ spectrum = np.array(spectrum)
+ fit_spectrum = spectrum[start_fit:end_fit]
+
+ pin_flat = [item for sublist in pin for item in sublist]
+ [p_out, _] = leastsq(residuals_ll, np.array(pin_flat), ftol=1e-3, args=(fit_energy, fit_spectrum,
+ only_positive_intensity))
+ p = []
+ for i in range(len(pin)):
+ if only_positive_intensity:
+ p_out[i * 3 + 1] = abs(p_out[i * 3 + 1])
+ p.append([p_out[i * 3], p_out[i * 3 + 1], abs(p_out[i * 3 + 2])])
+ return p
+
+
+#################################################################
+# CORE - LOSS functions
+#################################################################
+
+
+[docs]def get_x_sections(z=0):
+ """Reads X-ray fluorescent cross-sections from a pickle file.
+
+ Parameters
+ ----------
+ z: int
+ atomic number if zero all cross-sections will be returned
+
+ Returns
+ -------
+ dictionary
+ cross-section of an element or of all elements if z = 0
+
+ """
+ # pkl_file = open(data_path + '/edges_db.pkl', 'rb')
+ # x_sections = pickle.load(pkl_file)
+ # pkl_file.close()
+ # x_sections = pyTEMlib.config_dir.x_sections
+ z = int(z)
+
+ if z < 1:
+ return x_sections
+ else:
+ z = str(z)
+ if z in x_sections:
+ return x_sections[z]
+ else:
+ return 0
+
+
+[docs]def get_z(z):
+ """Returns the atomic number independent of input as a string or number
+
+ Parameter
+ ---------
+ z: int, str
+ atomic number of chemical symbol (0 if not valid)
+ """
+ x_sections = get_x_sections()
+
+ z_out = 0
+ if str(z).isdigit():
+ z_out = int(z)
+ elif isinstance(z, str):
+ for key in x_sections:
+ if x_sections[key]['name'].lower() == z.lower(): # Well one really should know how to write elemental
+ z_out = int(key)
+ return z_out
+
+
+[docs]def list_all_edges(z, verbose=False):
+ """List all ionization edges of an element with atomic number z
+
+ Parameters
+ ----------
+ z: int
+ atomic number
+
+ Returns
+ -------
+ out_string: str
+ string with all major edges in energy range
+ """
+
+ element = str(z)
+ x_sections = get_x_sections()
+ out_string = ''
+ if verbose:
+ print('Major edges')
+ edge_list = {x_sections[element]['name']: {}}
+
+ for key in all_edges:
+ if key in x_sections[element]:
+ if 'onset' in x_sections[element][key]:
+ if verbose:
+ print(f" {x_sections[element]['name']}-{key}: {x_sections[element][key]['onset']:8.1f} eV ")
+ out_string = out_string + f" {x_sections[element]['name']}-{key}: " \
+ f"{x_sections[element][key]['onset']:8.1f} eV /n"
+ edge_list[x_sections[element]['name']][key] = x_sections[element][key]['onset']
+ return out_string, edge_list
+
+
+[docs]def find_major_edges(edge_onset, maximal_chemical_shift=5.):
+ """Find all major edges within an energy range
+
+ Parameters
+ ----------
+ edge_onset: float
+ approximate energy of ionization edge
+ maximal_chemical_shift: float
+ optional, range of energy window around edge_onset to look for major edges
+
+ Returns
+ -------
+ text: str
+ string with all major edges in energy range
+
+ """
+ text = ''
+ x_sections = get_x_sections()
+ for element in x_sections:
+ for key in x_sections[element]:
+
+ # if isinstance(x_sections[element][key], dict):
+ if key in major_edges:
+
+ if abs(x_sections[element][key]['onset'] - edge_onset) < maximal_chemical_shift:
+ # print(element, x_sections[element]['name'], key, x_sections[element][key]['onset'])
+ text = text + f"\n {x_sections[element]['name']:2s}-{key}: " \
+ f"{x_sections[element][key]['onset']:8.1f} eV "
+
+ return text
+
+
+[docs]def find_all_edges(edge_onset, maximal_chemical_shift=5):
+ """Find all (major and minor) edges within an energy range
+
+ Parameters
+ ----------
+ edge_onset: float
+ approximate energy of ionization edge
+ maximal_chemical_shift: float
+ optional, range of energy window around edge_onset to look for major edges
+
+ Returns
+ -------
+ text: str
+ string with all edges in energy range
+
+ """
+
+ text = ''
+ x_sections = get_x_sections()
+ for element in x_sections:
+ for key in x_sections[element]:
+
+ if isinstance(x_sections[element][key], dict):
+ if 'onset' in x_sections[element][key]:
+ if abs(x_sections[element][key]['onset'] - edge_onset) < maximal_chemical_shift:
+ # print(element, x_sections[element]['name'], key, x_sections[element][key]['onset'])
+ text = text + f"\n {x_sections[element]['name']:2s}-{key}: " \
+ f"{x_sections[element][key]['onset']:8.1f} eV "
+ return text
+
+
+[docs]def find_associated_edges(dataset):
+ onsets = []
+ edges = []
+ if 'edges' in dataset.metadata:
+ for key, edge in dataset.metadata['edges'].items():
+ if key.isdigit():
+ element = edge['element']
+ pre_edge = 0. # edge['onset']-edge['start_exclude']
+ post_edge = edge['end_exclude'] - edge['onset']
+
+ for sym in edge['all_edges']: # TODO: Could be replaced with exclude
+ onsets.append(edge['all_edges'][sym]['onset'] + edge['chemical_shift']-pre_edge)
+ edges.append([key, f"{element}-{sym}", onsets[-1]])
+ for key, peak in dataset.metadata['peak_fit']['peaks'].items():
+ if key.isdigit():
+ distance = dataset.energy_loss[-1]
+ index = -1
+ for ii, onset in enumerate(onsets):
+ if onset < peak['position'] < onset+post_edge:
+ if distance > np.abs(peak['position'] - onset):
+ distance = np.abs(peak['position'] - onset) # TODO: check whether absolute is good
+ distance_onset = peak['position'] - onset
+ index = ii
+ if index >= 0:
+ peak['associated_edge'] = edges[index][1] # check if more info is necessary
+ peak['distance_to_onset'] = distance_onset
+
+
+[docs]def find_white_lines(dataset):
+ if 'edges' in dataset.metadata:
+ white_lines = {}
+ for index, peak in dataset.metadata['peak_fit']['peaks'].items():
+ if index.isdigit():
+ if 'associated_edge' in peak:
+ if peak['associated_edge'][-2:] in ['L3', 'L2', 'M5', 'M4']:
+ if peak['distance_to_onset'] < 10:
+ area = np.sqrt(2 * np.pi) * peak['amplitude'] * np.abs(peak['width']/np.sqrt(2 * np.log(2)))
+ if peak['associated_edge'] not in white_lines:
+ white_lines[peak['associated_edge']] = 0.
+ if area > 0:
+ white_lines[peak['associated_edge']] += area # TODO: only positive ones?
+ white_line_ratios = {}
+ white_line_sum = {}
+ for sym, area in white_lines.items():
+ if sym[-2:] in ['L2', 'M4', 'M2']:
+ if area > 0 and f"{sym[:-1]}{int(sym[-1]) + 1}" in white_lines:
+ if white_lines[f"{sym[:-1]}{int(sym[-1]) + 1}"] > 0:
+ white_line_ratios[f"{sym}/{sym[-2]}{int(sym[-1]) + 1}"] = area / white_lines[
+ f"{sym[:-1]}{int(sym[-1]) + 1}"]
+ white_line_sum[f"{sym}+{sym[-2]}{int(sym[-1]) + 1}"] = (
+ area + white_lines[f"{sym[:-1]}{int(sym[-1]) + 1}"])
+
+ areal_density = 1.
+ if 'edges' in dataset.metadata:
+ for key, edge in dataset.metadata['edges'].items():
+ if key.isdigit():
+ if edge['element'] == sym.split('-')[0]:
+ areal_density = edge['areal_density']
+ break
+ white_line_sum[f"{sym}+{sym[-2]}{int(sym[-1]) + 1}"] /= areal_density
+
+ dataset.metadata['peak_fit']['white_lines'] = white_lines
+ dataset.metadata['peak_fit']['white_line_ratios'] = white_line_ratios
+ dataset.metadata['peak_fit']['white_line_sums'] = white_line_sum
+
+
+[docs]def second_derivative(dataset, sensitivity):
+ """Calculates second derivative of a sidpy.dataset"""
+
+ dim = dataset.get_spectrum_dims()
+ energy_scale = np.array(dataset._axes[dim[0]])
+ if dataset.data_type.name == 'SPECTRAL_IMAGE':
+ spectrum = dataset.view.get_spectrum()
+ else:
+ spectrum = np.array(dataset)
+
+ spec = scipy.ndimage.gaussian_filter(spectrum, 3)
+
+ dispersion = get_slope(energy_scale)
+ second_dif = np.roll(spec, -3) - 2 * spec + np.roll(spec, +3)
+ second_dif[:3] = 0
+ second_dif[-3:] = 0
+
+ # find if there is a strong edge at high energy_scale
+ noise_level = 2. * np.std(second_dif[3:50])
+ [indices, _] = scipy.signal.find_peaks(second_dif, noise_level)
+ width = 50 / dispersion
+ if width < 50:
+ width = 50
+ start_end_noise = int(len(energy_scale) - width)
+ for index in indices[::-1]:
+ if index > start_end_noise:
+ start_end_noise = index - 70
+
+ noise_level_start = sensitivity * np.std(second_dif[3:50])
+ noise_level_end = sensitivity * np.std(second_dif[start_end_noise: start_end_noise + 50])
+ slope = (noise_level_end - noise_level_start) / (len(energy_scale) - 400)
+ noise_level = noise_level_start + np.arange(len(energy_scale)) * slope
+ return second_dif, noise_level
+
+
+[docs]def find_edges(dataset, sensitivity=2.5):
+ """find edges within a sidpy.Dataset"""
+
+ dim = dataset.get_spectrum_dims()
+ energy_scale = np.array(dataset._axes[dim[0]])
+
+ second_dif, noise_level = second_derivative(dataset, sensitivity=sensitivity)
+
+ [indices, peaks] = scipy.signal.find_peaks(second_dif, noise_level)
+
+ peaks['peak_positions'] = energy_scale[indices]
+ peaks['peak_indices'] = indices
+ edge_energies = [energy_scale[50]]
+ edge_indices = []
+
+ [indices, _] = scipy.signal.find_peaks(-second_dif, noise_level)
+ minima = energy_scale[indices]
+
+ for peak_number in range(len(peaks['peak_positions'])):
+ position = peaks['peak_positions'][peak_number]
+ if position - edge_energies[-1] > 20:
+ impossible = minima[minima < position]
+ impossible = impossible[impossible > position - 5]
+ if len(impossible) == 0:
+ possible = minima[minima > position]
+ possible = possible[possible < position + 5]
+ if len(possible) > 0:
+ edge_energies.append((position + possible[0])/2)
+ edge_indices.append(np.searchsorted(energy_scale, (position + possible[0])/2))
+
+ selected_edges = []
+ for peak in edge_indices:
+ if 525 < energy_scale[peak] < 533:
+ selected_edges.append('O-K1')
+ else:
+ selected_edge = ''
+ edges = find_major_edges(energy_scale[peak], 20)
+ edges = edges.split('\n')
+ minimum_dist = 100.
+ for edge in edges[1:]:
+ edge = edge[:-3].split(':')
+ name = edge[0].strip()
+ energy = float(edge[1].strip())
+ if np.abs(energy - energy_scale[peak]) < minimum_dist:
+ minimum_dist = np.abs(energy - energy_scale[peak])
+ selected_edge = name
+
+ if selected_edge != '':
+ selected_edges.append(selected_edge)
+
+ return selected_edges
+
+
+[docs]def assign_likely_edges(edge_channels, energy_scale):
+ edges_in_list = []
+ result = {}
+ for channel in edge_channels:
+ if channel not in edge_channels[edges_in_list]:
+ shift = 5
+ element_list = find_major_edges(energy_scale[channel], maximal_chemical_shift=shift)
+ while len(element_list) < 1:
+ shift+=1
+ element_list = find_major_edges(energy_scale[channel], maximal_chemical_shift=shift)
+
+ if len(element_list) > 1:
+ while len(element_list) > 0:
+ shift-=1
+ element_list = find_major_edges(energy_scale[channel], maximal_chemical_shift=shift)
+ element_list = find_major_edges(energy_scale[channel], maximal_chemical_shift=shift+1)
+ element = (element_list[:4]).strip()
+ z = get_z(element)
+ result[element] =[]
+ _, edge_list = list_all_edges(z)
+
+ for peak in edge_list:
+ for edge in edge_list[peak]:
+ possible_minor_edge = np.argmin(np.abs(energy_scale[edge_channels]-edge_list[peak][edge]))
+ if np.abs(energy_scale[edge_channels[possible_minor_edge]]-edge_list[peak][edge]) < 3:
+ #print('nex', next_e)
+ edges_in_list.append(possible_minor_edge)
+
+ result[element].append(edge)
+
+ return result
+
+
+[docs]def auto_id_edges(dataset):
+ edge_channels = identify_edges(dataset)
+ dim = dataset.get_spectrum_dims()
+ energy_scale = np.array(dataset._axes[dim[0]])
+ found_edges = assign_likely_edges(edge_channels, energy_scale)
+ return found_edges
+
+
+[docs]def identify_edges(dataset, noise_level=2.0):
+ """
+ Using first derivative to determine edge onsets
+ Any peak in first derivative higher than noise_level times standard deviation will be considered
+
+ Parameters
+ ----------
+ dataset: sidpy.Dataset
+ the spectrum
+ noise_level: float
+ ths number times standard deviation in first derivative decides on whether an edge onset is significant
+
+ Return
+ ------
+ edge_channel: numpy.ndarray
+
+ """
+ dim = dataset.get_spectrum_dims()
+ energy_scale = np.array(dataset._axes[dim[0]])
+ dispersion = get_slope(energy_scale)
+ spec = scipy.ndimage.gaussian_filter(dataset, 3/dispersion) # smooth with 3eV wideGaussian
+
+ first_derivative = spec - np.roll(spec, +2)
+ first_derivative[:3] = 0
+ first_derivative[-3:] = 0
+
+ # find if there is a strong edge at high energy_scale
+ noise_level = noise_level*np.std(first_derivative[3:50])
+ [edge_channels, _] = scipy.signal.find_peaks(first_derivative, noise_level)
+
+ return edge_channels
+
+
+[docs]def add_element_to_dataset(dataset, z):
+ """
+ """
+ # We check whether this element is already in the
+ energy_scale = dataset.energy_loss
+ zz = get_z(z)
+ if 'edges' not in dataset.metadata:
+ dataset.metadata['edges'] = {'model': {}, 'use_low_loss': False}
+ index = 0
+ for key, edge in dataset.metadata['edges'].items():
+ if key.isdigit():
+ index += 1
+ if 'z' in edge:
+ if zz == edge['z']:
+ index = int(key)
+ break
+
+ major_edge = ''
+ minor_edge = ''
+ all_edges = {}
+ x_section = get_x_sections(zz)
+ edge_start = 10 # int(15./ft.get_slope(self.energy_scale)+0.5)
+ for key in x_section:
+ if len(key) == 2 and key[0] in ['K', 'L', 'M', 'N', 'O'] and key[1].isdigit():
+ if energy_scale[edge_start] < x_section[key]['onset'] < energy_scale[-edge_start]:
+ if key in ['K1', 'L3', 'M5', 'M3']:
+ major_edge = key
+
+ all_edges[key] = {'onset': x_section[key]['onset']}
+
+ if major_edge != '':
+ key = major_edge
+ elif minor_edge != '':
+ key = minor_edge
+ else:
+ print(f'Could not find no edge of {zz} in spectrum')
+ return False
+
+
+ if str(index) not in dataset.metadata['edges']:
+ dataset.metadata['edges'][str(index)] = {}
+
+ start_exclude = x_section[key]['onset'] - x_section[key]['excl before']
+ end_exclude = x_section[key]['onset'] + x_section[key]['excl after']
+
+ dataset.metadata['edges'][str(index)] = {'z': zz, 'symmetry': key, 'element': elements[zz],
+ 'onset': x_section[key]['onset'], 'end_exclude': end_exclude,
+ 'start_exclude': start_exclude}
+ dataset.metadata['edges'][str(index)]['all_edges'] = all_edges
+ dataset.metadata['edges'][str(index)]['chemical_shift'] = 0.0
+ dataset.metadata['edges'][str(index)]['areal_density'] = 0.0
+ dataset.metadata['edges'][str(index)]['original_onset'] = dataset.metadata['edges'][str(index)]['onset']
+ return True
+
+
+[docs]def make_edges(edges_present, energy_scale, e_0, coll_angle, low_loss=None):
+ """Makes the edges dictionary for quantification
+
+ Parameters
+ ----------
+ edges_present: list
+ list of edges
+ energy_scale: numpy array
+ energy scale on which to make cross-section
+ e_0: float
+ acceleration voltage (in V)
+ coll_angle: float
+ collection angle in mrad
+ low_loss: numpy array with same length as energy_scale
+ low_less spectrum with which to convolve the cross-section (default=None)
+
+ Returns
+ -------
+ edges: dict
+ dictionary with all information on cross-section
+ """
+ x_sections = get_x_sections()
+ edges = {}
+ for i, edge in enumerate(edges_present):
+ element, symmetry = edge.split('-')
+ z = 0
+ for key in x_sections:
+ if element == x_sections[key]['name']:
+ z = int(key)
+ edges[i] = {}
+ edges[i]['z'] = z
+ edges[i]['symmetry'] = symmetry
+ edges[i]['element'] = element
+
+ for key in edges:
+ xsec = x_sections[str(edges[key]['z'])]
+ if 'chemical_shift' not in edges[key]:
+ edges[key]['chemical_shift'] = 0
+ if 'symmetry' not in edges[key]:
+ edges[key]['symmetry'] = 'K1'
+ if 'K' in edges[key]['symmetry']:
+ edges[key]['symmetry'] = 'K1'
+ elif 'L' in edges[key]['symmetry']:
+ edges[key]['symmetry'] = 'L3'
+ elif 'M' in edges[key]['symmetry']:
+ edges[key]['symmetry'] = 'M5'
+ else:
+ edges[key]['symmetry'] = edges[key]['symmetry'][0:2]
+
+ edges[key]['original_onset'] = xsec[edges[key]['symmetry']]['onset']
+ edges[key]['onset'] = edges[key]['original_onset'] + edges[key]['chemical_shift']
+ edges[key]['start_exclude'] = edges[key]['onset'] - xsec[edges[key]['symmetry']]['excl before']
+ edges[key]['end_exclude'] = edges[key]['onset'] + xsec[edges[key]['symmetry']]['excl after']
+
+ edges = make_cross_sections(edges, energy_scale, e_0, coll_angle, low_loss)
+
+ return edges
+
+[docs]def fit_dataset(dataset):
+ energy_scale = dataset.energy_loss
+ if 'fit_area' not in dataset.metadata['edges']:
+ dataset.metadata['edges']['fit_area'] = {}
+ if 'fit_start' not in dataset.metadata['edges']['fit_area']:
+ dataset.metadata['edges']['fit_area']['fit_start'] = energy_scale[50]
+ if 'fit_end' not in dataset.metadata['edges']['fit_area']:
+ dataset.metadata['edges']['fit_area']['fit_end'] = energy_scale[-2]
+ dataset.metadata['edges']['use_low_loss'] = False
+
+ if 'experiment' in dataset.metadata:
+ exp = dataset.metadata['experiment']
+ if 'convergence_angle' not in exp:
+ raise ValueError('need a convergence_angle in experiment of metadata dictionary ')
+ alpha = exp['convergence_angle']
+ beta = exp['collection_angle']
+ beam_kv = exp['acceleration_voltage']
+ energy_scale = dataset.energy_loss
+ eff_beta = effective_collection_angle(energy_scale, alpha, beta, beam_kv)
+ edges = make_cross_sections(dataset.metadata['edges'], np.array(energy_scale), beam_kv, eff_beta)
+ dataset.metadata['edges'] = fit_edges2(dataset, energy_scale, edges)
+ areal_density = []
+ elements = []
+ for key in edges:
+ if key.isdigit(): # only edges have numbers in that dictionary
+ elements.append(edges[key]['element'])
+ areal_density.append(edges[key]['areal_density'])
+ areal_density = np.array(areal_density)
+ out_string = '\nRelative composition: \n'
+ for i, element in enumerate(elements):
+ out_string += f'{element}: {areal_density[i] / areal_density.sum() * 100:.1f}% '
+
+ print(out_string)
+
+
+[docs]def auto_chemical_composition(dataset):
+
+ found_edges = auto_id_edges(dataset)
+ for key in found_edges:
+ add_element_to_dataset(dataset, key)
+ fit_dataset(dataset)
+
+
+[docs]def make_cross_sections(edges, energy_scale, e_0, coll_angle, low_loss=None):
+ """Updates the edges dictionary with collection angle-integrated X-ray photo-absorption cross-sections
+
+ """
+ for key in edges:
+ if str(key).isdigit():
+ edges[key]['data'] = xsec_xrpa(energy_scale, e_0 / 1000., edges[key]['z'], coll_angle,
+ edges[key]['chemical_shift']) / 1e10 # from barnes to 1/nm^2
+ if low_loss is not None:
+ low_loss = np.roll(np.array(low_loss), 1024 - np.argmax(np.array(low_loss)))
+ edges[key]['data'] = scipy.signal.convolve(edges[key]['data'], low_loss/low_loss.sum(), mode='same')
+
+ edges[key]['onset'] = edges[key]['original_onset'] + edges[key]['chemical_shift']
+ edges[key]['X_section_type'] = 'XRPA'
+ edges[key]['X_section_source'] = 'pyTEMlib'
+
+ return edges
+
+
+[docs]def power_law(energy, a, r):
+ """power law for power_law_background"""
+ return a * np.power(energy, -r)
+
+
+[docs]def power_law_background(spectrum, energy_scale, fit_area, verbose=False):
+ """fit of power law to spectrum """
+
+ # Determine energy window for background fit in pixels
+ startx = np.searchsorted(energy_scale, fit_area[0])
+ endx = np.searchsorted(energy_scale, fit_area[1])
+
+ x = np.array(energy_scale)[startx:endx]
+
+ y = np.array(spectrum)[startx:endx].flatten()
+
+ # Initial values of parameters
+ p0 = np.array([1.0E+20, 3])
+
+ # background fitting
+ def bgdfit(pp, yy, xx):
+ err = yy - power_law(xx, pp[0], pp[1])
+ return err
+
+ [p, _] = leastsq(bgdfit, p0, args=(y, x), maxfev=2000)
+
+ background_difference = y - power_law(x, p[0], p[1])
+ background_noise_level = std_dev = np.std(background_difference)
+ if verbose:
+ print(f'Power-law background with amplitude A: {p[0]:.1f} and exponent -r: {p[1]:.2f}')
+ print(background_difference.max() / background_noise_level)
+
+ print(f'Noise level in spectrum {std_dev:.3f} counts')
+
+ # Calculate background over the whole energy scale
+ background = power_law(energy_scale, p[0], p[1])
+ return background, p
+
+
+[docs]def cl_model(x, p, number_of_edges, xsec):
+ """ core loss model for fitting"""
+ y = (p[9] * np.power(x, (-p[10]))) + p[7] * x + p[8] * x * x
+ for i in range(number_of_edges):
+ y = y + p[i] * xsec[i, :]
+ return y
+
+
+[docs]def fit_edges2(spectrum, energy_scale, edges):
+ """fit edges for quantification"""
+
+ dispersion = energy_scale[1] - energy_scale[0]
+ # Determine fitting ranges and masks to exclude ranges
+ mask = np.ones(len(spectrum))
+
+ background_fit_start = edges['fit_area']['fit_start']
+ if edges['fit_area']['fit_end'] > energy_scale[-1]:
+ edges['fit_area']['fit_end'] = energy_scale[-1]
+ background_fit_end = edges['fit_area']['fit_end']
+
+ startx = np.searchsorted(energy_scale, background_fit_start)
+ endx = np.searchsorted(energy_scale, background_fit_end)
+ mask[0:startx] = 0.0
+ mask[endx:-1] = 0.0
+ for key in edges:
+ if key.isdigit():
+ if edges[key]['start_exclude'] > background_fit_start + dispersion:
+ if edges[key]['start_exclude'] < background_fit_end - dispersion * 2:
+ if edges[key]['end_exclude'] > background_fit_end - dispersion:
+ # we need at least one channel to fit.
+ edges[key]['end_exclude'] = background_fit_end - dispersion
+ startx = np.searchsorted(energy_scale, edges[key]['start_exclude'])
+ if startx < 2:
+ startx = 1
+ endx = np.searchsorted(energy_scale, edges[key]['end_exclude'])
+ mask[startx: endx] = 0.0
+
+ ########################
+ # Background Fit
+ ########################
+ bgd_fit_area = [background_fit_start, background_fit_end]
+ background, [A, r] = power_law_background(spectrum, energy_scale, bgd_fit_area, verbose=False)
+
+ #######################
+ # Edge Fit
+ #######################
+ x = energy_scale
+ blurred = gaussian_filter(spectrum, sigma=5)
+
+ y = blurred # now in probability
+ y[np.where(y < 1e-8)] = 1e-8
+
+ xsec = []
+ number_of_edges = 0
+ for key in edges:
+ if key.isdigit():
+ xsec.append(edges[key]['data'])
+ number_of_edges += 1
+ xsec = np.array(xsec)
+
+ def model(xx, pp):
+ yy = background + pp[6] + pp[7] * xx + pp[8] * xx * xx
+ for i in range(number_of_edges):
+ pp[i] = np.abs(pp[i])
+ yy = yy + pp[i] * xsec[i, :]
+ return yy
+
+ def residuals(pp, xx, yy):
+ err = np.abs((yy - model(xx, pp)) * mask) # / np.sqrt(np.abs(y))
+ return err
+
+ scale = y[100]
+ pin = np.array([scale / 5, scale / 5, scale / 5, scale / 5, scale / 5, scale / 5, -scale / 10, 1.0, 0.001])
+ [p, _] = leastsq(residuals, pin, args=(x, y))
+
+ for key in edges:
+ if key.isdigit():
+ edges[key]['areal_density'] = p[int(key)]
+
+ edges['model'] = {}
+ edges['model']['background'] = (background + p[6] + p[7] * x + p[8] * x * x)
+ edges['model']['background-poly_0'] = p[6]
+ edges['model']['background-poly_1'] = p[7]
+ edges['model']['background-poly_2'] = p[8]
+ edges['model']['background-A'] = A
+ edges['model']['background-r'] = r
+ edges['model']['spectrum'] = model(x, p)
+ edges['model']['blurred'] = blurred
+ edges['model']['mask'] = mask
+ edges['model']['fit_parameter'] = p
+ edges['model']['fit_area_start'] = edges['fit_area']['fit_start']
+ edges['model']['fit_area_end'] = edges['fit_area']['fit_end']
+
+ return edges
+
+
+[docs]def fit_edges(spectrum, energy_scale, region_tags, edges):
+ """fit edges for quantification"""
+
+ # Determine fitting ranges and masks to exclude ranges
+ mask = np.ones(len(spectrum))
+
+ background_fit_end = energy_scale[-1]
+ for key in region_tags:
+ end = region_tags[key]['start_x'] + region_tags[key]['width_x']
+
+ startx = np.searchsorted(energy_scale, region_tags[key]['start_x'])
+ endx = np.searchsorted(energy_scale, end)
+
+ if key == 'fit_area':
+ mask[0:startx] = 0.0
+ mask[endx:-1] = 0.0
+ else:
+ mask[startx:endx] = 0.0
+ if region_tags[key]['start_x'] < background_fit_end: # Which is the onset of the first edge?
+ background_fit_end = region_tags[key]['start_x']
+
+ ########################
+ # Background Fit
+ ########################
+ bgd_fit_area = [region_tags['fit_area']['start_x'], background_fit_end]
+ background, [A, r] = power_law_background(spectrum, energy_scale, bgd_fit_area, verbose=False)
+
+ #######################
+ # Edge Fit
+ #######################
+ x = energy_scale
+ blurred = gaussian_filter(spectrum, sigma=5)
+
+ y = blurred # now in probability
+ y[np.where(y < 1e-8)] = 1e-8
+
+ xsec = []
+ number_of_edges = 0
+ for key in edges:
+ if key.isdigit():
+ xsec.append(edges[key]['data'])
+ number_of_edges += 1
+ xsec = np.array(xsec)
+
+ def model(xx, pp):
+ yy = background + pp[6] + pp[7] * xx + pp[8] * xx * xx
+ for i in range(number_of_edges):
+ pp[i] = np.abs(pp[i])
+ yy = yy + pp[i] * xsec[i, :]
+ return yy
+
+ def residuals(pp, xx, yy):
+ err = np.abs((yy - model(xx, pp)) * mask) # / np.sqrt(np.abs(y))
+ return err
+
+ scale = y[100]
+ pin = np.array([scale / 5, scale / 5, scale / 5, scale / 5, scale / 5, scale / 5, -scale / 10, 1.0, 0.001])
+ [p, _] = leastsq(residuals, pin, args=(x, y))
+
+ for key in edges:
+ if key.isdigit():
+ edges[key]['areal_density'] = p[int(key) - 1]
+
+ edges['model'] = {}
+ edges['model']['background'] = (background + p[6] + p[7] * x + p[8] * x * x)
+ edges['model']['background-poly_0'] = p[6]
+ edges['model']['background-poly_1'] = p[7]
+ edges['model']['background-poly_2'] = p[8]
+ edges['model']['background-A'] = A
+ edges['model']['background-r'] = r
+ edges['model']['spectrum'] = model(x, p)
+ edges['model']['blurred'] = blurred
+ edges['model']['mask'] = mask
+ edges['model']['fit_parameter'] = p
+ edges['model']['fit_area_start'] = region_tags['fit_area']['start_x']
+ edges['model']['fit_area_end'] = region_tags['fit_area']['start_x'] + region_tags['fit_area']['width_x']
+
+ return edges
+
+
+[docs]def find_peaks(dataset, fit_start, fit_end, sensitivity=2):
+ """find peaks in spectrum"""
+
+ if dataset.data_type.name == 'SPECTRAL_IMAGE':
+ spectrum = dataset.view.get_spectrum()
+ else:
+ spectrum = np.array(dataset)
+
+ spec_dim = ft.get_dimensions_by_type('SPECTRAL', dataset)[0]
+ energy_scale = np.array(spec_dim[1])
+
+ second_dif, noise_level = second_derivative(dataset, sensitivity=sensitivity)
+ [indices, _] = scipy.signal.find_peaks(-second_dif, noise_level)
+
+ start_channel = np.searchsorted(energy_scale, fit_start)
+ end_channel = np.searchsorted(energy_scale, fit_end)
+ peaks = []
+ for index in indices:
+ if start_channel < index < end_channel:
+ peaks.append(index - start_channel)
+
+ if 'model' in dataset.metadata:
+ model = dataset.metadata['model'][start_channel:end_channel]
+
+ elif energy_scale[0] > 0:
+ if 'edges' not in dataset.metadata:
+ return
+ if 'model' not in dataset.metadata['edges']:
+ return
+ model = dataset.metadata['edges']['model']['spectrum'][start_channel:end_channel]
+
+ else:
+ model = np.zeros(end_channel - start_channel)
+
+ energy_scale = energy_scale[start_channel:end_channel]
+
+ difference = np.array(spectrum)[start_channel:end_channel] - model
+ fit = np.zeros(len(energy_scale))
+ p_out = []
+ if len(peaks) > 0:
+ p_in = np.ravel([[energy_scale[i], difference[i], .7] for i in peaks])
+ [p_out, _] = scipy.optimize.leastsq(residuals_smooth, p_in, ftol=1e-3, args=(energy_scale,
+ difference,
+ False))
+ fit = fit + model_smooth(energy_scale, p_out, False)
+
+ peak_model = np.zeros(len(spec_dim[1]))
+ peak_model[start_channel:end_channel] = fit
+
+ return peak_model, p_out
+
+
+[docs]def find_maxima(y, number_of_peaks):
+ """ find the first most prominent peaks
+
+ peaks are then sorted by energy
+
+ Parameters
+ ----------
+ y: numpy array
+ (part) of spectrum
+ number_of_peaks: int
+
+ Returns
+ -------
+ numpy array
+ indices of peaks
+ """
+ blurred2 = gaussian_filter(y, sigma=2)
+ peaks, _ = scipy.signal.find_peaks(blurred2)
+ prominences = peak_prominences(blurred2, peaks)[0]
+ prominences_sorted = np.argsort(prominences)
+ peaks = peaks[prominences_sorted[-number_of_peaks:]]
+
+ peak_indices = np.argsort(peaks)
+ return peaks[peak_indices]
+
+
+[docs]def gauss(x, p): # p[0]==mean, p[1]= amplitude p[2]==fwhm,
+ """Gaussian Function
+
+ p[0]==mean, p[1]= amplitude p[2]==fwhm
+ area = np.sqrt(2* np.pi)* p[1] * np.abs(p[2] / 2.3548)
+ FWHM = 2 * np.sqrt(2 np.log(2)) * sigma = 2.3548 * sigma
+ sigma = FWHM/3548
+ """
+ if p[2] == 0:
+ return x * 0.
+ else:
+ return p[1] * np.exp(-(x - p[0]) ** 2 / (2.0 * (p[2] / 2.3548) ** 2))
+
+
+[docs]def lorentz(x, p):
+ """lorentzian function"""
+ lorentz_peak = 0.5 * p[2] / np.pi / ((x - p[0]) ** 2 + (p[2] / 2) ** 2)
+ return p[1] * lorentz_peak / lorentz_peak.max()
+
+
+[docs]def zl(x, p, p_zl):
+ """zero-loss function"""
+ p_zl_local = p_zl.copy()
+ p_zl_local[2] += p[0]
+ p_zl_local[5] += p[0]
+ zero_loss = zl_func(p_zl_local, x)
+ return p[1] * zero_loss / zero_loss.max()
+
+
+[docs]def model3(x, p, number_of_peaks, peak_shape, p_zl, pin=None, restrict_pos=0, restrict_width=0):
+ """ model for fitting low-loss spectrum"""
+ if pin is None:
+ pin = p
+
+ # if len([restrict_pos]) == 1:
+ # restrict_pos = [restrict_pos]*number_of_peaks
+ # if len([restrict_width]) == 1:
+ # restrict_width = [restrict_width]*number_of_peaks
+ y = np.zeros(len(x))
+
+ for i in range(number_of_peaks):
+ index = int(i * 3)
+ if restrict_pos > 0:
+ if p[index] > pin[index] * (1.0 + restrict_pos):
+ p[index] = pin[index] * (1.0 + restrict_pos)
+ if p[index] < pin[index] * (1.0 - restrict_pos):
+ p[index] = pin[index] * (1.0 - restrict_pos)
+
+ p[index + 1] = abs(p[index + 1])
+ # print(p[index + 1])
+ p[index + 2] = abs(p[index + 2])
+ if restrict_width > 0:
+ if p[index + 2] > pin[index + 2] * (1.0 + restrict_width):
+ p[index + 2] = pin[index + 2] * (1.0 + restrict_width)
+
+ if peak_shape[i] == 'Lorentzian':
+ y = y + lorentz(x, p[index:])
+ elif peak_shape[i] == 'zl':
+
+ y = y + zl(x, p[index:], p_zl)
+ else:
+ y = y + gauss(x, p[index:])
+ return y
+
+
+[docs]def sort_peaks(p, peak_shape):
+ """sort fitting parameters by peak position"""
+ number_of_peaks = int(len(p) / 3)
+ p3 = np.reshape(p, (number_of_peaks, 3))
+ sort_pin = np.argsort(p3[:, 0])
+
+ p = p3[sort_pin].flatten()
+ peak_shape = np.array(peak_shape)[sort_pin].tolist()
+
+ return p, peak_shape
+
+
+[docs]def add_peaks(x, y, peaks, pin_in=None, peak_shape_in=None, shape='Gaussian'):
+ """ add peaks to fitting parameters"""
+ if pin_in is None:
+ return
+ if peak_shape_in is None:
+ return
+
+ pin = pin_in.copy()
+
+ peak_shape = peak_shape_in.copy()
+ if isinstance(shape, str): # if peak_shape is only a string make a list of it.
+ shape = [shape]
+
+ if len(shape) == 1:
+ shape = shape * len(peaks)
+ for i, peak in enumerate(peaks):
+ pin.append(x[peak])
+ pin.append(y[peak])
+ pin.append(.3)
+ peak_shape.append(shape[i])
+
+ return pin, peak_shape
+
+
+[docs]def fit_model(x, y, pin, number_of_peaks, peak_shape, p_zl, restrict_pos=0, restrict_width=0):
+ """model for fitting low-loss spectrum"""
+
+ pin_original = pin.copy()
+
+ def residuals3(pp, xx, yy):
+ err = (yy - model3(xx, pp, number_of_peaks, peak_shape, p_zl, pin_original, restrict_pos,
+ restrict_width)) / np.sqrt(np.abs(yy))
+ return err
+
+ [p, _] = leastsq(residuals3, pin, args=(x, y))
+ # p2 = p.tolist()
+ # p3 = np.reshape(p2, (number_of_peaks, 3))
+ # sort_pin = np.argsort(p3[:, 0])
+
+ # p = p3[sort_pin].flatten()
+ # peak_shape = np.array(peak_shape)[sort_pin].tolist()
+
+ return p, peak_shape
+
+
+[docs]def fix_energy_scale(spec, energy=None):
+ """Shift energy scale according to zero-loss peak position
+
+ This function assumes that the fzero loss peak is the maximum of the spectrum.
+ """
+
+ # determine start and end fitting region in pixels
+ if isinstance(spec, sidpy.Dataset):
+ if energy is None:
+ energy = spec.energy_loss.values
+ spec = np.array(spec)
+
+ else:
+ if energy is None:
+ return
+ if not isinstance(spec, np.ndarray):
+ return
+
+ start = np.searchsorted(np.array(energy), -10)
+ end = np.searchsorted(np.array(energy), 10)
+ startx = np.argmax(spec[start:end]) + start
+
+ end = startx + 3
+ start = startx - 3
+ for i in range(10):
+ if spec[startx - i] < 0.3 * spec[startx]:
+ start = startx - i
+ if spec[startx + i] < 0.3 * spec[startx]:
+ end = startx + i
+ if end - start < 3:
+ end = startx + 2
+ start = startx - 2
+
+ x = np.array(energy[int(start):int(end)])
+ y = np.array(spec[int(start):int(end)]).copy()
+
+ y[np.nonzero(y <= 0)] = 1e-12
+
+ p0 = [energy[startx], 1000.0, (energy[end] - energy[start]) / 3.] # Initial guess is a normal distribution
+
+ def errfunc(pp, xx, yy):
+ return (gauss(xx, pp) - yy) / np.sqrt(yy) # Distance to the target function
+
+ [p1, _] = leastsq(errfunc, np.array(p0[:]), args=(x, y))
+ fit_mu, area, fwhm = p1
+
+ return fwhm, fit_mu
+
+[docs]def resolution_function2(dataset, width =0.3):
+ guess = [0.2, 1000, 0.02, 0.2, 1000, 0.2]
+ p0 = np.array(guess)
+
+ start = np.searchsorted(dataset.energy_loss, -width / 2.)
+ end = np.searchsorted(dataset.energy_loss, width / 2.)
+ x = dataset.energy_loss[start:end]
+ y = np.array(dataset)[start:end]
+ def zl2(pp, yy, xx):
+ eerr = (yy - zl_func(pp, xx)) # /np.sqrt(y)
+ return eerr
+
+ [p_zl, _] = leastsq(zl2, p0, args=(y, x), maxfev=2000)
+
+ z_loss = zl_func(p_zl, dataset.energy_loss)
+ z_loss = dataset.like_data(z_loss)
+ z_loss.title = 'resolution_function'
+ z_loss.metadata['zero_loss_parameter']=p_zl
+
+ dataset.metadata['low_loss']['zero_loss'] = {'zero_loss_parameter': p_zl,
+ 'zero_loss_fit': 'Product2Lorentzians'}
+ zero_loss = dataset.like_array(z_loss)
+ return zero_loss, p_zl
+
+
+
+[docs]def resolution_function(energy_scale, spectrum, width, verbose=False):
+ """get resolution function (zero-loss peak shape) from low-loss spectrum"""
+
+ guess = [0.2, 1000, 0.02, 0.2, 1000, 0.2]
+ p0 = np.array(guess)
+
+ start = np.searchsorted(energy_scale, -width / 2.)
+ end = np.searchsorted(energy_scale, width / 2.)
+ x = energy_scale[start:end]
+ y = spectrum[start:end]
+
+ def zl2(pp, yy, xx):
+ eerr = (yy - zl_func(pp, xx)) # /np.sqrt(y)
+ return eerr
+
+ def zl_restrict(pp, yy, xx):
+
+ if pp[2] > xx[-1] * .8:
+ pp[2] = xx[-1] * .8
+ if pp[2] < xx[0] * .8:
+ pp[2] = xx[0] * .8
+
+ if pp[5] > xx[-1] * .8:
+ pp[5] = xx[-1] * .8
+ if pp[5] < x[0] * .8:
+ pp[5] = xx[0] * .8
+
+ if len(pp) > 6:
+ pp[7] = abs(pp[7])
+ if abs(pp[7]) > (pp[1] + pp[4]) / 10:
+ pp[7] = abs(pp[1] + pp[4]) / 10
+ if abs(pp[8]) > 1:
+ pp[8] = pp[8] / abs(pp[8])
+ pp[6] = abs(pp[6])
+ pp[9] = abs(pp[9])
+
+ pp[0] = abs(pp[0])
+ pp[3] = abs(pp[3])
+ if pp[0] > (xx[-1] - xx[0]) / 2.0:
+ pp[0] = xx[-1] - xx[0] / 2.0
+ if pp[3] > (xx[-1] - xx[0]) / 2.0:
+ pp[3] = xx[-1] - xx[0] / 2.0
+
+ yy[yy < 0] = 0. # no negative numbers in sqrt below
+ eerr = (yy - zl_func(pp, xx)) / np.sqrt(yy)
+
+ return eerr
+
+ [p_zl, _] = leastsq(zl2, p0, args=(y, x), maxfev=2000)
+ if verbose:
+ print('Fit of a Product of two Lorentzian')
+ print('Positions: ', p_zl[2], p_zl[5], 'Distance: ', p_zl[2] - p_zl[5])
+ print('Width: ', p_zl[0], p_zl[3])
+ print('Areas: ', p_zl[1], p_zl[4])
+ err = (y - zl_func(p_zl, x)) / np.sqrt(y)
+ print(f'Goodness of Fit: {sum(err ** 2) / len(y) / sum(y) * 1e2:.5}%')
+
+ z_loss = zl_func(p_zl, energy_scale)
+
+ return z_loss, p_zl
+
+
+[docs]def get_energy_shifts(spectrum_image, energy_scale=None, zero_loss_fit_width=0.3):
+ """ get shift of spectrum from zero-loss peak position
+ better to use get resolution_functions
+ """
+ resolution_functions = get_resolution_functions(spectrum_image, energy_scale=energy_scale, zero_loss_fit_width=zero_loss_fit_width)
+ return resolution_functions.metadata['low_loss']['shifts'], resolution_functions.metadata['low_loss']['widths']
+
+[docs]def get_resolution_functions(spectrum_image, energy_scale=None, zero_loss_fit_width=0.3):
+ """get resolution_function and shift of spectra form zero-loss peak position"""
+ if isinstance(spectrum_image, sidpy.Dataset):
+ energy_dimension = spectrum_image.get_dimensions_by_type('spectral')
+ if len(energy_dimension) != 1:
+ raise TypeError('Dataset needs to have exactly one spectral dimension to analyze zero-loss peak')
+ energy_dimension = spectrum_image.get_dimension_by_number(energy_dimension)[0]
+ energy_scale = energy_dimension.values
+ spatial_dimension = spectrum_image.get_dimensions_by_type('spatial')
+ if len(spatial_dimension) == 0:
+ fwhm, delta_e = fix_energy_scale(spectrum_image)
+ z_loss, p_zl = resolution_function(energy_scale - delta_e, spectrum_image, zero_loss_fit_width)
+ fwhm2, delta_e2 = fix_energy_scale(z_loss, energy_scale - delta_e)
+ return delta_e + delta_e2, fwhm2
+ elif len(spatial_dimension) != 2:
+ return
+ shifts = np.zeros(spectrum_image.shape[0:2])
+ widths = np.zeros(spectrum_image.shape[0:2])
+ resolution_functions = spectrum_image.copy()
+ for x in range(spectrum_image.shape[0]):
+ for y in range(spectrum_image.shape[1]):
+ spectrum = np.array(spectrum_image[x, y])
+ fwhm, delta_e = fix_energy_scale(spectrum, energy_scale)
+ z_loss, p_zl = resolution_function(energy_scale - delta_e, spectrum, zero_loss_fit_width)
+ resolution_functions[x, y] = z_loss
+ fwhm2, delta_e2 = fix_energy_scale(z_loss, energy_scale - delta_e)
+ shifts[x, y] = delta_e + delta_e2
+ widths[x,y] = fwhm2
+
+ resolution_functions.metadata['low_loss'] = {'shifts': shifts,
+ 'widths': widths}
+ return resolution_functions
+
+
+[docs]def shift_on_same_scale(spectrum_image, shifts=None, energy_scale=None, master_energy_scale=None):
+ """shift spectrum in energy"""
+ if isinstance(spectrum_image, sidpy.Dataset):
+ if shifts is None:
+ if 'low_loss' in spectrum_image.metadata:
+ if 'shifts' in spectrum_image.metadata['low_loss']:
+ shifts = spectrum_image.metadata['low_loss']['shifts']
+ else:
+ resolution_functions = get_resolution_functions(spectrum_image)
+ shifts = resolution_functions.metadata['low_loss']['shifts']
+ energy_dimension = spectrum_image.get_dimensions_by_type('spectral')
+ if len(energy_dimension) != 1:
+ raise TypeError('Dataset needs to have exactly one spectral dimension to analyze zero-loss peak')
+ energy_dimension = spectrum_image.get_dimension_by_number(energy_dimension)[0]
+ energy_scale = energy_dimension.values
+ master_energy_scale = energy_scale.copy()
+
+ new_si = spectrum_image.copy()
+ new_si *= 0.0
+ for x in range(spectrum_image.shape[0]):
+ for y in range(spectrum_image.shape[1]):
+ tck = interpolate.splrep(np.array(energy_scale - shifts[x, y]), np.array(spectrum_image[x, y]), k=1, s=0)
+ new_si[x, y, :] = interpolate.splev(master_energy_scale, tck, der=0)
+ return new_si
+
+
+[docs]def get_wave_length(e0):
+ """get deBroglie wavelength of electron accelerated by energy (in eV) e0"""
+
+ ev = constants.e * e0
+ return constants.h / np.sqrt(2 * constants.m_e * ev * (1 + ev / (2 * constants.m_e * constants.c ** 2)))
+
+
+[docs]def drude(peak_position, peak_width, gamma, energy_scale):
+ """dielectric function according to Drude theory"""
+
+ eps = 1 - (peak_position ** 2 - peak_width * energy_scale * 1j) / (energy_scale ** 2 + 2 * energy_scale * gamma * 1j) # Mod drude term
+ return eps
+
+
+[docs]def drude_lorentz(eps_inf, leng, ep, eb, gamma, e, amplitude):
+ """dielectric function according to Drude-Lorentz theory"""
+
+ eps = eps_inf
+ for i in range(leng):
+ eps = eps + amplitude[i] * (1 / (e + ep[i] + gamma[i] * 1j) - 1 / (e - ep[i] + gamma[i] * 1j))
+ return eps
+
+
+[docs]def plot_dispersion(plotdata, units, a_data, e_data, title, max_p, ee, ef=4., ep=16.8, es=0, ibt=[]):
+ """Plot loss function """
+
+ [x, y] = np.meshgrid(e_data + 1e-12, a_data[1024:2048] * 1000)
+
+ z = plotdata
+ lev = np.array([0.01, 0.05, 0.1, 0.25, 0.5, 1, 2, 3, 4, 4.9]) * max_p / 5
+
+ wavelength = get_wave_length(ee)
+ q = a_data[1024:2048] / (wavelength * 1e9) # in [1/nm]
+ scale = np.array([0, a_data[-1], e_data[0], e_data[-1]])
+ ev2hertz = constants.value('electron volt-hertz relationship')
+
+ if units[0] == 'mrad':
+ units[0] = 'scattering angle [mrad]'
+ scale[1] = scale[1] * 1000.
+ light_line = constants.c * a_data # for mrad
+ elif units[0] == '1/nm':
+ units[0] = 'scattering vector [1/nm]'
+ scale[1] = scale[1] / (wavelength * 1e9)
+ light_line = 1 / (constants.c / ev2hertz) * 1e-9
+
+ if units[1] == 'eV':
+ units[1] = 'energy loss [eV]'
+
+ if units[2] == 'ppm':
+ units[2] = 'probability [ppm]'
+ if units[2] == '1/eV':
+ units[2] = 'probability [eV$^{-1}$ srad$^{-1}$]'
+
+ alpha = 3. / 5. * ef / ep
+
+ ax2 = plt.gca()
+ fig2 = plt.gcf()
+ im = ax2.imshow(z.T, clim=(0, max_p), origin='lower', aspect='auto', extent=scale)
+ co = ax2.contour(y, x, z, levels=lev, colors='k', origin='lower')
+ # ,extent=(-ang*1000.,ang*1000.,e_data[0],e_data[-1]))#, vmin = p_vol.min(), vmax = 1000)
+
+ fig2.colorbar(im, ax=ax2, label=units[2])
+
+ ax2.plot(a_data, light_line, c='r', label='light line')
+ # ax2.plot(e_data*light_line*np.sqrt(np.real(eps_data)),e_data, color='steelblue',
+ # label='$\omega = c q \sqrt{\epsilon_2}$')
+
+ # ax2.plot(q, Ep_disp, c='r')
+ ax2.plot([11.5 * light_line, 0.12], [11.5, 11.5], c='r')
+
+ ax2.text(.05, 11.7, 'surface plasmon', color='r')
+ ax2.plot([0.0, 0.12], [16.8, 16.8], c='r')
+ ax2.text(.05, 17, 'volume plasmon', color='r')
+ ax2.set_xlim(0, scale[1])
+ ax2.set_ylim(0, 20)
+ # Interband transitions
+ ax2.plot([0.0, 0.25], [4.2, 4.2], c='g', label='interband transitions')
+ ax2.plot([0.0, 0.25], [5.2, 5.2], c='g')
+ ax2.set_ylabel(units[1])
+ ax2.set_xlabel(units[0])
+ ax2.legend(loc='lower right')
+
+
+[docs]def zl_func(p, x):
+ """zero-loss peak function"""
+
+ p[0] = abs(p[0])
+
+ gauss1 = np.zeros(len(x))
+ gauss2 = np.zeros(len(x))
+ lorentz3 = np.zeros(len(x))
+ lorentz = ((0.5 * p[0] * p[1] / 3.14) / ((x - p[2]) ** 2 + ((p[0] / 2) ** 2)))
+ lorentz2 = ((0.5 * p[3] * p[4] / 3.14) / ((x - (p[5])) ** 2 + ((p[3] / 2) ** 2)))
+ if len(p) > 6:
+ lorentz3 = (0.5 * p[6] * p[7] / 3.14) / ((x - p[8]) ** 2 + (p[6] / 2) ** 2)
+ gauss2 = p[10] * np.exp(-(x - p[11]) ** 2 / (2.0 * (p[9] / 2.3548) ** 2))
+ # ((0.5 * p[9]* p[10]/3.14)/((x- (p[11]))**2+(( p[9]/2)**2)))
+ y = (lorentz * lorentz2) + gauss1 + gauss2 + lorentz3
+
+ return y
+
+
+[docs]def drude2(tags, e, p):
+ """dielectric function according to Drude theory for fitting"""
+
+ return drude(e, p[0], p[1], p[2], p[3])
+
+
+[docs]def xsec_xrpa(energy_scale, e0, z, beta, shift=0):
+ """ Calculate momentum-integrated cross-section for EELS from X-ray photo-absorption cross-sections.
+
+ X-ray photo-absorption cross-sections from NIST.
+ Momentum-integrated cross-section for EELS according to Egerton Ultramicroscopy 50 (1993) 13-28 equation (4)
+
+ Parameters
+ ----------
+ energy_scale: numpy array
+ energy scale of spectrum to be analyzed
+ e0: float
+ acceleration voltage in keV
+ z: int
+ atomic number of element
+ beta: float
+ effective collection angle in mrad
+ shift: float
+ chemical shift of edge in eV
+ """
+ beta = beta * 0.001 # collection half angle theta [rad]
+ # theta_max = self.parent.spec[0].convAngle * 0.001 # collection half angle theta [rad]
+ dispersion = energy_scale[1] - energy_scale[0]
+
+ x_sections = get_x_sections(z)
+ enexs = x_sections['ene']
+ datxs = x_sections['dat']
+
+ # enexs = enexs[:len(datxs)]
+
+ #####
+ # Cross Section according to Egerton Ultramicroscopy 50 (1993) 13-28 equation (4)
+ #####
+
+ # Relativistic correction factors
+ t = 511060.0 * (1.0 - 1.0 / (1.0 + e0 / 511.06) ** 2) / 2.0
+ gamma = 1 + e0 / 511.06
+ a = 6.5 # e-14 *10**14
+ b = beta
+
+ theta_e = enexs / (2 * gamma * t)
+
+ g = 2 * np.log(gamma) - np.log((b ** 2 + theta_e ** 2) / (b ** 2 + theta_e ** 2 / gamma ** 2)) - (
+ gamma - 1) * b ** 2 / (b ** 2 + theta_e ** 2 / gamma ** 2)
+ datxs = datxs * (a / enexs / t) * (np.log(1 + b ** 2 / theta_e ** 2) + g) / 1e8
+
+ datxs = datxs * dispersion # from per eV to per dispersion
+ coeff = splrep(enexs, datxs, s=0) # now in areal density atoms / m^2
+ xsec = np.zeros(len(energy_scale))
+ # shift = 0# int(ek -onsetXRPS)#/dispersion
+ lin = interp1d(enexs, datxs, kind='linear') # Linear instead of spline interpolation to avoid oscillations.
+ if energy_scale[0] < enexs[0]:
+ start = np.searchsorted(energy_scale, enexs[0])+1
+ else:
+ start = 0
+ xsec[start:] = lin(energy_scale[start:] - shift)
+
+ return xsec
+
+
+[docs]def drude_simulation(dset, e, ep, ew, tnm, eb):
+ """probabilities of dielectric function eps relative to zero-loss integral (i0 = 1)
+
+ Gives probabilities of dielectric function eps relative to zero-loss integral (i0 = 1) per eV
+ Details in R.F.Egerton: EELS in the Electron Microscope, 3rd edition, Springer 2011
+
+ # function drude(ep,ew,eb,epc,e0,beta,nn,tnm)
+ # Given the plasmon energy (ep), plasmon fwhm (ew) and binding energy(eb),
+ # this program generates:
+ # EPS1, EPS2 from modified Eq. (3.40), ELF=Im(-1/EPS) from Eq. (3.42),
+ # single scattering from Eq. (4.26) and SRFINT from Eq. (4.31)
+ # The output is e, ssd into the file drude.ssd (for use in Flog etc.)
+ # and e,eps1 ,eps2 into drude.eps (for use in Kroeger etc.)
+ # Gives probabilities relative to zero-loss integral (i0 = 1) per eV
+ # Details in R.F.Egerton: EELS in the Electron Microscope, 3rd edition, Springer 2011
+ # Version 10.11.26
+
+
+ b.7 drude Simulation of a Low-Loss Spectrum
+ The program DRUDE calculates a single-scattering plasmon-loss spectrum for
+ a specimen of a given thickness tnm (in nm), recorded with electrons of a
+ specified incident energy e0 by a spectrometer that accepts scattering up to a
+ specified collection semi-angle beta. It is based on the extended drude model
+ (Section 3.3.2), with a volume energy-loss function elf in accord with Eq. (3.64) and
+ a surface-scattering energy-loss function srelf as in Eq. (4.31). Retardation effects
+ and coupling between the two surface modes are not included. The surface term can
+ be made negligible by entering a large specimen thickness (tnm > 1000).
+ Surface intensity srfint and volume intensity volint are calculated from
+ Eqs. (4.31) and (4.26), respectively. The total spectral intensity ssd is written to
+ the file DRUDE.SSD, which can be used as input for KRAKRO. These intensities are
+ all divided by i0, to give relative probabilities (per eV). The real and imaginary parts
+ of the dielectric function are written to DRUDE.EPS and can be used for comparison
+ with the results of Kramers–Kronig analysis (KRAKRO.DAT).
+ Written output includes the surface-loss probability Ps, obtained by integrating
+ srfint (a value that relates to two surfaces but includes the negative begrenzungs
+ term), for comparison with the analytical integration represented by Eq. (3.77). The
+ volume-loss probability p_v is obtained by integrating volint and is used to calculate
+ the volume plasmon mean free path (lam = tnm/p_v). The latter is listed and
+ compared with the MFP obtained from Eq. (3.44), which represents analytical integration
+ assuming a zero-width plasmon peak. The total probability (Pt = p_v+Ps) is
+ calculated and used to evaluate the thickness (lam.Pt) that would be given by the formula
+ t/λ = ln(It/i0), ignoring the surface-loss probability. Note that p_v will exceed
+ 1 for thicker specimens (t/λ > 1), since it represents the probability of plasmon
+ scattering relative to that of no inelastic scattering.
+ The command-line usage is drude(ep,ew,eb,epc,beta,e0,tnm,nn), where ep is the
+ plasmon energy, ew the plasmon width, eb the binding energy of the electrons (0 for
+ a metal), and nn is the number of channels in the output spectrum. An example of
+ the output is shown in Fig. b.1a,b.
+
+ """
+
+ epc = dset.energy_scale[1] - dset.energy_scale[0] # input('ev per channel : ');
+
+ b = dset.metadata['collection_angle']/ 1000. # rad
+ epc = dset.energy_scale[1] - dset.energy_scale[0] # input('ev per channel : ');
+ e0 = dset.metadata['acceleration_voltage'] / 1000. # input('incident energy e0(kev) : ');
+
+ # effective kinetic energy: T = m_o v^2/2,
+ t = 1000.0 * e0 * (1. + e0 / 1022.12) / (1.0 + e0 / 511.06) ** 2 # eV # equ.5.2a or Appendix E p 427
+
+ # 2 gamma T
+ tgt = 1000 * e0 * (1022.12 + e0) / (511.06 + e0) # eV Appendix E p 427
+
+ rk0 = 2590 * (1.0 + e0 / 511.06) * np.sqrt(2.0 * t / 511060)
+
+ os = e[0]
+ ew_mod = eb
+ tags = dset.metadata
+
+ eps = 1 - (ep ** 2 - ew_mod * e * 1j) / (e ** 2 + 2 * e * ew * 1j) # Mod drude term
+
+ eps[np.nonzero(eps == 0.0)] = 1e-19
+ elf = np.imag(-1 / eps)
+
+ the = e / tgt # varies with energy loss! # Appendix E p 427
+ # srfelf = 4..*eps2./((1+eps1).^2+eps2.^2) - elf; %equivalent
+ srfelf = np.imag(-4. / (1.0 + eps)) - elf # for 2 surfaces
+ angdep = np.arctan(b / the) / the - b / (b * b + the * the)
+ srfint = angdep * srfelf / (3.1416 * 0.05292 * rk0 * t) # probability per eV
+ anglog = np.log(1.0 + b * b / the / the)
+ i0 = dset.sum() # *tags['counts2e']
+
+
+ # 2 * t = m_0 v**2 !!! a_0 = 0.05292 nm
+ volint = abs(tnm / (np.pi * 0.05292 * t * 2.0) * elf * anglog) # S equ 4.26% probability per eV
+ volint = volint * i0 / epc # S probability per channel
+ ssd = volint # + srfint;
+
+ if e[0] < -1.0:
+ xs = int(abs(-e[0] / epc))
+
+ ssd[0:xs] = 0.0
+ volint[0:xs] = 0.0
+ srfint[0:xs] = 0.0
+
+ # if os <0:
+ p_s = np.trapz(e, srfint) # 2 surfaces but includes negative Begrenzung contribution.
+ p_v = abs(np.trapz(e, abs(volint / tags['spec'].sum()))) # integrated volume probability
+ p_v = (volint / i0).sum() # our data have he same epc and the trapez formula does not include
+ lam = tnm / p_v # does NOT depend on free-electron approximation (no damping).
+ lamfe = 4.0 * 0.05292 * t / ep / np.log(1 + (b * tgt / ep) ** 2) # Eq.(3.44) approximation
+
+ tags['eps'] = eps
+ tags['lam'] = lam
+ tags['lamfe'] = lamfe
+ tags['p_v'] = p_v
+
+ return ssd # /np.pi
+
+
+[docs]def effective_collection_angle(energy_scale, alpha, beta, beam_kv):
+ """Calculates the effective collection angle in mrad:
+
+ Translate from original Fortran program
+ Calculates the effective collection angle in mrad:
+ Parameter
+ ---------
+ energy_scale: numpy array
+ first and last energy loss of spectrum in eV
+ alpha: float
+ convergence angle in mrad
+ beta: float
+ collection angle in mrad
+ beamKV: float
+ acceleration voltage in V
+
+ Returns
+ -------
+ eff_beta: float
+ effective collection angle in mrad
+
+ # function y = effbeta(ene, alpha, beta, beam_kv)
+ #
+ # This program computes etha(alpha,beta), that is the collection
+ # efficiency associated to the following geometry :
+ #
+ # alpha = half angle of illumination (0 -> pi/2)
+ # beta = half angle of collection (0 -> pi/2)
+ # (pi/2 = 1570.795 mrad)
+ #
+ # A constant angular distribution of incident electrons is assumed
+ # for any incident angle (-alpha,alpha). These electrons imping the
+ # target and a single energy-loss event occurs, with a characteristic
+ # angle theta-e (relativistic). The angular distribution of the
+ # electrons after the target is analytically derived.
+ # This program integrates this distribution from theta=0 up to
+ # theta=beta with an adjustable angular step.
+ # This program also computes beta* which is the theoretical
+ # collection angle which would give the same value of etha(alpha,beta)
+ # with a parallel incident beam.
+ #
+ # subroutines and function subprograms required
+ # ---------------------------------------------
+ # none
+ #
+ # comments
+ # --------
+ #
+ # The following parameters are asked as input :
+ # accelerating voltage (kV), energy loss range (eV) for the study,
+ # energy loss step (eV) in this range, alpha (mrad), beta (mrad).
+ # The program returns for each energy loss step :
+ # alpha (mrad), beta (mrad), theta-e (relativistic) (mrad),
+ # energy loss (eV), etha (#), beta * (mrad)
+ #
+ # author :
+ # --------
+ # Pierre TREBBIA
+ # US 41 : "Microscopie Electronique Analytique Quantitative"
+ # Laboratoire de Physique des Solides, Bat. 510
+ # Universite Paris-Sud, F91405 ORSAY Cedex
+ # Phone : (33-1) 69 41 53 68
+ #
+ """
+ if beam_kv == 0:
+ beam_kv = 100.0
+
+ if alpha == 0:
+ return beta
+
+ if beta == 0:
+ return alpha
+
+ z1 = beam_kv # eV
+ z2 = energy_scale[0]
+ z3 = energy_scale[-1]
+ z4 = 100.0
+
+ z5 = alpha * 0.001 # rad
+ z6 = beta * 0.001 # rad
+ z7 = 500.0 # number of integration steps to be modified at will
+
+ # main loop on energy loss
+ #
+ for zx in range(int(z2), int(z3), int(z4)): # ! zx = current energy loss
+ eta = 0.0
+ x0 = float(zx) * (z1 + 511060.) / (z1 * (z1 + 1022120.)) # x0 = relativistic theta-e
+ x1 = np.pi / (2. * x0)
+ x2 = x0 * x0 + z5 * z5
+ x3 = z5 / x0 * z5 / x0
+ x4 = 0.1 * np.sqrt(x2)
+ dtheta = (z6 - x4) / z7
+ #
+ # calculation of the analytical expression
+ #
+ for zi in range(1, int(z7)):
+ theta = x4 + dtheta * float(zi)
+ x5 = theta * theta
+ x6 = 4. * x5 * x0 * x0
+ x7 = x2 - x5
+ x8 = np.sqrt(x7 * x7 + x6)
+ x9 = (x8 + x7) / (2. * x0 * x0)
+ x10 = 2. * theta * dtheta * np.log(x9)
+ eta = eta + x10
+
+ eta = eta + x2 / 100. * np.log(1. + x3) # addition of the central contribution
+ x4 = z5 * z5 * np.log(1. + x1 * x1) # normalisation
+ eta = eta / x4
+ #
+ # correction by geometrical factor (beta/alpha)**2
+ #
+ if z6 < z5:
+ x5 = z5 / z6
+ eta = eta * x5 * x5
+
+ etha2 = eta * 100.
+ #
+ # calculation of beta *
+ #
+ x6 = np.power((1. + x1 * x1), eta)
+ x7 = x0 * np.sqrt(x6 - 1.)
+ beta = x7 * 1000. # in mrad
+
+ return beta
+
+
+[docs]def kroeger_core(e_data, a_data, eps_data, ee, thick, relativistic=True):
+ """This function calculates the differential scattering probability
+
+ .. math::
+ \\frac{d^2P}{d \\Omega d_e}
+ of the low-loss region for total loss and volume plasmon loss
+
+ Args:
+ e_data (array): energy scale [eV]
+ a_data (array): angle or momentum range [rad]
+ eps_data (array): dielectric function data
+ ee (float): acceleration voltage [keV]
+ thick (float): thickness in m
+ relativistic: boolean include relativistic corrections
+
+ Returns:
+ P (numpy array 2d): total loss probability
+ p_vol (numpy array 2d): volume loss probability
+ """
+
+ # $d^2P/(dEd\Omega) = \frac{1}{\pi^2 a_0 m_0 v^2} \Im \left[ \frac{t\mu^2}{\varepsilon \phi^2 } \right] $ \
+
+ # ee = 200 #keV
+ # thick = 32.0# nm
+ thick = thick * 1e-9 # input thickness now in m
+ # Define constants
+ # ec = 14.4;
+ m_0 = constants.value(u'electron mass') # REST electron mass in kg
+ # h = constants.Planck # Planck's constant
+ hbar = constants.hbar
+
+ c = constants.speed_of_light # speed of light m/s
+ bohr = constants.value(u'Bohr radius') # Bohr radius in meters
+ e = constants.value(u'elementary charge') # electron charge in Coulomb
+ print('hbar =', hbar, ' [Js] =', hbar / e, '[ eV s]')
+
+ # Calculate fixed terms of equation
+ va = 1 - (511. / (511. + ee)) ** 2 # ee is incident energy in keV
+ v = c * np.sqrt(va)
+ beta = v / c # non-relativistic for =1
+
+ if relativistic:
+ gamma = 1. / np.sqrt(1 - beta ** 2)
+ else:
+ gamma = 1 # set = 1 to correspond to E+B & Siegle
+
+ momentum = m_0 * v * gamma # used for xya, E&B have no gamma
+
+ # ##### Define mapped variables
+
+ # Define independent variables E, theta
+ a_data = np.array(a_data)
+ e_data = np.array(e_data)
+ [energy, theta] = np.meshgrid(e_data + 1e-12, a_data)
+ # Define CONJUGATE dielectric function variable eps
+ [eps, _] = np.meshgrid(np.conj(eps_data), a_data)
+
+ # ##### Calculate lambda in equation EB 2.3
+ theta2 = theta ** 2 + 1e-15
+ theta_e = energy * e / momentum / v
+ theta_e2 = theta_e ** 2
+
+ lambda2 = theta2 - eps * theta_e2 * beta ** 2 # Eq 2.3
+
+ lambd = np.sqrt(lambda2)
+ if (np.real(lambd) < 0).any():
+ print(' error negative lambda')
+
+ # ##### Calculate lambda0 in equation EB 2.4
+ # According to Kröger real(lambda0) is defined as positive!
+
+ phi2 = lambda2 + theta_e2 # Eq. 2.2
+ lambda02 = theta2 - theta_e2 * beta ** 2 # eta=1 Eq 2.4
+ lambda02[lambda02 < 0] = 0
+ lambda0 = np.sqrt(lambda02)
+ if not (np.real(lambda0) >= 0).any():
+ print(' error negative lambda0')
+
+ de = thick * energy * e / 2.0 / hbar / v # Eq 2.5
+
+ xya = lambd * de / theta_e # used in Eqs 2.6, 2.7, 4.4
+
+ lplus = lambda0 * eps + lambd * np.tanh(xya) # eta=1 %Eq 2.6
+ lminus = lambda0 * eps + lambd / np.tanh(xya) # eta=1 %Eq 2.7
+
+ mue2 = 1 - (eps * beta ** 2) # Eq. 4.5
+ phi20 = lambda02 + theta_e2 # Eq 4.6
+ phi201 = theta2 + theta_e2 * (1 - (eps + 1) * beta ** 2) # eta=1, eps-1 in E+B Eq.(4.7)
+
+ # Eq 4.2
+ a1 = phi201 ** 2 / eps
+ a2 = np.sin(de) ** 2 / lplus + np.cos(de) ** 2 / lminus
+ a = a1 * a2
+
+ # Eq 4.3
+ b1 = beta ** 2 * lambda0 * theta_e * phi201
+ b2 = (1. / lplus - 1. / lminus) * np.sin(2. * de)
+ b = b1 * b2
+
+ # Eq 4.4
+ c1 = -beta ** 4 * lambda0 * lambd * theta_e2
+ c2 = np.cos(de) ** 2 * np.tanh(xya) / lplus
+ c3 = np.sin(de) ** 2 / np.tanh(xya) / lminus
+ c = c1 * (c2 + c3)
+
+ # Put all the pieces together...
+ p_coef = e / (bohr * np.pi ** 2 * m_0 * v ** 2)
+
+ p_v = thick * mue2 / eps / phi2
+
+ p_s1 = 2. * theta2 * (eps - 1) ** 2 / phi20 ** 2 / phi2 ** 2 # ASSUMES eta=1
+ p_s2 = hbar / momentum
+ p_s3 = a + b + c
+
+ p_s = p_s1 * p_s2 * p_s3
+
+ # print(p_v.min(),p_v.max(),p_s.min(),p_s.max())
+ # Calculate P and p_vol (volume only)
+ dtheta = a_data[1] - a_data[0]
+ scale = np.sin(np.abs(theta)) * dtheta * 2 * np.pi
+
+ p = p_coef * np.imag(p_v - p_s) # Eq 4.1
+ p_vol = p_coef * np.imag(p_v) * scale
+
+ # lplus_min = e_data[np.argmin(np.real(lplus), axis=1)]
+ # lminus_min = e_data[np.argmin(np.imag(lminus), axis=1)]
+
+ p_simple = p_coef * np.imag(1 / eps) * thick / (
+ theta2 + theta_e2) * scale # Watch it eps is conjugated dielectric function
+
+ return p, p * scale * 1e2, p_vol * 1e2, p_simple * 1e2 # ,lplus_min,lminus_min
+
+
+[docs]def kroeger_core2(e_data, a_data, eps_data, acceleration_voltage_kev, thickness, relativistic=True):
+ """This function calculates the differential scattering probability
+
+ .. math::
+ \\frac{d^2P}{d \\Omega d_e}
+ of the low-loss region for total loss and volume plasmon loss
+
+ Args:
+ e_data (array): energy scale [eV]
+ a_data (array): angle or momentum range [rad]
+ eps_data (array) dielectric function
+ acceleration_voltage_kev (float): acceleration voltage [keV]
+ thickness (float): thickness in nm
+ relativistic (boolean): relativistic correction
+
+ Returns:
+ P (numpy array 2d): total loss probability
+ p_vol (numpy array 2d): volume loss probability
+
+ return P, P*scale*1e2,p_vol*1e2, p_simple*1e2
+ """
+
+ # $d^2P/(dEd\Omega) = \frac{1}{\pi^2 a_0 m_0 v^2} \Im \left[ \frac{t\mu^2}{\varepsilon \phi^2 } \right]
+ """
+ # Internally everything is calculated in si units
+ # acceleration_voltage_kev = 200 #keV
+ # thick = 32.0*10-9 # m
+
+ """
+ a_data = np.array(a_data)
+ e_data = np.array(e_data)
+ # adjust input to si units
+ wavelength = get_wave_length(acceleration_voltage_kev * 1e3) # in m
+ thickness = thickness * 1e-9 # input thickness now in m
+
+ # Define constants
+ # ec = 14.4;
+ m_0 = constants.value(u'electron mass') # REST electron mass in kg
+ # h = constants.Planck # Planck's constant
+ hbar = constants.hbar
+
+ c = constants.speed_of_light # speed of light m/s
+ bohr = constants.value(u'Bohr radius') # Bohr radius in meters
+ e = constants.value(u'elementary charge') # electron charge in Coulomb
+ # print('hbar =', hbar ,' [Js] =', hbar/e ,'[ eV s]')
+
+ # Calculate fixed terms of equation
+ va = 1 - (511. / (511. + acceleration_voltage_kev)) ** 2 # acceleration_voltage_kev is incident energy in keV
+ v = c * np.sqrt(va)
+
+ if relativistic:
+ beta = v / c # non-relativistic for =1
+ gamma = 1. / np.sqrt(1 - beta ** 2)
+ else:
+ beta = 1
+ gamma = 1 # set = 1 to correspond to E+B & Siegle
+
+ momentum = m_0 * v * gamma # used for xya, E&B have no gamma
+
+ # ##### Define mapped variables
+
+ # Define independent variables E, theta
+ [energy, theta] = np.meshgrid(e_data + 1e-12, a_data)
+ # Define CONJUGATE dielectric function variable eps
+ [eps, _] = np.meshgrid(np.conj(eps_data), a_data)
+
+ # ##### Calculate lambda in equation EB 2.3
+ theta2 = theta ** 2 + 1e-15
+
+ theta_e = energy * e / momentum / v # critical angle
+
+ lambda2 = theta2 - eps * theta_e ** 2 * beta ** 2 # Eq 2.3
+
+ lambd = np.sqrt(lambda2)
+ if (np.real(lambd) < 0).any():
+ print(' error negative lambda')
+
+ # ##### Calculate lambda0 in equation EB 2.4
+ # According to Kröger real(lambda0) is defined as positive!
+
+ phi2 = lambda2 + theta_e ** 2 # Eq. 2.2
+ lambda02 = theta2 - theta_e ** 2 * beta ** 2 # eta=1 Eq 2.4
+ lambda02[lambda02 < 0] = 0
+ lambda0 = np.sqrt(lambda02)
+ if not (np.real(lambda0) >= 0).any():
+ print(' error negative lambda0')
+
+ de = thickness * energy * e / (2.0 * hbar * v) # Eq 2.5
+ xya = lambd * de / theta_e # used in Eqs 2.6, 2.7, 4.4
+
+ lplus = lambda0 * eps + lambd * np.tanh(xya) # eta=1 %Eq 2.6
+ lminus = lambda0 * eps + lambd / np.tanh(xya) # eta=1 %Eq 2.7
+
+ mue2 = 1 - (eps * beta ** 2) # Eq. 4.5
+ phi20 = lambda02 + theta_e ** 2 # Eq 4.6
+ phi201 = theta2 + theta_e ** 2 * (1 - (eps + 1) * beta ** 2) # eta=1, eps-1 in E+b Eq.(4.7)
+
+ # Eq 4.2
+ a1 = phi201 ** 2 / eps
+ a2 = np.sin(de) ** 2 / lplus + np.cos(de) ** 2 / lminus
+ a = a1 * a2
+
+ # Eq 4.3
+ b1 = beta ** 2 * lambda0 * theta_e * phi201
+ b2 = (1. / lplus - 1. / lminus) * np.sin(2. * de)
+ b = b1 * b2
+
+ # Eq 4.4
+ c1 = -beta ** 4 * lambda0 * lambd * theta_e ** 2
+ c2 = np.cos(de) ** 2 * np.tanh(xya) / lplus
+ c3 = np.sin(de) ** 2 / np.tanh(xya) / lminus
+ c = c1 * (c2 + c3)
+
+ # Put all the pieces together...
+ p_coef = e / (bohr * np.pi ** 2 * m_0 * v ** 2)
+
+ p_v = thickness * mue2 / eps / phi2
+
+ p_s1 = 2. * theta2 * (eps - 1) ** 2 / phi20 ** 2 / phi2 ** 2 # ASSUMES eta=1
+ p_s2 = hbar / momentum
+ p_s3 = a + b + c
+
+ p_s = p_s1 * p_s2 * p_s3
+
+ # print(p_v.min(),p_v.max(),p_s.min(),p_s.max())
+ # Calculate P and p_vol (volume only)
+ dtheta = a_data[1] - a_data[0]
+ scale = np.sin(np.abs(theta)) * dtheta * 2 * np.pi
+
+ p = p_coef * np.imag(p_v - p_s) # Eq 4.1
+ p_vol = p_coef * np.imag(p_v) * scale
+
+ # lplus_min = e_data[np.argmin(np.real(lplus), axis=1)]
+ # lminus_min = e_data[np.argmin(np.imag(lminus), axis=1)]
+
+ p_simple = p_coef * np.imag(1 / eps) * thickness / (theta2 + theta_e ** 2) * scale
+ # Watch it: eps is conjugated dielectric function
+
+ return p, p * scale * 1e2, p_vol * 1e2, p_simple * 1e2 # ,lplus_min,lminus_min
+
+
+##########################
+# EELS Database
+##########################
+
+
+[docs]def read_msa(msa_string):
+ """read msa formated file"""
+ parameters = {}
+ y = []
+ x = []
+ # Read the keywords
+ data_section = False
+ msa_lines = msa_string.split('\n')
+
+ for line in msa_lines:
+ if data_section is False:
+ if len(line) > 0:
+ if line[0] == "#":
+ try:
+ key, value = line.split(': ')
+ value = value.strip()
+ except ValueError:
+ key = line
+ value = None
+ key = key.strip('#').strip()
+
+ if key != 'SPECTRUM':
+ parameters[key] = value
+ else:
+ data_section = True
+ else:
+ # Read the data
+
+ if len(line) > 0 and line[0] != "#" and line.strip():
+ if parameters['DATATYPE'] == 'XY':
+ xy = line.replace(',', ' ').strip().split()
+ y.append(float(xy[1]))
+ x.append(float(xy[0]))
+ elif parameters['DATATYPE'] == 'Y':
+ print('y')
+ data = [
+ float(i) for i in line.replace(',', ' ').strip().split()]
+ y.extend(data)
+ parameters['data'] = np.array(y)
+ if 'XPERCHAN' in parameters:
+ parameters['XPERCHAN'] = str(parameters['XPERCHAN']).split(' ')[0]
+ parameters['OFFSET'] = str(parameters['OFFSET']).split(' ')[0]
+ parameters['energy_scale'] = np.arange(len(y)) * float(parameters['XPERCHAN']) + float(parameters['OFFSET'])
+ return parameters
+
+
+[docs]def get_spectrum_eels_db(formula=None, edge=None, title=None, element=None):
+ """
+ get spectra from EELS database
+ chemical formula and edge is accepted.
+ Could expose more of the search parameters
+ """
+ valid_edges = ['K', 'L1', 'L2,3', 'M2,3', 'M4,5', 'N2,3', 'N4,5', 'O2,3', 'O4,5']
+ if edge is not None and edge not in valid_edges:
+ print('edge should be a in ', valid_edges)
+
+ spectrum_type = None
+ title = title
+ author = None
+ element = element
+ min_energy = None
+ max_energy = None
+ resolution = None
+ min_energy_compare = "gt"
+ max_energy_compare = "lt",
+ resolution_compare = "lt"
+ max_n = -1
+ monochromated = None
+ order = None
+ order_direction = "ASC"
+ verify_certificate = True
+ # Verify arguments
+
+ if spectrum_type is not None and spectrum_type not in {'coreloss', 'lowloss', 'zeroloss', 'xrayabs'}:
+ raise ValueError("spectrum_type must be one of \'coreloss\', \'lowloss\', "
+ "\'zeroloss\', \'xrayabs\'.")
+ # valid_edges = ['K', 'L1', 'L2,3', 'M2,3', 'M4,5', 'N2,3', 'N4,5', 'O2,3', 'O4,5']
+
+ params = {
+ "type": spectrum_type,
+ "title": title,
+ "author": author,
+ "edge": edge,
+ "min_energy": min_energy,
+ "max_energy": max_energy,
+ "resolution": resolution,
+ "resolution_compare": resolution_compare,
+ "monochromated": monochromated,
+ "formula": formula,
+ 'element': element,
+ "min_energy_compare": min_energy_compare,
+ "max_energy_compare": max_energy_compare,
+ "per_page": max_n,
+ "order": order,
+ "order_direction": order_direction,
+ }
+
+ request = requests.get('http://api.eelsdb.eu/spectra', params=params, verify=True)
+ # spectra = []
+ jsons = request.json()
+ if "message" in jsons:
+ # Invalid query, EELSdb raises error.
+ raise IOError(
+ "Please report the following error to the HyperSpy developers: "
+ "%s" % jsons["message"])
+ reference_spectra = {}
+ for json_spectrum in jsons:
+ download_link = json_spectrum['download_link']
+ # print(download_link)
+ msa_string = requests.get(download_link, verify=verify_certificate).text
+ # print(msa_string[:100])
+ parameters = read_msa(msa_string)
+ if 'XPERCHAN' in parameters:
+ reference_spectra[parameters['TITLE']] = parameters
+ print(parameters['TITLE'])
+ print(f'found {len(reference_spectra.keys())} spectra in EELS database)')
+
+ return reference_spectra
+
+"""file_tools: All tools to load and save data
+
+##################################
+
+ 2018 01 31 Included Nion Swift files to be opened
+ major revision 2020 09 to include sidpy and pyNSID data formats
+ 2022 change to ase format for structures: this changed the default unit of length to Angstrom!!!
+
+##################################
+"""
+
+import numpy as np
+import h5py
+import os
+import pickle
+
+# For structure files of various flavor for instance POSCAR and other theory packages
+import ase.io
+
+# =============================================
+# Include pycroscopy libraries #
+# =============================================
+import SciFiReaders
+import pyNSID
+import sidpy
+import ipywidgets as widgets
+from IPython.display import display
+
+# =============================================
+# Include pyTEMlib libraries #
+# =============================================
+import pyTEMlib.crystal_tools
+from pyTEMlib.config_dir import config_path
+from pyTEMlib.sidpy_tools import *
+
+from pyTEMlib.sidpy_tools import *
+
+Qt_available = True
+try:
+ from PyQt5 import QtCore, QtWidgets, QtGui
+except ModuleNotFoundError:
+ print('Qt dialogs are not available')
+ Qt_available = False
+
+Dimension = sidpy.Dimension
+
+get_slope = sidpy.base.num_utils.get_slope
+__version__ = '2022.3.3'
+
+
+[docs]class FileWidget(object):
+ """Widget to select directories or widgets from a list
+
+ Works in google colab.
+ The widget converts the name of the nion file to the one in Nion's swift software,
+ because it is otherwise incomprehensible
+
+ Attributes
+ ----------
+ dir_name: str
+ name of starting directory
+ extension: list of str
+ extensions of files to be listed in widget
+
+ Methods
+ -------
+ get_directory
+ set_options
+ get_file_name
+
+ Example
+ -------
+ >>from google.colab import drive
+ >>drive.mount("/content/drive")
+ >>file_list = pyTEMlib.file_tools.FileWidget()
+ next code cell:
+ >>dataset = pyTEMlib.file_tools.open_file(file_list.file_name)
+
+ """
+
+ def __init__(self, dir_name=None, extension=['*']):
+ self.save_path = False
+ self.dir_dictionary = {}
+ self.dir_list = ['.', '..']
+ self.display_list = ['.', '..']
+
+ self.dir_name = '.'
+ if dir_name is None:
+ self.dir_name = get_last_path()
+ self.save_path = True
+ elif os.path.isdir(dir_name):
+ self.dir_name = dir_name
+
+ self.get_directory(self.dir_name)
+ self.dir_list = ['.']
+ self.extensions = extension
+ self.file_name = ''
+ self.datasets ={}
+ self.dataset = None
+
+ self.select_files = widgets.Select(
+ options=self.dir_list,
+ value=self.dir_list[0],
+ description='Select file:',
+ disabled=False,
+ rows=10,
+ layout=widgets.Layout(width='70%')
+ )
+
+ select_button = widgets.Button(description='Select Main',
+ layout=widgets.Layout(width='auto', grid_area='header'),
+ style=widgets.ButtonStyle(button_color='lightblue'))
+
+ add_button = widgets.Button(description='Add',
+ layout=widgets.Layout(width='auto', grid_area='header'),
+ style=widgets.ButtonStyle(button_color='lightblue'))
+
+ self.path_choice = widgets.Dropdown(options=['None'],
+ value='None',
+ description='directory:',
+ disabled=False,
+ button_style='',
+ layout=widgets.Layout(width='90%'))
+ self.dataset_list = ['None']
+ self.loaded_datasets = widgets.Dropdown(options=self.dataset_list,
+ value=self.dataset_list[0],
+ description='loaded datasets:',
+ disabled=False,
+ button_style='')
+
+ self.set_options()
+ ui = widgets.VBox([self.path_choice, self.select_files, widgets.HBox([select_button, add_button, self.loaded_datasets])])
+ display(ui)
+
+ self.select_files.observe(self.get_file_name, names='value')
+ self.path_choice.observe(self.set_dir, names='value')
+
+ select_button.on_click(self.select_main)
+ add_button.on_click(self.add_dataset)
+ self.loaded_datasets.observe(self.selected_dataset)
+
+ def select_main(self, value=0):
+ self.datasets = {}
+ self.loaded_datasets.value = self.dataset_list[0]
+ self.dataset_list = []
+ #self.loaded_datasets.options = self.dataset_list
+
+ self.datasets = open_file(self.file_name)
+ self.dataset_list = []
+ for key in self.datasets.keys():
+ self.dataset_list.append(f'{key}: {self.datasets[key].title}')
+ self.loaded_datasets.options = self.dataset_list
+ self.loaded_datasets.value = self.dataset_list[0]
+ self.dataset = self.datasets[list(self.datasets.keys())[0]]
+ self.selected_dataset = self.dataset
+
+ def add_dataset(self, value=0):
+ key = add_dataset_from_file(self.datasets, self.file_name, 'Channel')
+ self.dataset_list.append(f'{key}: {self.datasets[key].title}')
+ self.loaded_datasets.options = self.dataset_list
+ self.loaded_datasets.value = self.dataset_list[-1]
+
+[docs] def get_directory(self, directory=None):
+ self.dir_name = directory
+ self.dir_dictionary = {}
+ self.dir_list = []
+ self.dir_list = ['.', '..'] + os.listdir(directory)
+
+ def set_dir(self, value=0):
+ self.dir_name = self.path_choice.value
+ self.select_files.index = 0
+ self.set_options()
+
+ def selected_dataset(self, value=0):
+
+ key = self.loaded_datasets.value.split(':')[0]
+ if key != 'None':
+ self.selected_dataset = self.datasets[key]
+
+[docs] def set_options(self):
+ self.dir_name = os.path.abspath(os.path.join(self.dir_name, self.dir_list[self.select_files.index]))
+ dir_list = os.listdir(self.dir_name)
+ file_dict = update_directory_list(self.dir_name)
+
+ sort = np.argsort(file_dict['directory_list'])
+ self.dir_list = ['.', '..']
+ self.display_list = ['.', '..']
+ for j in sort:
+ self.display_list.append(f" * {file_dict['directory_list'][j]}")
+ self.dir_list.append(file_dict['directory_list'][j])
+
+ sort = np.argsort(file_dict['display_file_list'])
+
+ for i, j in enumerate(sort):
+ if '--' in dir_list[j]:
+ self.display_list.append(f" {i:3} {file_dict['display_file_list'][j]}")
+ else:
+ self.display_list.append(f" {i:3} {file_dict['display_file_list'][j]}")
+ self.dir_list.append(file_dict['file_list'][j])
+
+ self.dir_label = os.path.split(self.dir_name)[-1] + ':'
+ self.select_files.options = self.display_list
+
+ path = self.dir_name
+ old_path = ' '
+ path_list = []
+ while path != old_path:
+ path_list.append(path)
+ old_path = path
+ path = os.path.split(path)[0]
+ self.path_choice.options = path_list
+ self.path_choice.value = path_list[0]
+
+[docs] def get_file_name(self, b):
+
+ if os.path.isdir(os.path.join(self.dir_name, self.dir_list[self.select_files.index])):
+ self.set_options()
+
+ elif os.path.isfile(os.path.join(self.dir_name, self.dir_list[self.select_files.index])):
+ self.file_name = os.path.join(self.dir_name, self.dir_list[self.select_files.index])
+
+
+[docs]class ChooseDataset(object):
+ """Widget to select dataset object """
+
+ def __init__(self, input_object, show_dialog=True):
+ self.datasets = None
+ if isinstance(input_object, sidpy.Dataset):
+ if isinstance(input_object.h5_dataset, h5py.Dataset):
+ self.current_channel = input_object.h5_dataset.parent
+ elif isinstance(input_object, h5py.Group):
+ self.current_channel = input_object
+ elif isinstance(input_object, h5py.Dataset):
+ self.current_channel = input_object.parent
+ elif isinstance(input_object, dict):
+ self.datasets = input_object
+ else:
+ raise ValueError('Need hdf5 group or sidpy Dataset to determine image choices')
+ self.dataset_names = []
+ self.dataset_list = []
+ self.dataset_type = None
+ self.dataset = None
+ if not isinstance(self.datasets, dict):
+ self.reader = SciFiReaders.NSIDReader(self.current_channel.file.filename)
+ else:
+ self.reader = None
+ self.get_dataset_list()
+ self.select_image = widgets.Dropdown(options=self.dataset_list,
+ value=self.dataset_list[0],
+ description='select dataset:',
+ disabled=False,
+ button_style='')
+ if show_dialog:
+ display(self.select_image)
+
+ self.select_image.observe(self.set_dataset, names='value')
+ self.set_dataset(0)
+ self.select_image.index = (len(self.dataset_names) - 1)
+
+[docs] def get_dataset_list(self):
+ """ Get by Log number sorted list of datasets"""
+ if not isinstance(self.datasets, dict):
+ dataset_list = self.reader.read()
+ self.datasets = {}
+ for dataset in dataset_list:
+ self.datasets[dataset.title] = dataset
+ order = []
+ keys = []
+ for title, dset in self.datasets.items():
+ if isinstance(dset, sidpy.Dataset):
+ if self.dataset_type is None or dset.data_type == self.data_type:
+ if 'Log' in title:
+ order.append(2)
+ else:
+ order.append(0)
+ keys.append(title)
+ for index in np.argsort(order):
+ self.dataset_names.append(keys[index])
+ self.dataset_list.append(keys[index] + ': ' + self.datasets[keys[index]].title)
+
+ def set_dataset(self, b):
+ index = self.select_image.index
+ self.key = self.dataset_names[index]
+ self.dataset = self.datasets[self.key]
+ self.dataset.title = self.dataset.title.split('/')[-1]
+ self.dataset.title = self.dataset.title.split('/')[-1]
+
+
+[docs]def add_to_dict(file_dict, name):
+ full_name = os.path.join(file_dict['directory'], name)
+ basename, extension = os.path.splitext(name)
+ size = os.path.getsize(full_name) * 2 ** -20
+ display_name = name
+ if len(extension) == 0:
+ display_file_list = f' {name} - {size:.1f} MB'
+ elif extension[0] == 'hf5':
+ if extension in ['.hf5']:
+ display_file_list = f" {name} - {size:.1f} MB"
+ elif extension in ['.h5', '.ndata']:
+ try:
+ reader = SciFiReaders.NionReader(full_name)
+ dataset_nion = reader.read()
+ display_name = dataset_nion.title
+ display_file_list = f" {display_name}{extension} - {size:.1f} MB"
+ except:
+ display_file_list = f" {name} - {size:.1f} MB"
+ else:
+ display_file_list = f' {name} - {size:.1f} MB'
+ file_dict[name] = {'display_string': display_file_list, 'basename': basename, 'extension': extension,
+ 'size': size, 'display_name': display_name}
+
+
+[docs]def update_directory_list(directory_name):
+ dir_list = os.listdir(directory_name)
+
+ if '.pyTEMlib.files.pkl' in dir_list:
+ with open(os.path.join(directory_name, '.pyTEMlib.files.pkl'), 'rb') as f:
+ file_dict = pickle.load(f)
+ if directory_name != file_dict['directory']:
+ print('directory moved since last time read')
+ file_dict['directory'] = directory_name
+ dir_list.remove('.pyTEMlib.files.pkl')
+ else:
+ file_dict = {'directory': directory_name}
+
+ # add new files
+ file_dict['file_list'] = []
+ file_dict['display_file_list'] = []
+ file_dict['directory_list'] = []
+
+ for name in dir_list:
+ if os.path.isfile(os.path.join(file_dict['directory'], name)):
+ if name not in file_dict:
+ add_to_dict(file_dict, name)
+ file_dict['file_list'].append(name)
+ file_dict['display_file_list'].append(file_dict[name]['display_string'])
+ else:
+ file_dict['directory_list'].append(name)
+ remove_item = []
+
+ # delete items of deleted files
+ save_pickle = False
+
+ for name in file_dict.keys():
+ if name not in dir_list and name not in ['directory', 'file_list', 'directory_list', 'display_file_list']:
+ remove_item.append(name)
+ else:
+ if 'extension' in file_dict[name]:
+ save_pickle = True
+ for item in remove_item:
+ file_dict.pop(item)
+
+ if save_pickle:
+ with open(os.path.join(file_dict['directory'], '.pyTEMlib.files.pkl'), 'wb') as f:
+ pickle.dump(file_dict, f)
+ return file_dict
+
+
+####
+# General Open and Save Methods
+####
+
+[docs]def get_last_path():
+ """Returns the path of the file last opened"""
+ try:
+ fp = open(config_path + '\\path.txt', 'r')
+ path = fp.read()
+ fp.close()
+ except IOError:
+ path = ''
+
+ if len(path) < 2:
+ path = '.'
+ return path
+
+
+[docs]def save_path(filename):
+ """Save path of last opened file"""
+
+ if len(filename) > 1:
+ fp = open(config_path + '\\path.txt', 'w')
+ path, fname = os.path.split(filename)
+ fp.write(path)
+ fp.close()
+ else:
+ path = '.'
+ return path
+
+
+if Qt_available:
+ def get_qt_app():
+ """
+ will start QT Application if not running yet
+
+ :returns: QApplication
+
+ """
+
+ # start qt event loop
+ _instance = QtWidgets.QApplication.instance()
+ if not _instance:
+ # print('not_instance')
+ _instance = QtWidgets.QApplication([])
+
+ return _instance
+
+
+[docs]def open_file_dialog_qt(file_types=None): # , multiple_files=False):
+ """Opens a File dialog which is used in open_file() function
+
+ This function uses pyQt5.
+ The app of the Gui has to be running for QT. Tkinter does not run on Macs at this point in time.
+ In jupyter notebooks use %gui Qt early in the notebook.
+
+ The file looks first for a path.txt file for the last directory you used.
+
+ Parameters
+ ----------
+ file_types : string
+ file type filter in the form of '*.hf5'
+
+
+ Returns
+ -------
+ filename : string
+ full filename with absolute path and extension as a string
+
+ Example
+ -------
+ >> import file_tools as ft
+ >> filename = ft.openfile_dialog()
+ >> print(filename)
+
+ """
+ """will start QT Application if not running yet and returns QApplication """
+
+ # determine file types by extension
+ if file_types is None:
+ file_types = 'TEM files (*.dm3 *.dm4 *.emd *.ndata *.h5 *.hf5);;pyNSID files (*.hf5);;QF files ( *.qf3);;' \
+ 'DM files (*.dm3 *.dm4);;Nion files (*.ndata *.h5);;All files (*)'
+ elif file_types == 'pyNSID':
+ file_types = 'pyNSID files (*.hf5);;TEM files (*.dm3 *.dm4 *.qf3 *.ndata *.h5 *.hf5);;QF files ( *.qf3);;' \
+ 'DM files (*.dm3 *.dm4);;Nion files (*.ndata *.h5);;All files (*)'
+
+ # file_types = [("TEM files",["*.dm*","*.hf*","*.ndata" ]),("pyNSID files","*.hf5"),("DM files","*.dm*"),
+ # ("Nion files",["*.h5","*.ndata"]),("all files","*.*")]
+
+ # Determine last path used
+ path = get_last_path()
+
+ if Qt_available:
+ _ = get_qt_app()
+ filename = sidpy.io.interface_utils.openfile_dialog_QT(file_types=file_types, file_path=path)
+ save_path(filename)
+ return filename
+
+[docs]def save_file_dialog_qt(file_types=None): # , multiple_files=False):
+ """Opens a File dialog which is used in open_file() function
+
+ This function uses pyQt5.
+ The app of the Gui has to be running for QT. Tkinter does not run on Macs at this point in time.
+ In jupyter notebooks use %gui Qt early in the notebook.
+
+ The file looks first for a path.txt file for the last directory you used.
+
+ Parameters
+ ----------
+ file_types : string
+ file type filter in the form of '*.hf5'
+
+
+ Returns
+ -------
+ filename : string
+ full filename with absolute path and extension as a string
+
+ Example
+ -------
+ >> import file_tools as ft
+ >> filename = ft.openfile_dialog()
+ >> print(filename)
+
+ """
+ """will start QT Application if not running yet and returns QApplication """
+
+ # determine file types by extension
+ if file_types is None:
+ file_types = 'pyNSID files (*.hf5);;TEM files (*.dm3 *.dm4 *.qf3 *.ndata *.h5 *.hf5);;QF files ( *.qf3);;' \
+ 'DM files (*.dm3 *.dm4);;Nion files (*.ndata *.h5);;All files (*)'
+ elif file_types == 'TEM':
+ file_types = 'TEM files (*.dm3 *.dm4 *.emd *.ndata *.h5 *.hf5);;pyNSID files (*.hf5);;QF files ( *.qf3);;' \
+ 'DM files (*.dm3 *.dm4);;Nion files (*.ndata *.h5);;All files (*)'
+
+
+ # file_types = [("TEM files",["*.dm*","*.hf*","*.ndata" ]),("pyNSID files","*.hf5"),("DM files","*.dm*"),
+ # ("Nion files",["*.h5","*.ndata"]),("all files","*.*")]
+
+ # Determine last path used
+ path = get_last_path()
+
+ if Qt_available:
+ _ = get_qt_app()
+ filename = sidpy.io.interface_utils.savefile_dialog(file_types=file_types, file_path=path)
+ save_path(filename)
+ return filename
+
+
+[docs]def save_dataset(dataset, filename=None, h5_group=None):
+ """ Saves a dataset to a file in pyNSID format
+ Parameters
+ ----------
+ dataset: sidpy.Dataset
+ the data
+ filename: str
+ name of file to be opened, if filename is None, a QT file dialog will try to open
+ h5_group: hd5py.Group
+ not used yet
+ """
+ if filename is None:
+ filename = save_file_dialog_qt()
+ h5_filename = get_h5_filename(filename)
+ h5_file = h5py.File(h5_filename, mode='a')
+ path, file_name = os.path.split(filename)
+ basename, _ = os.path.splitext(file_name)
+
+ if isinstance(dataset, dict):
+ h5_group = save_dataset_dictionary(h5_file, dataset)
+ return h5_group
+
+ elif isinstance(dataset, sidpy.Dataset):
+ h5_dataset = save_single_dataset(h5_file, dataset, h5_group=h5_group)
+ return h5_dataset.parent
+ else:
+ raise TypeError('Only sidpy.datasets or dictionaries can be saved with pyTEMlib')
+
+
+[docs]def save_single_dataset(h5_file, dataset, h5_group=None):
+ if h5_group is None:
+ h5_measurement_group = sidpy.hdf.prov_utils.create_indexed_group(h5_file, 'Measurement_')
+ h5_group = sidpy.hdf.prov_utils.create_indexed_group(h5_measurement_group, 'Channel_')
+
+ elif isinstance(h5_group, str):
+ if h5_group not in h5_file:
+ h5_group = h5_file.create_group(h5_group)
+ else:
+ if h5_group[-1] == '/':
+ h5_group = h5_group[:-1]
+
+ channel = h5_group.split('/')[-1]
+ h5_measurement_group = h5_group[:-len(channel)]
+ h5_group = sidpy.hdf.prov_utils.create_indexed_group(h5_group, 'Channel_')
+ else:
+ raise ValueError('h5_group needs to be string or None')
+
+ h5_dataset = pyNSID.hdf_io.write_nsid_dataset(dataset, h5_group)
+ dataset.h5_dataset = h5_dataset
+ h5_dataset.file.flush()
+ return h5_dataset
+
+
+[docs]def save_dataset_dictionary(h5_file, datasets):
+ h5_measurement_group = sidpy.hdf.prov_utils.create_indexed_group(h5_file, 'Measurement_')
+ for key, dataset in datasets.items():
+ if key[-1] == '/':
+ key = key[:-1]
+ if isinstance(dataset, sidpy.Dataset):
+ h5_group = h5_measurement_group.create_group(key)
+ h5_dataset = pyNSID.hdf_io.write_nsid_dataset(dataset, h5_group)
+ dataset.h5_dataset = h5_dataset
+ h5_dataset.file.flush()
+ elif isinstance(dataset, dict):
+ sidpy.hdf.hdf_utils.write_dict_to_h5_group(h5_measurement_group, dataset, key)
+ else:
+ print('could not save item ', key, 'of dataset dictionary')
+ return h5_measurement_group
+
+
+[docs]def h5_group_to_dict(group, group_dict={}):
+ if not isinstance(group, h5py.Group):
+ raise TypeError('we need a h5py group to read from')
+ if not isinstance(group_dict, dict):
+ raise TypeError('group_dict needs to be a python dictionary')
+
+ group_dict[group.name.split('/')[-1]] = dict(group.attrs)
+ for key in group.keys():
+ h5_group_to_dict(group[key], group_dict[group.name.split('/')[-1]])
+ return group_dict
+
+
+[docs]def open_file(filename=None, h5_group=None, write_hdf_file=False): # save_file=False,
+ """Opens a file if the extension is .hf5, .ndata, .dm3 or .dm4
+
+ If no filename is provided the QT open_file windows opens (if QT_available==True)
+ Everything will be stored in a NSID style hf5 file.
+ Subroutines used:
+ - NSIDReader
+ - nsid.write_
+ - get_main_tags
+ - get_additional tags
+
+ Parameters
+ ----------
+ filename: str
+ name of file to be opened, if filename is None, a QT file dialog will try to open
+ h5_group: hd5py.Group
+ not used yet #TODO: provide hook for usage of external chosen group
+ write_hdf_file: bool
+ set to false so that sidpy dataset will not be written to hf5-file automatically
+
+ Returns
+ -------
+ sidpy.Dataset
+ sidpy dataset with location of hdf5 dataset as attribute
+
+ """
+ if filename is None:
+ selected_file = open_file_dialog_qt()
+ filename = selected_file
+
+ else:
+ if not isinstance(filename, str):
+ raise TypeError('filename must be a non-empty string or None (to a QT open file dialog)')
+ elif filename == '':
+ raise TypeError('filename must be a non-empty string or None (to a QT open file dialog)')
+
+ path, file_name = os.path.split(filename)
+ basename, extension = os.path.splitext(file_name)
+
+ if extension == '.hf5':
+ reader = SciFiReaders.NSIDReader(filename)
+ datasets = reader.read()
+ if len(datasets) < 1:
+ print('no hdf5 dataset found in file')
+ return {}
+ else:
+ dataset_dict = {}
+ for index, dataset in enumerate(datasets):
+ title = dataset.title.split('/')[2]
+ dataset.title = dataset.title.split('/')[-1]
+ dataset_dict[title] = dataset
+ if index == 0:
+ file = datasets[0].h5_dataset.file
+ master_group = datasets[0].h5_dataset.parent.parent.parent
+ for key in master_group.keys():
+
+ if key not in dataset_dict:
+ dataset_dict[key] = h5_group_to_dict(master_group[key])
+ print()
+ if not write_hdf_file:
+ file.close()
+ # datasets[0].h5_dataset = None
+ return dataset_dict
+
+ """
+ should go to no dataset found
+ if 'Raw_Data' in h5_group:
+ dataset = read_old_h5group(h5_group)
+ dataset.h5_dataset = h5_group['Raw_Data']
+ """
+
+ elif extension in ['.dm3', '.dm4', '.ndata', '.ndata1', '.h5', '.emd', '.emi']:
+
+ # tags = open_file(filename)
+ if extension in ['.dm3', '.dm4']:
+ reader = SciFiReaders.DMReader(filename)
+
+ elif extension in ['.emi']:
+ try:
+ import hyperspy.api as hs
+ s = hs.load(filename)
+ dataset_dict = {}
+ spectrum_number = 0
+ if not isinstance(s, list):
+ s = [s]
+ for index, datum in enumerate(s):
+ dset = SciFiReaders.convert_hyperspy(datum)
+ if datum.data.ndim == 1:
+ dset.title = dset.title + f'_{spectrum_number}_Spectrum'
+ spectrum_number +=1
+ elif datum.data.ndim == 3:
+ dset.title = dset.title +'_SI'
+ dset = dset.T
+ dset.title = dset.title[11:]
+ dataset_dict[f'Channel_{index:03d}']=dset
+ return dataset_dict
+ except ImportError:
+ print('This file type needs hyperspy to be installed to be able to be read')
+ return
+ elif extension == '.emd':
+ reader = SciFiReaders.EMDReader(filename)
+
+ elif extension in ['.ndata', '.h5']:
+ reader = SciFiReaders.NionReader(filename)
+
+ else:
+ raise NotImplementedError('extension not supported')
+
+ path, file_name = os.path.split(filename)
+ basename, _ = os.path.splitext(file_name)
+ if extension != '.emi':
+ dset = reader.read()
+
+ if extension in ['.dm3', '.dm4']:
+ title = (basename.strip().replace('-', '_')).split('/')[-1]
+ if not isinstance(dset, list):
+ print('Please use new SciFiReaders Package for full functionality')
+ dset = [dset]
+ if 'PageSetup' in dset[0].original_metadata:
+ del dset[0].original_metadata['PageSetup']
+ dset[0].original_metadata['original_title'] = title
+
+ if isinstance(dset, list):
+ if len(dset) < 1:
+ print('no dataset found in file')
+ return {}
+ else:
+ dataset_dict = {}
+ for index, dataset in enumerate(dset):
+ if extension == '.emi':
+ if 'experiment' in dataset.metadata:
+ if 'detector' in dataset.metadata['experiment']:
+ dataset.title = dataset.metadata['experiment']['detector']
+ dataset.filename = basename.strip()
+ # read_essential_metadata(dataset)
+ dataset.metadata['filename'] = filename
+ dataset_dict[f'Channel_{index:03}'] = dataset
+ else:
+ dset.filename = basename.strip().replace('-', '_')
+ read_essential_metadata(dset)
+ dset.metadata['filename'] = filename
+ dataset_dict = {'Channel_000': dset}
+
+ if write_hdf_file:
+ h5_master_group = save_dataset(dataset_dict, filename=filename)
+
+ save_path(filename)
+ return dataset_dict
+ else:
+ print('file type not handled yet.')
+ return
+
+
+################################################################
+# Read Functions
+#################################################################
+
+[docs]def read_essential_metadata(dataset):
+ """Updates dataset.metadata['experiment'] with essential information read from original metadata
+
+ This depends on whether it is originally a nion or a dm3 file
+ """
+ if not isinstance(dataset, sidpy.Dataset):
+ raise TypeError("we need a sidpy.Dataset")
+ experiment_dictionary = {}
+ if 'metadata' in dataset.original_metadata:
+ if 'hardware_source' in dataset.original_metadata['metadata']:
+ experiment_dictionary = read_nion_image_info(dataset.original_metadata)
+ if 'DM' in dataset.original_metadata:
+ experiment_dictionary = read_dm3_info(dataset.original_metadata)
+ if 'experiment' not in dataset.metadata:
+ dataset.metadata['experiment'] = {}
+
+ dataset.metadata['experiment'].update(experiment_dictionary)
+
+
+[docs]def read_dm3_info(original_metadata):
+ """Read essential parameter from original_metadata originating from a dm3 file"""
+ if not isinstance(original_metadata, dict):
+ raise TypeError('We need a dictionary to read')
+
+ if 'DM' not in original_metadata:
+ return {}
+ if 'ImageTags' not in original_metadata:
+ return {}
+ exp_dictionary = original_metadata['ImageTags']
+ experiment = {}
+ if 'EELS' in exp_dictionary:
+ if 'Acquisition' in exp_dictionary['EELS']:
+ for key, item in exp_dictionary['EELS']['Acquisition'].items():
+ if 'Exposure' in key:
+ _, units = key.split('(')
+ if units[:-1] == 's':
+ experiment['single_exposure_time'] = item
+ if 'Integration' in key:
+ _, units = key.split('(')
+ if units[:-1] == 's':
+ experiment['exposure_time'] = item
+ if 'frames' in key:
+ experiment['number_of_frames'] = item
+
+ if 'Experimental Conditions' in exp_dictionary['EELS']:
+ for key, item in exp_dictionary['EELS']['Experimental Conditions'].items():
+ if 'Convergence' in key:
+ experiment['convergence_angle'] = item
+ if 'Collection' in key:
+ # print(item)
+ # for val in item.values():
+ experiment['collection_angle'] = item
+ if 'number_of_frames' not in experiment:
+ experiment['number_of_frames'] = 1
+ if 'exposure_time' not in experiment:
+ if 'single_exposure_time' in experiment:
+ experiment['exposure_time'] = experiment['number_of_frames'] * experiment['single_exposure_time']
+
+ else:
+ if 'Acquisition' in exp_dictionary:
+ if 'Parameters' in exp_dictionary['Acquisition']:
+ if 'High Level' in exp_dictionary['Acquisition']['Parameters']:
+ if 'Exposure (s)' in exp_dictionary['Acquisition']['Parameters']['High Level']:
+ experiment['exposure_time'] = exp_dictionary['Acquisition']['Parameters']['High Level'][
+ 'Exposure (s)']
+
+ if 'Microscope Info' in exp_dictionary:
+ if 'Microscope' in exp_dictionary['Microscope Info']:
+ experiment['microscope'] = exp_dictionary['Microscope Info']['Microscope']
+ if 'Voltage' in exp_dictionary['Microscope Info']:
+ experiment['acceleration_voltage'] = exp_dictionary['Microscope Info']['Voltage']
+
+ return experiment
+
+
+[docs]def read_nion_image_info(original_metadata):
+ """Read essential parameter from original_metadata originating from a dm3 file"""
+ if not isinstance(original_metadata, dict):
+ raise TypeError('We need a dictionary to read')
+ if 'metadata' not in original_metadata:
+ return {}
+ if 'hardware_source' not in original_metadata['metadata']:
+ return {}
+ if 'ImageScanned' not in original_metadata['metadata']['hardware_source']:
+ return {}
+
+ exp_dictionary = original_metadata['metadata']['hardware_source']['ImageScanned']
+ experiment = exp_dictionary
+ # print(exp_dictionary)
+ if 'autostem' in exp_dictionary:
+ pass
+
+
+[docs]def get_h5_filename(fname):
+ """Determines file name of hdf5 file for newly converted data file"""
+
+ path, filename = os.path.split(fname)
+ basename, extension = os.path.splitext(filename)
+ h5_file_name_original = os.path.join(path, basename + '.hf5')
+ h5_file_name = h5_file_name_original
+
+ if os.path.exists(os.path.abspath(h5_file_name_original)):
+ count = 1
+ h5_file_name = h5_file_name_original[:-4] + '-' + str(count) + '.hf5'
+ while os.path.exists(os.path.abspath(h5_file_name)):
+ count += 1
+ h5_file_name = h5_file_name_original[:-4] + '-' + str(count) + '.hf5'
+
+ if h5_file_name != h5_file_name_original:
+ path, filename = os.path.split(h5_file_name)
+ print('Cannot overwrite file. Using: ', filename)
+ return str(h5_file_name)
+
+
+[docs]def get_start_channel(h5_file):
+ """ Legacy for get start channel"""
+
+ DeprecationWarning('Depreciated: use function get_main_channel instead')
+ return get_main_channel(h5_file)
+
+
+[docs]def get_main_channel(h5_file):
+ """Returns name of first channel group in hdf5-file"""
+
+ current_channel = None
+ if 'Measurement_000' in h5_file:
+ if 'Measurement_000/Channel_000' in h5_file:
+ current_channel = h5_file['Measurement_000/Channel_000']
+ return current_channel
+
+
+[docs]def h5_tree(input_object):
+ """Just a wrapper for the sidpy function print_tree,
+
+ so that sidpy does not have to be loaded in notebook
+
+ """
+
+ if isinstance(input_object, sidpy.Dataset):
+ if not isinstance(input_object.h5_dataset, h5py.Dataset):
+ raise ValueError('sidpy dataset does not have an associated h5py dataset')
+ h5_file = input_object.h5_dataset.file
+ elif isinstance(input_object, h5py.Dataset):
+ h5_file = input_object.file
+ elif isinstance(input_object, (h5py.Group, h5py.File)):
+ h5_file = input_object
+ else:
+ raise TypeError('should be a h5py.object or sidpy Dataset')
+ sidpy.hdf_utils.print_tree(h5_file)
+
+
+[docs]def log_results(h5_group, dataset=None, attributes=None):
+ """Log Results in hdf5-file
+
+ Saves either a sidpy.Dataset or dictionary in a hdf5-file.
+ The group for the result will consist of 'Log_' and a running index.
+ That group will be placed in h5_group.
+
+ Parameters
+ ----------
+ h5_group: hd5py.Group, or sidpy.Dataset
+ groups where result group are to be stored
+ dataset: sidpy.Dataset or None
+ sidpy dataset to be stored
+ attributes: dict
+ dictionary containing results that are not based on a sidpy.Dataset
+
+ Returns
+ -------
+ log_group: hd5py.Group
+ group in hdf5 file with results.
+
+ """
+ if isinstance(h5_group, sidpy.Dataset):
+ h5_group = h5_group.h5_dataset
+ if not isinstance(h5_group, h5py.Dataset):
+ raise TypeError('Use h5_dataset of sidpy.Dataset is not a valid h5py.Dataset')
+ h5_group = h5_group.parent.parent
+
+ if not isinstance(h5_group, h5py.Group):
+ raise TypeError('Need a valid h5py.Group for logging results')
+
+ if dataset is None:
+ log_group = sidpy.hdf.prov_utils.create_indexed_group(h5_group, 'Log_')
+ else:
+ log_group = pyNSID.hdf_io.write_results(h5_group, dataset=dataset)
+ if hasattr(dataset, 'meta_data'):
+ if 'analysis' in dataset.meta_data:
+ log_group['analysis'] = dataset.meta_data['analysis']
+ if hasattr(dataset, 'structures'):
+ for structure in dataset.structures.values():
+ h5_add_crystal_structure(log_group, structure)
+
+ dataset.h5_dataset = log_group[dataset.title.replace('-', '_')][dataset.title.replace('-', '_')]
+ if attributes is not None:
+ for key, item in attributes.items():
+ if not isinstance(item, dict):
+ log_group[key] = attributes[key]
+ else:
+ log_group.create_group(key)
+ sidpy.hdf.hdf_utils.write_simple_attrs(log_group[key], attributes[key])
+ return log_group
+
+
+[docs]def add_dataset_from_file(datasets, filename=None, key_name='Log', single_dataset=True):
+ """Add dataset to datasets dictionary
+
+ Parameters
+ ----------
+ dataset: dict
+ dictionary to write to file
+ filename: str, default: None,
+ name of file to open, if None, adialog will appear
+ key_name: str, default: 'Log'
+ name for key in dictionary with running number being added
+
+ Returns
+ -------
+ key_name: str
+ actual last used name of dictionary key
+ """
+
+ datasets2 = open_file(filename=filename)
+ first_dataset = datasets2[list(datasets2)[0]]
+ if isinstance(first_dataset, sidpy.Dataset):
+
+ index = 0
+ for key in datasets.keys():
+ if key_name in key:
+ if int(key[-3:]) >= index:
+ index = int(key[-3:])+1
+ if single_dataset:
+ datasets[key_name+f'_{index:03}'] = first_dataset
+ else:
+ for dataset in datasets2.values():
+ datasets[key_name+f'_{index:03}'] = dataset
+ index += 1
+ index -= 1
+ else:
+ return None
+
+ return f'{key_name}_{index:03}'
+
+
+# ##
+# Crystal Structure Read and Write
+# ##
+[docs]def read_poscar(file_name=None):
+ """
+ Open a POSCAR file from Vasp
+ If no file name is provided an open file dialog to select a POSCAR file appears
+
+ Parameters
+ ----------
+ file_name: str
+ if None is provided an open file dialog will appear
+
+ Return
+ ------
+ crystal: ase.Atoms
+ crystal structure in ase format
+ """
+
+ if file_name is None:
+ file_name = open_file_dialog_qt('POSCAR (POSCAR*.txt);;All files (*)')
+
+ # use ase package to read file
+ base = os.path.basename(file_name)
+ base_name = os.path.splitext(base)[0]
+ crystal = ase.io.read(file_name, format='vasp', parallel=False)
+
+ # make dictionary and plot structure (not essential for further notebook)
+ crystal.info = {'title': base_name}
+ return crystal
+
+
+[docs]def read_cif(file_name=None, verbose=False): # open file dialog to select cif file
+ """
+ Open a cif file
+ If no file name is provided an open file dialog to select a cif file appears
+
+ Parameters
+ ----------
+ file_name: str
+ if None is provided an open file dialog will appear
+ verbose: bool
+
+ Return
+ ------
+ crystal: ase.Atoms
+ crystal structure in ase format
+ """
+
+ if file_name is None:
+ file_name = open_file_dialog_qt('cif (*.cif);;All files (*)')
+ # use ase package to read file
+
+ base = os.path.basename(file_name)
+ base_name = os.path.splitext(base)[0]
+ crystal = ase.io.read(file_name, format='cif', store_tags=True, parallel=False)
+
+ # make dictionary and plot structure (not essential for further notebook)
+ if crystal.info is None:
+ crystal.info = {'title': base_name}
+ crystal.info.update({'title': base_name})
+ if verbose:
+ print('Opened cif file for ', crystal.get_chemical_formula())
+
+ return crystal
+
+
+[docs]def h5_add_crystal_structure(h5_file, input_structure, name=None):
+ """Write crystal structure to NSID file"""
+
+ if isinstance(input_structure, ase.Atoms):
+
+ crystal_tags = pyTEMlib.crystal_tools.get_dictionary(input_structure)
+ if crystal_tags['metadata'] == {}:
+ crystal_tags['metadata'] = {'title': input_structure.get_chemical_formula()}
+ elif isinstance(input_structure, dict):
+ crystal_tags = input_structure
+ else:
+ raise TypeError('Need a dictionary or an ase.Atoms object with ase installed')
+
+ structure_group = sidpy.hdf.prov_utils.create_indexed_group(h5_file, 'Structure_')
+
+ for key, item in crystal_tags.items():
+ if not isinstance(item, dict):
+ structure_group[key] = item
+
+ if 'base' in crystal_tags:
+ structure_group['relative_positions'] = crystal_tags['base']
+ if 'title' in crystal_tags:
+ structure_group['title'] = str(crystal_tags['title'])
+ structure_group['_' + crystal_tags['title']] = str(crystal_tags['title'])
+
+ # ToDo: Save all of info dictionary
+ if 'metadata' in input_structure:
+ structure_group.create_group('metadata')
+ sidpy.hdf.hdf_utils.write_simple_attrs(structure_group['metadata'], input_structure['metadata'])
+
+ h5_file.file.flush()
+ return structure_group
+
+
+[docs]def h5_add_to_structure(structure_group, crystal_tags):
+ """add dictionary as structure group"""
+
+ for key in crystal_tags:
+ if key in structure_group.keys():
+ print(key, ' not written; use new name')
+ else:
+ structure_group[key] = crystal_tags[key]
+
+
+[docs]def h5_get_crystal_structure(structure_group):
+ """Read crystal structure from NSID file
+ Any additional information will be read as dictionary into the info attribute of the ase.Atoms object
+
+ Parameters
+ ----------
+ structure_group: h5py.Group
+ location in hdf5 file to where the structure information is stored
+
+ Returns
+ -------
+ atoms: ase.Atoms object
+ crystal structure in ase format
+
+ """
+
+ crystal_tags = {'unit_cell': structure_group['unit_cell'][()],
+ 'base': structure_group['relative_positions'][()],
+ 'title': structure_group['title'][()]}
+ if '2D' in structure_group:
+ crystal_tags['2D'] = structure_group['2D'][()]
+ elements = structure_group['elements'][()]
+ crystal_tags['elements'] = []
+ for e in elements:
+ crystal_tags['elements'].append(e.astype(str, copy=False))
+
+ atoms = pyTEMlib.crystal_tools.atoms_from_dictionary(crystal_tags)
+ if 'metadata' in structure_group:
+ atoms.info = sidpy.hdf.hdf_utils.h5_group_to_dict(structure_group)
+
+ if 'zone_axis' in structure_group:
+ atoms.info = {'experiment': {'zone_axis': structure_group['zone_axis'][()]}}
+ # ToDo: Read all of info dictionary
+ return atoms
+
+
+###############################################
+# Support old pyTEM file format
+###############################################
+
+[docs]def read_old_h5group(current_channel):
+ """Make a sidpy.Dataset from pyUSID style hdf5 group
+
+ Parameters
+ ----------
+ current_channel: h5_group
+
+ Returns
+ -------
+ sidpy.Dataset
+ """
+
+ dim_dir = []
+ if 'nDim_Data' in current_channel:
+ h5_dataset = current_channel['nDim_Data']
+ reader = pyNSID.NSIDReader(h5_dataset.file.filename)
+ dataset = reader.read(h5_dataset)
+ dataset.h5_file = current_channel.file
+ return dataset
+ elif 'Raw_Data' in current_channel:
+ if 'image_stack' in current_channel:
+ sid_dataset = sidpy.Dataset.from_array(np.swapaxes(current_channel['image_stack'][()], 2, 0))
+ dim_dir = ['SPATIAL', 'SPATIAL', 'TEMPORAL']
+ elif 'data' in current_channel:
+ sid_dataset = sidpy.Dataset.from_array(current_channel['data'][()])
+ dim_dir = ['SPATIAL', 'SPATIAL']
+ else:
+ size_x = int(current_channel['spatial_size_x'][()])
+ size_y = int(current_channel['spatial_size_y'][()])
+ if 'spectral_size_x' in current_channel:
+ size_s = int(current_channel['spectral_size_x'][()])
+ else:
+ size_s = 0
+ data = np.reshape(current_channel['Raw_Data'][()], (size_x, size_y, size_s))
+ sid_dataset = sidpy.Dataset.from_array(data)
+ if size_x > 1:
+ dim_dir.append('SPATIAL')
+ if size_y > 1:
+ dim_dir.append('SPATIAL')
+ if size_s > 1:
+ dim_dir.append('SPECTRAL')
+ sid_dataset.h5_dataset = current_channel['Raw_Data']
+
+ elif 'data' in current_channel:
+ sid_dataset = sidpy.Dataset.from_array(current_channel['data'][()])
+ dim_dir = ['SPATIAL', 'SPATIAL']
+ sid_dataset.h5_dataset = current_channel['data']
+ else:
+ return
+
+ if 'SPATIAL' in dim_dir:
+ if 'SPECTRAL' in dim_dir:
+ sid_dataset.data_type = sidpy.DataType.SPECTRAL_IMAGE
+ elif 'TEMPORAL' in dim_dir:
+ sid_dataset.data_type = sidpy.DataType.IMAGE_STACK
+ else:
+ sid_dataset.data_type = sidpy.DataType.IMAGE
+ else:
+ sid_dataset.data_type = sidpy.DataType.SPECTRUM
+
+ sid_dataset.quantity = 'intensity'
+ sid_dataset.units = 'counts'
+ if 'analysis' in current_channel:
+ sid_dataset.source = current_channel['analysis'][()]
+
+ set_dimensions(sid_dataset, current_channel)
+
+ return sid_dataset
+
+
+[docs]def set_dimensions(dset, current_channel):
+ """Attaches correct dimension from old pyTEMlib style.
+
+ Parameters
+ ----------
+ dset: sidpy.Dataset
+ current_channel: hdf5.Group
+ """
+ dim = 0
+ if dset.data_type == sidpy.DataType.IMAGE_STACK:
+ dset.set_dimension(dim, sidpy.Dimension(np.arange(dset.shape[dim]), name='frame',
+ units='frame', quantity='stack',
+ dimension_type='TEMPORAL'))
+ dim += 1
+ if 'IMAGE' in dset.data_type:
+
+ if 'spatial_scale_x' in current_channel:
+ scale_x = current_channel['spatial_scale_x'][()]
+ else:
+ scale_x = 1
+ if 'spatial_units' in current_channel:
+ units_x = current_channel['spatial_units'][()]
+ if len(units_x) < 2:
+ units_x = 'pixel'
+ else:
+ units_x = 'generic'
+ if 'spatial_scale_y' in current_channel:
+ scale_y = current_channel['spatial_scale_y'][()]
+ else:
+ scale_y = 0
+ dset.set_dimension(dim, sidpy.Dimension('x', np.arange(dset.shape[dim])*scale_x,
+ units=units_x, quantity='Length',
+ dimension_type='SPATIAL'))
+ dim += 1
+ dset.set_dimension(dim, sidpy.Dimension('y', np.arange(dset.shape[dim])*scale_y,
+ units=units_x, quantity='Length',
+ dimension_type='SPATIAL'))
+ dim += 1
+ if dset.data_type in [sidpy.DataType.SPECTRUM, sidpy.DataType.SPECTRAL_IMAGE]:
+ if 'spectral_scale_x' in current_channel:
+ scale_s = current_channel['spectral_scale_x'][()]
+ else:
+ scale_s = 1.0
+ if 'spectral_units_x' in current_channel:
+ units_s = current_channel['spectral_units_x']
+ else:
+ units_s = 'eV'
+
+ if 'spectral_offset_x' in current_channel:
+ offset = current_channel['spectral_offset_x']
+ else:
+ offset = 0.0
+ dset.set_dimension(dim, sidpy.Dimension(np.arange(dset.shape[dim]) * scale_s + offset,
+ name='energy',
+ units=units_s,
+ quantity='energy_loss',
+ dimension_type='SPECTRAL'))
+
+"""
+
+"""
+import numpy as np
+# import ase
+import sys
+
+# from scipy.spatial import cKDTree, Voronoi, ConvexHull
+import scipy.spatial
+import scipy.optimize
+import scipy.interpolate
+
+from skimage.measure import grid_points_in_poly, points_in_poly
+
+# import plotly.graph_objects as go
+# import plotly.express as px
+import matplotlib.patches as patches
+
+import pyTEMlib.crystal_tools
+from tqdm.auto import tqdm, trange
+
+from .graph_viz import *
+
+
+###########################################################################
+# utility functions
+###########################################################################
+
+[docs]def interstitial_sphere_center(vertex_pos, atom_radii, optimize=True):
+ """
+ Function finds center and radius of the largest interstitial sphere of a simplex.
+ Which is the center of the cirumsphere if all atoms have the same radius,
+ but differs for differently sized atoms.
+ In the last case, the circumsphere center is used as starting point for refinement.
+
+ Parameters
+ -----------------
+ vertex_pos : numpy array
+ The position of vertices of a tetrahedron
+ atom_radii : float
+ bond radii of atoms
+ optimize: boolean
+ whether atom bond lengths are optimized or not
+ Returns
+ ----------
+ new_center : numpy array
+ The center of the largest interstitial sphere
+ radius : float
+ The radius of the largest interstitial sphere
+ """
+ center, radius = circum_center(vertex_pos, tol=1e-4)
+
+ def distance_deviation(sphere_center):
+ return np.std(np.linalg.norm(vertex_pos - sphere_center, axis=1) - atom_radii)
+
+ if np.std(atom_radii) == 0 or not optimize:
+ return center, radius-atom_radii[0]
+ else:
+ center_new = scipy.optimize.minimize(distance_deviation, center)
+ return center_new.x, np.linalg.norm(vertex_pos[0]-center_new.x)-atom_radii[0]
+
+
+[docs]def circum_center(vertex_pos, tol=1e-4):
+ """
+ Function finds the center and the radius of the circumsphere of every simplex.
+ Reference:
+ Fiedler, Miroslav. Matrices and graphs in geometry. No. 139. Cambridge University Press, 2011.
+ (p.29 bottom: example 2.1.11)
+ Code started from https://github.com/spatala/gbpy
+ with help of https://codereview.stackexchange.com/questions/77593/calculating-the-volume-of-a-tetrahedron
+
+ Parameters
+ -----------------
+ vertex_pos : numpy array
+ The position of vertices of a tetrahedron
+ tol : float
+ Tolerance defined to identify co-planar tetrahedrons
+ Returns
+ ----------
+ circum_center : numpy array
+ The center of the circumsphere
+ circum_radius : float
+ The radius of the circumsphere
+ """
+
+ # Make Cayley-Menger Matrix
+ number_vertices = len(vertex_pos)
+ matrix_c = np.identity(number_vertices+1)*-1+1
+ distances = scipy.spatial.distance.pdist(np.asarray(vertex_pos, dtype=float), metric='sqeuclidean')
+ matrix_c[1:, 1:] = scipy.spatial.distance.squareform(distances)
+ det_matrix_c = (np.linalg.det(matrix_c))
+ if abs(det_matrix_c) < tol:
+ return np.array(vertex_pos[0]*0), 0
+ matrix = -2 * np.linalg.inv(matrix_c)
+
+ center = vertex_pos[0, :]*0
+ for i in range(number_vertices):
+ center += matrix[0, i+1] * vertex_pos[i, :]
+ center /= np.sum(matrix[0, 1:])
+
+ circum_radius = np.sqrt(matrix[0, 0]) / 2
+
+ return np.array(center), circum_radius
+
+
+[docs]def voronoi_volumes(atoms):
+ """
+ Volumes of voronoi cells from
+ https://stackoverflow.com/questions/19634993/volume-of-voronoi-cell-python
+ """
+ points = atoms.positions
+ v = scipy.spatial.Voronoi(points)
+ vol = np.zeros(v.npoints)
+ for i, reg_num in enumerate(v.point_region):
+ indices = v.regions[reg_num]
+ if -1 in indices: # some regions can be opened
+ vol[i] = 0
+ else:
+ try:
+ hull = scipy.spatial.ConvexHull(v.simplices[indices])
+ vol[i] = hull.volume
+ except:
+ vol[i] = 0.
+
+ if atoms.info is None:
+ atoms.info = {}
+ # atoms.info.update({'volumes': vol})
+ return vol
+
+
+[docs]def get_bond_radii(atoms, bond_type='bond'):
+ """ get all bond radii from Kirkland
+ Parameter:
+ ----------
+ atoms ase.Atoms object
+ structure information in ase format
+ type: str
+ type of bond 'covalent' or 'metallic'
+ """
+
+ r_a = []
+ for atom in atoms:
+ if atom.symbol == 'X':
+ r_a.append(1.2)
+ else:
+ if bond_type == 'covalent':
+ r_a.append(pyTEMlib.crystal_tools.electronFF[atom.symbol]['bond_length'][0])
+ else:
+ r_a.append(pyTEMlib.crystal_tools.electronFF[atom.symbol]['bond_length'][1])
+ if atoms.info is None:
+ atoms.info = {}
+ atoms.info['bond_radii'] = r_a
+ return r_a
+
+
+[docs]def set_bond_radii(atoms, bond_type='bond'):
+ """ set certain or all bond-radii taken from Kirkland
+
+ Bond_radii are also stored in atoms.info
+
+ Parameter:
+ ----------
+ atoms ase.Atoms object
+ structure information in ase format
+ type: str
+ type of bond 'covalent' or 'metallic'
+ Return:
+ -------
+ r_a: list
+ list of atomic bond-radii
+
+ """
+ if atoms.info is None:
+ atoms.info = {}
+ if 'bond_radii' in atoms.info:
+ r_a = atoms.info['bond_radii']
+ else:
+ r_a = np.ones(len(atoms))
+
+ for atom in atoms:
+ if bond_type == 'covalent':
+ r_a[atom.index] = (pyTEMlib.crystal_tools.electronFF[atom.symbol]['bond_length'][0])
+ else:
+ r_a[atom.index] = (pyTEMlib.crystal_tools.electronFF[atom.symbol]['bond_length'][1])
+ atoms.info['bond_radii'] = r_a
+ return r_a
+
+
+[docs]def get_voronoi(tetrahedra, atoms, bond_radii=None, optimize=True):
+ """
+ Find Voronoi vertices and keep track of associated tetrahedrons and interstitial radii
+
+ Used in find_polyhedra function
+
+ Parameters
+ ----------
+ tetrahedra: scipy.spatial.Delaunay object
+ Delaunay tesselation
+ atoms: ase.Atoms object
+ the structural information
+ optimize: boolean
+ whether to use different atom radii or not
+
+ Returns
+ -------
+ voronoi_vertices: list
+ list of positions of voronoi vertices
+ voronoi_tetrahedra:
+ list of indices of associated vertices of tetrahedra
+ r_vv: list of float
+ list of all interstitial sizes
+ """
+
+ extent = atoms.cell.lengths()
+ if atoms.info is None:
+ atoms.info = {}
+
+ if bond_radii is not None:
+ bond_radii = [bond_radii]*len(atoms)
+ elif 'bond_radii' in atoms.info:
+ bond_radii = atoms.info['bond_radii']
+
+ else:
+ bond_radii = get_bond_radii(atoms)
+
+ voronoi_vertices = []
+ voronoi_tetrahedrons = []
+ r_vv = []
+ r_aa = []
+ print('Find interstitials (finding centers for different elements takes a bit)')
+ for vertices in tqdm(tetrahedra.simplices):
+ r_a = []
+ for vert in vertices:
+ r_a.append(bond_radii[vert])
+ voronoi, radius = interstitial_sphere_center(atoms.positions[vertices], r_a, optimize=optimize)
+
+ r_a = np.average(r_a) # np.min(r_a)
+ r_aa.append(r_a)
+
+ if (voronoi >= 0).all() and (extent - voronoi > 0).all() and radius > 0.01:
+ voronoi_vertices.append(voronoi)
+ voronoi_tetrahedrons.append(vertices)
+ r_vv.append(radius)
+ return voronoi_vertices, voronoi_tetrahedrons, r_vv, np.max(r_aa)
+
+
+[docs]def find_overlapping_spheres(voronoi_vertices, r_vv, r_a, cheat=1.):
+ """Find overlapping spheres"""
+
+ vertex_tree = scipy.spatial.cKDTree(voronoi_vertices)
+
+ pairs = vertex_tree.query_pairs(r=r_a * 2)
+
+ overlapping_pairs = []
+ for (i, j) in pairs:
+ if np.linalg.norm(voronoi_vertices[i] - voronoi_vertices[j]) < (r_vv[i] + r_vv[j]) * cheat:
+ overlapping_pairs.append([i, j])
+
+ return np.array(sorted(overlapping_pairs))
+
+
+[docs]def find_interstitial_clusters(overlapping_pairs):
+ """Make clusters
+ Breadth first search to go through the list of overlapping spheres or circles to determine clusters
+ """
+ visited_all = []
+ clusters = []
+ for initial in overlapping_pairs[:, 0]:
+ if initial not in visited_all:
+ # breadth first search
+ visited = [] # the atoms we visited
+ queue = [initial]
+ while queue:
+ node = queue.pop(0)
+ if node not in visited_all:
+ visited.append(node)
+ visited_all.append(node)
+ # neighbors = overlapping_pairs[overlapping_pairs[:,0]==node,1]
+ neighbors = np.append(overlapping_pairs[overlapping_pairs[:, 1] == node, 0],
+ overlapping_pairs[overlapping_pairs[:, 0] == node, 1])
+
+ for i, neighbour in enumerate(neighbors):
+ if neighbour not in visited:
+ queue.append(neighbour)
+ clusters.append(visited)
+ return clusters, visited_all
+
+
+[docs]def make_polygons(atoms, voronoi_vertices, voronoi_tetrahedrons, clusters, visited_all):
+ """ make polygons from convex hulls of vertices around interstitial positions"""
+ polyhedra = {}
+ for index, cluster in tqdm(enumerate(clusters)):
+ cc = []
+ for c in cluster:
+ cc = cc + list(voronoi_tetrahedrons[c])
+
+ hull = scipy.spatial.ConvexHull(atoms.positions[list(set(cc)), :2])
+ faces = []
+ triangles = []
+ for s in hull.simplices:
+ faces.append(atoms.positions[list(set(cc))][s])
+ triangles.append(list(s))
+ polyhedra[index] = {'vertices': atoms.positions[list(set(cc))], 'indices': list(set(cc)),
+ 'faces': faces, 'triangles': triangles,
+ 'length': len(list(set(cc))),
+ 'combined_vertices': cluster,
+ 'interstitial_index': index,
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[cluster].mean(axis=0),
+ 'atomic_numbers': atoms.get_atomic_numbers()[list(set(cc))]} # , 'volume': hull.volume}
+ # 'coplanar': hull.coplanar}
+
+ print('Define conventional interstitial polyhedra')
+ running_number = index + 0
+ for index in trange(len(voronoi_vertices)):
+ if index not in visited_all:
+ vertices = voronoi_tetrahedrons[index]
+ hull = scipy.spatial.ConvexHull(atoms.positions[vertices, :2])
+ faces = []
+ triangles = []
+ for s in hull.simplices:
+ faces.append(atoms.positions[vertices][s])
+ triangles.append(list(s))
+
+ polyhedra[running_number] = {'vertices': atoms.positions[vertices], 'indices': vertices,
+ 'faces': faces, 'triangles': triangles,
+ 'length': len(vertices),
+ 'combined_vertices': index,
+ 'interstitial_index': running_number,
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[index],
+ 'atomic_numbers': atoms.get_atomic_numbers()[vertices]}
+ # 'volume': hull.volume}
+
+ running_number += 1
+
+ return polyhedra
+
+
+[docs]def make_polyhedrons(atoms, voronoi_vertices, voronoi_tetrahedrons, clusters, visited_all):
+ """collect output data and make dictionary"""
+
+ polyhedra = {}
+ import scipy.sparse
+ connectivity_matrix = scipy.sparse.dok_matrix((len(atoms), len(atoms)), dtype=bool)
+
+ print('Define clustered interstitial polyhedra')
+ for index, cluster in tqdm(enumerate(clusters)):
+ cc = []
+ for c in cluster:
+ cc = cc + list(voronoi_tetrahedrons[c])
+ cc = list(set(cc))
+
+ hull = scipy.spatial.ConvexHull(atoms.positions[cc])
+ faces = []
+ triangles = []
+ for s in hull.simplices:
+ faces.append(atoms.positions[cc][s])
+ triangles.append(list(s))
+ for k in range(len(s)):
+ l = (k + 1) % len(s)
+ if cc[s[k]] > cc[s[l]]:
+ connectivity_matrix[cc[s[l]], cc[s[k]]] = True
+ else:
+ connectivity_matrix[cc[s[k]], cc[s[l]]] = True
+
+ polyhedra[index] = {'vertices': atoms.positions[list(set(cc))], 'indices': list(set(cc)),
+ 'faces': faces, 'triangles': triangles,
+ 'length': len(list(set(cc))),
+ 'combined_vertices': cluster,
+ 'interstitial_index': index,
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[cluster].mean(axis=0),
+ 'atomic_numbers': atoms.get_atomic_numbers()[list(set(cc))],
+ 'volume': hull.volume}
+ # 'coplanar': hull.coplanar}
+
+ print('Define conventional interstitial polyhedra')
+ running_number = index + 0
+ for index in range(len(voronoi_vertices)):
+ if index not in visited_all:
+ vertices = voronoi_tetrahedrons[index]
+ hull = scipy.spatial.ConvexHull(atoms.positions[vertices])
+ faces = []
+ triangles = []
+ for s in hull.simplices:
+ faces.append(atoms.positions[vertices][s])
+ triangles.append(list(s))
+ for k in range(len(s)):
+ l = (k + 1) % len(s)
+ if cc[s[k]] > cc[s[l]]:
+ connectivity_matrix[cc[s[l]], cc[s[k]]] = True
+ else:
+ connectivity_matrix[cc[s[k]], cc[s[l]]] = True
+
+ polyhedra[running_number] = {'vertices': atoms.positions[vertices], 'indices': vertices,
+ 'faces': faces, 'triangles': triangles,
+ 'length': len(vertices),
+ 'combined_vertices': index,
+ 'interstitial_index': running_number,
+ 'interstitial_site': np.array(voronoi_tetrahedrons)[index],
+ 'atomic_numbers': atoms.get_atomic_numbers()[vertices],
+ 'volume': hull.volume}
+
+ running_number += 1
+ if atoms.info is None:
+ atoms.info = {}
+ atoms.info.update({'graph': {'connectivity_matrix': connectivity_matrix}})
+ return polyhedra
+
+
+##################################################################
+# polyhedra functions
+##################################################################
+
+[docs]def get_non_periodic_supercell(super_cell):
+ super_cell.wrap()
+ atoms = super_cell*3
+ atoms.positions -= super_cell.cell.lengths()
+ atoms.positions[:,0] += super_cell.cell[0,0]*.0
+ del(atoms[atoms.positions[: , 0]<-5])
+ del(atoms[atoms.positions[: , 0]>super_cell.cell[0,0]+5])
+ del(atoms[atoms.positions[: , 1]<-5])
+ del(atoms[atoms.positions[: , 1]>super_cell.cell[1,1]+5])
+ del(atoms[atoms.positions[: , 2]<-5])
+ del(atoms[atoms.positions[: , 2]>super_cell.cell[2,2]+5])
+ return atoms
+
+[docs]def get_connectivity_matrix(crystal, atoms, polyhedra):
+ crystal_tree = scipy.spatial.cKDTree(crystal.positions)
+
+
+ connectivity_matrix = np.zeros([len(atoms),len(atoms)], dtype=int)
+
+ for polyhedron in polyhedra.values():
+ vertices = polyhedron['vertices'] - crystal.cell.lengths()
+ atom_ind = np.array(polyhedron['indices'])
+ dd, polyhedron['atom_indices'] = crystal_tree.query(vertices , k=1)
+ to_bond = np.where(dd<0.001)[0]
+
+ for triangle in polyhedron['triangles']:
+ triangle = np.array(triangle)
+ for permut in [[0,1], [1,2], [0,2]]:
+ vertex = [np.min(triangle[permut]), np.max(triangle[permut])]
+ if vertex[0] in to_bond or vertex[1] in to_bond:
+ connectivity_matrix[atom_ind[vertex[1]], atom_ind[vertex[0]]] = 1
+ connectivity_matrix[atom_ind[vertex[0]], atom_ind[vertex[1]]] = 1
+ return connectivity_matrix
+
+
+
+[docs]def get_bonds(crystal, shift= 0., verbose = False, cheat=1.0):
+ """
+ Get polyhedra, and bonds from and edges and lengths of edges for each polyhedron and store it in info dictionary of new ase.Atoms object
+
+ Parameter:
+ ----------
+ crystal: ase.atoms_object
+ information on all polyhedra
+ """
+ crystal.positions += shift * crystal.cell[0, 0]
+ crystal.wrap()
+
+ atoms = get_non_periodic_supercell(crystal)
+ atoms = atoms[atoms.numbers.argsort()]
+
+
+ atoms.positions += crystal.cell.lengths()
+ polyhedra = find_polyhedra(atoms, cheat=cheat)
+
+ connectivity_matrix = get_connectivity_matrix(crystal, atoms, polyhedra)
+ coord = connectivity_matrix.sum(axis=1)
+
+ del(atoms[np.where(coord==0)])
+ new_polyhedra = {}
+ index = 0
+ octahedra =[]
+ tetrahedra = []
+ other = []
+ super_cell_atoms =[]
+
+ atoms_tree = scipy.spatial.cKDTree(atoms.positions-crystal.cell.lengths())
+ crystal_tree = scipy.spatial.cKDTree(crystal.positions)
+ connectivity_matrix = np.zeros([len(atoms),len(atoms)], dtype=float)
+
+ for polyhedron in polyhedra.values():
+ polyhedron['vertices'] -= crystal.cell.lengths()
+ vertices = polyhedron['vertices']
+ center = np.average(polyhedron['vertices'], axis=0)
+
+ dd, polyhedron['indices'] = atoms_tree.query(vertices , k=1)
+ atom_ind = (np.array(polyhedron['indices']))
+ dd, polyhedron['atom_indices'] = crystal_tree.query(vertices , k=1)
+
+ to_bond = np.where(dd<0.001)[0]
+ super_cell_atoms.extend(list(atom_ind[to_bond]))
+
+ edges = []
+ lengths = []
+ for triangle in polyhedron['triangles']:
+ triangle = np.array(triangle)
+ for permut in [[0,1], [1,2], [0,2]]:
+ vertex = [np.min(triangle[permut]), np.max(triangle[permut])]
+ length = np.linalg.norm(vertices[vertex[0]]-vertices[vertex[1]])
+ if vertex[0] in to_bond or vertex[1] in to_bond:
+ connectivity_matrix[atom_ind[vertex[1]], atom_ind[vertex[0]]] = length
+ connectivity_matrix[atom_ind[vertex[0]], atom_ind[vertex[1]]] = length
+ if vertex[0] not in to_bond:
+ atoms[atom_ind[vertex[0]]].symbol = 'Be'
+ if vertex[1] not in to_bond:
+ atoms[atom_ind[vertex[1]]].symbol = 'Be'
+ if vertex not in edges:
+ edges.append(vertex)
+ lengths.append(np.linalg.norm(vertices[vertex[0]]-vertices[vertex[1]] ))
+ polyhedron['edges'] = edges
+ polyhedron['edge_lengths'] = lengths
+ if all(center > -0.000001) and all(center < crystal.cell.lengths()-0.01):
+ new_polyhedra[str(index)]=polyhedron
+ if polyhedron['length'] == 4:
+ tetrahedra.append(str(index))
+ elif polyhedron['length'] == 6:
+ octahedra.append(str(index))
+ else:
+ other.append(str(index))
+ if verbose:
+ print(polyhedron['length'])
+ index += 1
+ atoms.positions -= crystal.cell.lengths()
+ coord = connectivity_matrix.copy()
+ coord[np.where(coord>.1)] = 1
+ coord = coord.sum(axis=1)
+
+ super_cell_atoms = np.sort(np.unique(super_cell_atoms))
+ atoms.info.update({'polyhedra': {'polyhedra': new_polyhedra,
+ 'tetrahedra': tetrahedra,
+ 'octahedra': octahedra,
+ 'other' : other}})
+ atoms.info.update({'bonds': {'connectivity_matrix': connectivity_matrix,
+ 'super_cell_atoms': super_cell_atoms,
+ 'super_cell_dimensions': crystal.cell.array,
+ 'coordination': coord}})
+ atoms.info.update({'supercell': crystal})
+ return atoms
+
+[docs]def plot_atoms(atoms: ase.Atoms, polyhedra_indices=None, plot_bonds=False, color='', template=None, atom_size=None, max_size=35) -> go.Figure:
+ """
+ Plot structure in a ase.Atoms object with plotly
+
+ If the info dictionary of the atoms object contains bond or polyedra information, these can be set tobe plotted
+
+ Partameter:
+ -----------
+ atoms: ase.Atoms object
+ structure of supercell
+ polyhedra_indices: list of integers
+ indices of polyhedra to be plotted
+ plot_bonds: boolean
+ whether to plot bonds or not
+
+ Returns:
+ --------
+ fig: plotly figure object
+ handle to figure needed to modify appearance
+ """
+ energies = np.zeros(len(atoms))
+ if 'bonds' in atoms.info:
+ if 'atom_energy' in atoms.info['bonds']:
+ energies = np.round(np.array(atoms.info['bonds']['atom_energy'] - 12 * atoms.info['bonds']['ideal_bond_energy']) *1000,0)
+
+ for atom in atoms:
+ if atom.index not in atoms.info['bonds']['super_cell_atoms']:
+ energies[atom.index] = 0.
+ if color == 'coordination':
+ colors = atoms.info['bonds']['coordination']
+ elif color == 'layer':
+ colors = atoms.positions[:, 2]
+ elif color == 'energy':
+ colors = energies
+ colors[colors>50] = 50
+ colors = np.log(1+ energies)
+
+ else:
+ colors = atoms.get_atomic_numbers()
+
+ if atom_size is None:
+ atom_size = atoms.get_atomic_numbers()*4
+ elif isinstance(atom_size, float):
+ atom_size = atoms.get_atomic_numbers()*4*atom_size
+ atom_size[atom_size>max_size] = max_size
+ elif isinstance(atom_size, int):
+ atom_size = [atom_size]*len(atoms)
+ if len(atom_size) != len(atoms):
+ atom_size = [10]*len(atoms)
+ print('wrong length of atom_size parameter')
+ plot_polyhedra = False
+ data = []
+ if polyhedra_indices is not None:
+ if 'polyhedra' in atoms.info:
+ if polyhedra_indices == -1:
+ data = plot_polyhedron(atoms.info['polyhedra']['polyhedra'], range(len(atoms.info['polyhedra']['polyhedra'])))
+ plot_polyhedra = True
+ elif isinstance(polyhedra_indices, list):
+ data = plot_polyhedron(atoms.info['polyhedra']['polyhedra'], polyhedra_indices)
+ plot_polyhedra = True
+ text = []
+ if 'bonds' in atoms.info:
+ coord = atoms.info['bonds']['coordination']
+ for atom in atoms:
+ if atom.index in atoms.info['bonds']['super_cell_atoms']:
+
+ text.append(f'Atom {atom.index}: coordination={coord[atom.index]}' +
+ f'x:{atom.position[0]:.2f} \n y:{atom.position[1]:.2f} \n z:{atom.position[2]:.2f}')
+ if 'atom_energy' in atoms.info['bonds']:
+ text[-1] += f"\n energy: {energies[atom.index]:.0f} meV"
+ else:
+ text.append('')
+ else:
+ text = [''] * len(atoms)
+
+ if plot_bonds:
+ data += get_plot_bonds(atoms)
+ if plot_polyhedra or plot_bonds:
+ fig = go.Figure(data=data)
+ else:
+ fig = go.Figure()
+ if color=='energy':
+ fig.add_trace(go.Scatter3d(
+ mode='markers',
+ x=atoms.positions[:,0], y=atoms.positions[:,1], z=atoms.positions[:,2],
+ hovertemplate='<b>%{text}</b><extra></extra>',
+ text = text,
+ marker=dict(
+ color=colors,
+ size=atom_size,
+ sizemode='diameter',
+ colorscale='Rainbow', #px.colors.qualitative.Light24,
+ colorbar=dict(thickness=10, orientation='h'))))
+ #hover_name = colors))) # ["blue", "green", "red"])))
+
+ elif 'bonds' in atoms.info:
+ fig.add_trace(go.Scatter3d(
+ mode='markers',
+ x=atoms.positions[:,0], y=atoms.positions[:,1], z=atoms.positions[:,2],
+ hovertemplate='<b>%{text}</b><extra></extra>',
+ text = text,
+ marker=dict(
+ color=colors,
+ size=atom_size,
+ sizemode='diameter',
+ colorscale= px.colors.qualitative.Light24)))
+ #hover_name = colors))) # ["blue", "green", "red"])))
+
+ else:
+ fig.add_trace(go.Scatter3d(
+ mode='markers',
+ x=atoms.positions[:,0], y=atoms.positions[:,1], z=atoms.positions[:,2],
+ marker=dict(
+ color=colors,
+ size=atom_size,
+ sizemode='diameter',
+ colorbar=dict(thickness=10),
+ colorscale= px.colors.qualitative.Light24)))
+ #hover_name = colors))) # ["blue", "green", "red"])))
+ fig.update_layout(width=1000, height=700, showlegend=False, template=template)
+ fig.update_layout(scene_aspectmode='data',
+ scene_aspectratio=dict(x=1, y=1, z=1))
+
+ camera = {'up': {'x': 0, 'y': 1, 'z': 0},
+ 'center': {'x': 0, 'y': 0, 'z': 0},
+ 'eye': {'x': 0, 'y': 0, 'z': 1}}
+ fig.update_coloraxes(showscale=True)
+ fig.update_layout(scene_camera=camera, title=r"Al-GB $")
+ fig.update_scenes(camera_projection_type="orthographic" )
+ fig.show()
+ return fig
+
+
+
+
+[docs]def find_polyhedra(atoms, optimize=True, cheat=1.0, bond_radii=None):
+ """ get polyhedra information from an ase.Atoms object
+
+ This is following the method of Banadaki and Patala
+ http://dx.doi.org/10.1038/s41524-017-0016-0
+
+ We are using the bond radius according to Kirkland, which is tabulated in
+ - pyTEMlib.crystal_tools.electronFF[atoms.symbols[vert]]['bond_length'][1]
+
+ Parameter
+ ---------
+ atoms: ase.Atoms object
+ the structural information
+ cheat: float
+ does not exist
+
+ Returns
+ -------
+ polyhedra: dict
+ dictionary with all information of polyhedra
+ """
+ if not isinstance(atoms, ase.Atoms):
+ raise TypeError('This function needs an ase.Atoms object')
+
+ if np.abs(atoms.positions[:, 2]).sum() <= 0.01:
+ tetrahedra = scipy.spatial.Delaunay(atoms.positions[:, :2])
+ else:
+ tetrahedra = scipy.spatial.Delaunay(atoms.positions)
+
+ voronoi_vertices, voronoi_tetrahedrons, r_vv, r_a = get_voronoi(tetrahedra, atoms, optimize=optimize, bond_radii=bond_radii)
+ if np.abs(atoms.positions[:, 2]).sum() <= 0.01:
+ r_vv = np.array(r_vv)*3.
+ overlapping_pairs = find_overlapping_spheres(voronoi_vertices, r_vv, r_a, cheat=cheat)
+
+ clusters, visited_all = find_interstitial_clusters(overlapping_pairs)
+
+ if np.abs(atoms.positions[:, 2]).sum() <= 0.01:
+ rings = get_polygons(atoms, clusters, voronoi_tetrahedrons)
+ return rings
+ else:
+ polyhedra = make_polyhedrons(atoms, voronoi_vertices, voronoi_tetrahedrons, clusters, visited_all)
+ return polyhedra
+
+
+[docs]def polygon_sort(corners):
+ center = np.average(corners[:, :2], axis=0)
+ angles = (np.arctan2(corners[:,0]-center[0], corners[:,1]-center[1]) + 2.0 * np.pi)% (2.0 * np.pi)
+ return corners[np.argsort(angles)]
+
+[docs]def get_polygons(atoms, clusters, voronoi_tetrahedrons):
+ polygons = []
+ cyclicity = []
+ centers = []
+ corners =[]
+ for index, cluster in (enumerate(clusters)):
+ cc = []
+ for c in cluster:
+ cc = cc + list(voronoi_tetrahedrons[c])
+
+ sorted_corners = polygon_sort(atoms.positions[list(set(cc)), :2])
+ cyclicity.append(len(sorted_corners))
+ corners.append(sorted_corners)
+ centers.append(np.mean(sorted_corners[:,:2], axis=0))
+ polygons.append(patches.Polygon(np.array(sorted_corners)[:,:2], closed=True, fill=True, edgecolor='red'))
+
+ rings={'atoms': atoms.positions[:, :2],
+ 'cyclicity': np.array(cyclicity),
+ 'centers': np.array(centers),
+ 'corners': corners,
+ 'polygons': polygons}
+ return rings
+
+
+[docs]def sort_polyhedra_by_vertices(polyhedra, visible=range(4, 100), z_lim=[0, 100], verbose=False):
+ indices = []
+
+ for key, polyhedron in polyhedra.items():
+ if 'length' not in polyhedron:
+ polyhedron['length'] = len(polyhedron['vertices'])
+
+ if polyhedron['length'] in visible:
+ center = polyhedron['vertices'].mean(axis=0)
+ if z_lim[0] < center[2] < z_lim[1]:
+ indices.append(key)
+ if verbose:
+ print(key, polyhedron['length'], center)
+ return indices
+
+# color_scheme = ['lightyellow', 'silver', 'rosybrown', 'lightsteelblue', 'orange', 'cyan', 'blue', 'magenta',
+# 'firebrick', 'forestgreen']
+
+
+
+##########################
+# New Graph Stuff
+##########################
+[docs]def breadth_first_search(graph, initial, projected_crystal):
+ """ breadth first search of atoms viewed as a graph
+
+ the projection dictionary has to contain the following items
+ 'number_of_nearest_neighbours', 'rotated_cell', 'near_base', 'allowed_variation'
+
+ Parameters
+ ----------
+ graph: numpy array (Nx2)
+ the atom positions
+ initial: int
+ index of starting atom
+ projection_tags: dict
+ dictionary with information on projected unit cell (with 'rotated_cell' item)
+
+ Returns
+ -------
+ graph[visited]: numpy array (M,2) with M<N
+ positions of atoms hopped in unit cell lattice
+ ideal: numpy array (M,2)
+ ideal atom positions
+ """
+
+ projection_tags = projected_crystal.info['projection']
+
+ # get lattice vectors to hopp along through graph
+ projected_unit_cell = projected_crystal.cell[:2, :2]
+ a_lattice_vector = projected_unit_cell[0]
+ b_lattice_vector = projected_unit_cell[1]
+ main = np.array([a_lattice_vector, -a_lattice_vector, b_lattice_vector, -b_lattice_vector]) # vectors of unit cell
+ near = np.append(main, projection_tags['near_base'], axis=0) # all nearest atoms
+ # get k next nearest neighbours for each node
+ neighbour_tree = scipy.spatial.cKDTree(graph)
+ distances, indices = neighbour_tree.query(graph, # let's get all neighbours
+ k=50) # projection_tags['number_of_nearest_neighbours']*2 + 1)
+ # print(projection_tags['number_of_nearest_neighbours'] * 2 + 1)
+ visited = [] # the atoms we visited
+ ideal = [] # atoms at ideal lattice
+ sub_lattice = [] # atoms in base and disregarded
+ queue = [initial]
+ ideal_queue = [graph[initial]]
+
+ while queue:
+ node = queue.pop(0)
+ ideal_node = ideal_queue.pop(0)
+
+ if node not in visited:
+ visited.append(node)
+ ideal.append(ideal_node)
+ # print(node,ideal_node)
+ neighbors = indices[node]
+ for i, neighbour in enumerate(neighbors):
+ if neighbour not in visited:
+ distance_to_ideal = np.linalg.norm(near + graph[node] - graph[neighbour], axis=1)
+
+ if np.min(distance_to_ideal) < projection_tags['allowed_variation']:
+ direction = np.argmin(distance_to_ideal)
+ if direction > 3: # counting starts at 0
+ sub_lattice.append(neighbour)
+ elif distances[node, i] < projection_tags['distance_unit_cell'] * 1.05:
+ queue.append(neighbour)
+ ideal_queue.append(ideal_node + near[direction])
+
+ return graph[visited], ideal
+
+####################
+# Distortion Matrix
+####################
+[docs]def get_distortion_matrix(atoms, ideal_lattice):
+ """ Calculates distortion matrix
+
+ Calculates the distortion matrix by comparing ideal and distorted Voronoi tiles
+ """
+
+ vor = scipy.spatial.Voronoi(atoms)
+
+ # determine a middle Voronoi tile
+ ideal_vor = scipy.spatial.Voronoi(ideal_lattice)
+ near_center = np.average(ideal_lattice, axis=0)
+ index = np.argmin(np.linalg.norm(ideal_lattice - near_center, axis=0))
+
+ # the ideal vertices fo such an Voronoi tile (are there crystals with more than one voronoi?)
+ ideal_vertices = ideal_vor.vertices[ideal_vor.regions[ideal_vor.point_region[index]]]
+ ideal_vertices = get_significant_vertices(ideal_vertices - np.average(ideal_vertices, axis=0))
+
+ distortion_matrix = []
+ for index in range(vor.points.shape[0]):
+ done = int((index + 1) / vor.points.shape[0] * 50)
+ sys.stdout.write('\r')
+ # progress output :
+ sys.stdout.write("[%-50s] %d%%" % ('=' * done, 2 * done))
+ sys.stdout.flush()
+
+ # determine vertices of Voronoi polygons of an atom with number index
+ poly_point = vor.points[index]
+ poly_vertices = get_significant_vertices(vor.vertices[vor.regions[vor.point_region[index]]] - poly_point)
+
+ # where ATOM has to be moved (not pixel)
+ ideal_point = ideal_lattice[index]
+
+ # transform voronoi to ideal one and keep transformation matrix A
+ uncorrected, corrected, aa = transform_voronoi(poly_vertices, ideal_vertices)
+
+ # pixel positions
+ corrected = corrected + ideal_point + (np.rint(poly_point) - poly_point)
+ for i in range(len(corrected)):
+ # original image pixels
+ x, y = uncorrected[i] + np.rint(poly_point)
+ # collect the two origin and target coordinates and store
+ distortion_matrix.append([x, y, corrected[i, 0], corrected[i, 1]])
+ print()
+ return np.array(distortion_matrix)
+
+
+[docs]def undistort(distortion_matrix, image_data):
+ """ Undistort image according to distortion matrix
+
+ Uses the griddata interpolation of scipy to apply distortion matrix to image.
+ The distortion matrix contains in origin and target pixel coordinates
+ target is where the pixel has to be moved (floats)
+
+ Parameters
+ ----------
+ distortion_matrix: numpy array (Nx2)
+ distortion matrix (format N x 2)
+ image_data: numpy array or sidpy.Dataset
+ image
+
+ Returns
+ -------
+ interpolated: numpy array
+ undistorted image
+ """
+
+ intensity_values = image_data[(distortion_matrix[:, 0].astype(int), distortion_matrix[:, 1].astype(int))]
+
+ corrected = distortion_matrix[:, 2:4]
+
+ size_x, size_y = 2 ** np.round(np.log2(image_data.shape[0:2])) # nearest power of 2
+ size_x = int(size_x)
+ size_y = int(size_y)
+ grid_x, grid_y = np.mgrid[0:size_x - 1:size_x * 1j, 0:size_y - 1:size_y * 1j]
+ print('interpolate')
+
+ interpolated = scipy.interpolate.griddata(np.array(corrected), np.array(intensity_values), (grid_x, grid_y), method='linear')
+ return interpolated
+
+
+def transform_voronoi(vertices, ideal_voronoi):
+ """ find transformation matrix A between a distorted polygon and a perfect reference one
+
+ Returns
+ -------
+ uncorrected: list of points:
+ all points on a grid within original polygon
+ corrected: list of points:
+ coordinates of these points where pixel have to move to
+ aa: 2x2 matrix A:
+ transformation matrix
+ """
+
+ # Find Transformation Matrix, note polygons have to be ordered first.
+ sort_vert = []
+ for vert in ideal_voronoi:
+ sort_vert.append(np.argmin(np.linalg.norm(vertices - vert, axis=1)))
+ vertices = np.array(vertices)[sort_vert]
+
+ # Solve the least squares problem X * A = Y
+ # to find our transformation matrix aa = A
+ aa, res, rank, s = np.linalg.lstsq(vertices, ideal_voronoi, rcond=None)
+
+ # expand polygon to include more points in distortion matrix
+ vertices2 = vertices + np.sign(vertices) # +np.sign(vertices)
+
+ ext_v = int(np.abs(vertices2).max() + 1)
+
+ polygon_grid = np.mgrid[0:ext_v * 2 + 1, :ext_v * 2 + 1] - ext_v
+ polygon_grid = np.swapaxes(polygon_grid, 0, 2)
+ polygon_array = polygon_grid.reshape(-1, polygon_grid.shape[-1])
+
+ p = points_in_poly(polygon_array, vertices2)
+ uncorrected = polygon_array[p]
+
+ corrected = np.dot(uncorrected, aa)
+
+ return uncorrected, corrected, aa
+
+
+[docs]def get_maximum_view(distortion_matrix):
+ distortion_matrix_extent = np.ones(distortion_matrix.shape[1:], dtype=int)
+ distortion_matrix_extent[distortion_matrix[0] == -1000.] = 0
+
+ area = distortion_matrix_extent
+ view_square = np.array([0, distortion_matrix.shape[1] - 1, 0, distortion_matrix.shape[2] - 1], dtype=int)
+ while np.array(np.where(area == 0)).shape[1] > 0:
+ view_square = view_square + [1, -1, 1, -1]
+ area = distortion_matrix_extent[view_square[0]:view_square[1], view_square[2]:view_square[3]]
+
+ change = [-int(np.sum(np.min(distortion_matrix_extent[:view_square[0], view_square[2]:view_square[3]], axis=1))),
+ int(np.sum(np.min(distortion_matrix_extent[view_square[1]:, view_square[2]:view_square[3]], axis=1))),
+ -int(np.sum(np.min(distortion_matrix_extent[view_square[0]:view_square[1], :view_square[2]], axis=0))),
+ int(np.sum(np.min(distortion_matrix_extent[view_square[0]:view_square[1], view_square[3]:], axis=0)))]
+
+ return np.array(view_square) + change
+
+
+[docs]def get_significant_vertices(vertices, distance=3):
+ """Calculate average for all points that are closer than distance apart, otherwise leave the points alone
+
+ Parameters
+ ----------
+ vertices: numpy array (n,2)
+ list of points
+ distance: float
+ (in same scale as points )
+
+ Returns
+ -------
+ ideal_vertices: list of floats
+ list of points that are all a minimum of 3 apart.
+ """
+
+ tt = scipy.spatial.cKDTree(np.array(vertices))
+ near = tt.query_ball_point(vertices, distance)
+ ideal_vertices = []
+ for indices in near:
+ if len(indices) == 1:
+ ideal_vertices.append(vertices[indices][0])
+ else:
+ ideal_vertices.append(np.average(vertices[indices], axis=0))
+ ideal_vertices = np.unique(np.array(ideal_vertices), axis=0)
+ angles = np.arctan2(ideal_vertices[:, 1], ideal_vertices[:, 0])
+ ang_sort = np.argsort(angles)
+
+ ideal_vertices = ideal_vertices[ang_sort]
+
+ return ideal_vertices
+
+
+[docs]def transform_voronoi(vertices, ideal_voronoi):
+ """
+ find transformation matrix A between a polygon and a perfect one
+
+ returns:
+ list of points: all points on a grid within original polygon
+ list of points: coordinates of these points where pixel have to move to
+ 2x2 matrix aa: transformation matrix
+ """
+ # Find Transformation Matrix, note polygons have to be ordered first.
+ sort_vert = []
+ for vert in ideal_voronoi:
+ sort_vert.append(np.argmin(np.linalg.norm(vertices - vert, axis=1)))
+ vertices = np.array(vertices)[sort_vert]
+
+ # Solve the least squares problem X * A = Y
+ # to find our transformation matrix A
+ aa, res, rank, s = np.linalg.lstsq(vertices, ideal_voronoi, rcond=None)
+
+ # expand polygon to include more points in distortion matrix
+ vertices2 = vertices + np.sign(vertices) # +np.sign(vertices)
+
+ ext_v = int(np.abs(vertices2).max() + 1)
+
+ polygon_grid = np.mgrid[0:ext_v * 2 + 1, :ext_v * 2 + 1] - ext_v
+ polygon_grid = np.swapaxes(polygon_grid, 0, 2)
+ polygon_array = polygon_grid.reshape(-1, polygon_grid.shape[-1])
+
+ p = points_in_poly(polygon_array, vertices2)
+ uncorrected = polygon_array[p]
+
+ corrected = np.dot(uncorrected, aa)
+
+ return uncorrected, corrected, aa
+
+
+
+[docs]def undistort_sitk(image_data, distortion_matrix):
+ """ use simple ITK to undistort image
+
+ Parameters
+ ----------
+ image_data: numpy array with size NxM
+ distortion_matrix: sidpy.Dataset or numpy array with size 2 x P x Q
+ with P, Q >= M, N
+
+ Returns
+ -------
+ image: numpy array MXN
+
+ """
+ resampler = sitk.ResampleImageFilter()
+ resampler.SetReferenceImage(sitk.GetImageFromArray(image_data))
+ resampler.SetInterpolator(sitk.sitkBSpline)
+ resampler.SetDefaultPixelValue(0)
+
+ distortion_matrix2 = distortion_matrix[:, :image_data.shape[0], :image_data.shape[1]]
+
+ displ2 = sitk.Compose(
+ [sitk.GetImageFromArray(-distortion_matrix2[1]), sitk.GetImageFromArray(-distortion_matrix2[0])])
+ out_tx = sitk.DisplacementFieldTransform(displ2)
+ resampler.SetTransform(out_tx)
+ out = resampler.Execute(sitk.GetImageFromArray(image_data))
+ return sitk.GetArrayFromImage(out)
+
+
+[docs]def undistort_stack_sitk(distortion_matrix, image_stack):
+ """
+ use simple ITK to undistort stack of image
+ input:
+ image: numpy array with size NxM
+ distortion_matrix: h5 Dataset or numpy array with size 2 x P x Q
+ with P, Q >= M, N
+ output:
+ image M, N
+
+ """
+
+ resampler = sitk.ResampleImageFilter()
+ resampler.SetReferenceImage(sitk.GetImageFromArray(image_stack[0]))
+ resampler.SetInterpolator(sitk.sitkBSpline)
+ resampler.SetDefaultPixelValue(0)
+
+ displ2 = sitk.Compose(
+ [sitk.GetImageFromArray(-distortion_matrix[1]), sitk.GetImageFromArray(-distortion_matrix[0])])
+ out_tx = sitk.DisplacementFieldTransform(displ2)
+ resampler.SetTransform(out_tx)
+
+ interpolated = np.zeros(image_stack.shape)
+
+ nimages = image_stack.shape[0]
+
+ if QT_available:
+ progress = pyTEMlib.sidpy_tools.ProgressDialog("Correct Scan Distortions", nimages)
+
+ for i in range(nimages):
+ if QT_available:
+ progress.setValue(i)
+ out = resampler.Execute(sitk.GetImageFromArray(image_stack[i]))
+ interpolated[i] = sitk.GetArrayFromImage(out)
+
+ progress.setValue(nimages)
+
+ if QT_available:
+ progress.setValue(nimages)
+
+ return interpolated
+
+
+[docs]def undistort_stack(distortion_matrix, data):
+ """ Undistort stack with distortion matrix
+
+ Use the griddata interpolation of scipy to apply distortion matrix to image
+ The distortion matrix contains in each pixel where the pixel has to be moved (floats)
+
+ Parameters
+ ----------
+ distortion_matrix: numpy array
+ distortion matrix to undistort image (format image.shape[0], image.shape[2], 2)
+ data: numpy array or sidpy.Dataset
+ image
+ """
+
+ corrected = distortion_matrix[:, 2:4]
+ intensity_values = data[:, distortion_matrix[:, 0].astype(int), distortion_matrix[:, 1].astype(int)]
+
+ size_x, size_y = 2 ** np.round(np.log2(data.shape[1:])) # nearest power of 2
+ size_x = int(size_x)
+ size_y = int(size_y)
+
+ grid_x, grid_y = np.mgrid[0:size_x - 1:size_x * 1j, 0:size_y - 1:size_y * 1j]
+ print('interpolate')
+
+ interpolated = np.zeros([data.shape[0], size_x, size_y])
+ nimages = data.shape[0]
+ done = 0
+
+ if QT_available:
+ progress = ft.ProgressDialog("Correct Scan Distortions", nimages)
+ for i in range(nimages):
+ if QT_available:
+ progress.set_value(i)
+ elif done < int((i + 1) / nimages * 50):
+ done = int((i + 1) / nimages * 50)
+ sys.stdout.write('\r')
+ # progress output :
+ sys.stdout.write("[%-50s] %d%%" % ('=' * done, 2 * done))
+ sys.stdout.flush()
+
+ interpolated[i, :, :] = griddata(corrected, intensity_values[i, :], (grid_x, grid_y), method='linear')
+ if QT_available:
+ progress.set_value(nimages)
+ print(':-)')
+ print('You have successfully completed undistortion of image stack')
+ return interpolated
+
+"""
+##################################################################
+# plotting functions for graph_tools
+##################################################################
+
+part of pyTEMlib
+a pycrosccopy package
+
+Author: Gerd Duscher
+First Version: 2022-01-08
+"""
+import numpy as np
+import ase
+
+import plotly.graph_objects as go
+import plotly.express as px
+
+import pyTEMlib.crystal_tools
+import pyTEMlib.graph_tools
+
+
+[docs]def plot_super_cell(super_cell, shift_x=0.):
+ """ make a super_cell to plot with extra atoms at periodic boundaries"""
+
+ if not isinstance(super_cell, ase.Atoms):
+ raise TypeError('Need an ase Atoms object')
+
+ plot_boundary = super_cell * (2, 2, 3)
+ plot_boundary.positions[:, 0] = plot_boundary.positions[:, 0] - super_cell.cell[0, 0] * shift_x
+
+ del plot_boundary[plot_boundary.positions[:, 2] > super_cell.cell[2, 2] * 1.5 + 0.1]
+ del plot_boundary[plot_boundary.positions[:, 1] > super_cell.cell[1, 1] + 0.1]
+ del plot_boundary[plot_boundary.positions[:, 0] > super_cell.cell[0, 0] + 0.1]
+ del plot_boundary[plot_boundary.positions[:, 0] < -0.1]
+ plot_boundary.cell = super_cell.cell * (1, 1, 1.5)
+
+ return plot_boundary
+
+
+[docs]def plot_polyhedron(polyhedra, indices, center=False):
+ """
+ Information to plot polyhedra with plotly
+
+ Parameter
+ ---------
+ polyhedra: dict
+ dictionary of all polyhedra
+ indices: list or integer
+ list or index of polyhedron to plot.
+ center: boolean
+ whether to center polyhedra on origin
+
+ Returns
+ -------
+ data: dict
+ instructions to plot for plotly
+ """
+
+ if isinstance(indices, int):
+ indices = [indices]
+ if len(indices) == 0:
+ print('Did not find any polyhedra')
+ return {}
+
+ center_point = np.mean(polyhedra[indices[0]]['vertices'], axis=0)
+
+ if center:
+ print(center_point)
+ center = center_point
+ else:
+ center = [0, 0, 0]
+
+ data = []
+ for index in indices:
+ polyhedron = polyhedra[index]
+
+ vertices = polyhedron['vertices'] - center
+ faces = np.array(polyhedron['triangles'])
+ x, y, z = vertices.T
+ i_i, j_j, k_k = faces.T
+
+ mesh = dict(type='mesh3d',
+ x=x,
+ y=y,
+ z=z,
+ i=i_i,
+ j=j_j,
+ k=k_k,
+ name='',
+ opacity=0.2,
+ color=px.colors.qualitative.Light24[len(vertices) % 24]
+ )
+ tri_vertices = vertices[faces]
+ x_e = []
+ y_e = []
+ z_e = []
+ for t_v in tri_vertices:
+ x_e += [t_v[k % 3][0] for k in range(4)] + [None]
+ y_e += [t_v[k % 3][1] for k in range(4)] + [None]
+ z_e += [t_v[k % 3][2] for k in range(4)] + [None]
+
+ # define the lines to be plotted
+ lines = dict(type='scatter3d',
+ x=x_e,
+ y=y_e,
+ z=z_e,
+ mode='lines',
+ name='',
+ line=dict(color='rgb(70,70,70)', width=1.5))
+ data.append(mesh)
+ data.append(lines)
+ return data
+
+
+[docs]def plot_bonds(polyhedra):
+ """
+ Information to plot bonds with plotly
+
+ Parameter
+ ---------
+ polyhedra: dict
+ dictionary of all polyhedra
+
+ Returns
+ -------
+ data: dict
+ instructions to plot for plotly
+ """
+ indices = range(len(polyhedra))
+
+ data = []
+ for index in indices:
+ polyhedron = polyhedra[index]
+
+ vertices = polyhedron['vertices']
+ faces = np.array(polyhedron['triangles'])
+ x, y, z = vertices.T
+ i_i, j_j, k_k = faces.T
+
+ tri_vertices = vertices[faces]
+ x_e = []
+ y_e = []
+ z_e = []
+ for t_v in tri_vertices:
+ x_e += [t_v[k % 3][0] for k in range(4)] + [None]
+ y_e += [t_v[k % 3][1] for k in range(4)] + [None]
+ z_e += [t_v[k % 3][2] for k in range(4)] + [None]
+
+ # define the lines to be plotted
+ lines = dict(type='scatter3d',
+ x=x_e,
+ y=y_e,
+ z=z_e,
+ mode='lines',
+ name='',
+ line=dict(color='rgb(70,70,70)', width=1.5))
+ data.append(lines)
+ return data
+
+
+[docs]def get_boundary_polyhedra(polyhedra, boundary_x=0, boundary_width=0.5, verbose=True, z_lim=[0, 100]):
+ """
+ get indices of polyhedra at boundary (assumed to be parallel to x-axis)
+
+ Parameter
+ ---------
+ polyhedra: dict
+ dictionary of all polyhedra
+ boundary_x: float
+ position of boundary in Angstrom
+ boundary_width: float
+ width of boundary where center of polyhedra are considered in Angstrom
+ verbose: boolean
+ optional
+ z_lim: list
+ upper and lower limit of polyhedra to plot
+
+ Returns
+ -------
+ boundary_polyhedra: list
+ list of polyhedra at boundary
+ """
+ boundary_polyhedra = []
+ for key, polyhedron in polyhedra.items():
+ center = polyhedron['vertices'].mean(axis=0)
+ if abs(center[0] - boundary_x) < 0.5 and (z_lim[0] < center[2] < z_lim[1]):
+ boundary_polyhedra.append(key)
+ if verbose:
+ print(key, polyhedron['length'], center)
+
+ return boundary_polyhedra
+
+
+[docs]def plot_with_polyhedra(polyhedra, indices, atoms=None, title=''):
+ """
+ plot atoms and polyhedra with plotly
+
+ Parameter
+ ---------
+ polyhedra: dict
+ dictionary of all polyhedra
+ indices: list or integer
+ list or index of polyhedron to plot.
+ atoms: ase.Atoms
+ optional structure info to plot atoms (with correct color)
+
+ Returns
+ -------
+ fig: plotly.figure
+ plotly figure instance
+ """
+
+ data = plot_polyhedron(polyhedra, indices)
+ if not isinstance(atoms, ase.Atoms):
+ atoms = None
+
+ data[0]['opacity'] = 0.05
+ fig = go.Figure(data=data)
+ if atoms is not None:
+ fig.add_trace(go.Scatter3d(
+ mode='markers',
+ x=atoms.positions[:, 0], y=atoms.positions[:, 1], z=atoms.positions[:, 2],
+ marker=dict(
+ color=atoms.get_atomic_numbers(),
+ size=5,
+ sizemode='diameter',
+ colorscale=["blue", "green", "red"])))
+
+ fig.update_layout(width=1000, height=700, showlegend=False)
+ fig.update_layout(scene_aspectmode='data',
+ scene_aspectratio=dict(x=1, y=1, z=1))
+
+ camera = {'up': {'x': 1, 'y': 0, 'z': 0},
+ 'center': {'x': 0, 'y': 0, 'z': 0},
+ 'eye': {'x': 0, 'y': 0, 'z': 1}}
+
+ fig.update_layout(scene_camera=camera, title=title)
+ fig.update_scenes(camera_projection_type="orthographic")
+ return fig
+
+
+[docs]def plot_supercell(supercell, size=(1, 1, 1), shift_x=0.25, title=''):
+ """
+ plot supercell with plotly
+
+ Parameter
+ ---------
+ supercell: ase.Atoms
+ optional structure info to plot atoms (with correct color)
+ shift_x: float
+ amount of shift in x direction of supercell
+ title: str
+ title of plot
+
+ Returns
+ -------
+ fig: plotly.figure
+ plotly figure instance
+ """
+
+ plot_cell = pyTEMlib.graph_tools.plot_super_cell(supercell * size, shift_x=shift_x)
+
+ # grain_boundary.cell.volume
+ supercell_area = supercell.cell.lengths()[1] / supercell.cell.lengths()[2]
+ print(supercell.symbols)
+ volume__bulk_atom = 16.465237835776012
+ ideal_volume = len(supercell.positions) * volume__bulk_atom
+ print(len(supercell.positions) * volume__bulk_atom, supercell.cell.volume)
+ x_0 = ideal_volume / supercell.cell.lengths()[1] / supercell.cell.lengths()[2]
+ print(f'Zero volume expansion supercell length: {x_0 / 10:.2f} nm; '
+ f' compared to actual {supercell.cell.lengths()[0] / 10:.2f} nm')
+
+ fig = go.Figure(data=[
+ go.Scatter3d(x=plot_cell.positions[:, 0], y=plot_cell.positions[:, 1], z=plot_cell.positions[:, 2],
+ mode='markers',
+ marker=dict(
+ color=plot_cell.get_atomic_numbers(),
+ size=5,
+ sizemode='diameter',
+ colorscale=["blue", "green", "red"]))])
+
+ fig.update_layout(width=700, margin=dict(r=10, l=10, b=10, t=10))
+ fig.update_layout(scene_aspectmode='data',
+ scene_aspectratio=dict(x=1, y=1, z=1))
+
+ camera = dict(
+ up=dict(x=0, y=1, z=0),
+ center=dict(x=0, y=0, z=0),
+ eye=dict(x=0, y=0, z=1)
+ )
+ fig.update_layout(scene_camera=camera, title=title)
+ fig.update_scenes(camera_projection_type="orthographic")
+ return fig
+
+
+[docs]def plot_supercell_bonds(polyhedra, atoms, volumes=None, atom_size=15, title=''):
+ """
+ plot atoms and bonds with plotly
+
+ Parameter
+ ---------
+ polyhedra: dict
+ dictionary of all polyhedra
+ atoms: ase.Atoms
+ optional structure info to plot atoms (with correct color)
+ volumes: list
+ list of volumes, optional structure
+ atoms_size: float
+ sie of atoms to plot
+ title: str
+ title of plot
+
+ Returns
+ -------
+ fig: plotly.figure
+ plotly figure instance
+ """
+
+ data = plot_bonds(polyhedra)
+ if volumes is None:
+ volumes = [atom_size] * len(atoms.get_atomic_numbers())
+
+ fig = go.Figure(data=data)
+ fig.add_trace(go.Scatter3d(
+ mode='markers',
+ x=atoms.positions[:, 0], y=atoms.positions[:, 1], z=atoms.positions[:, 2],
+ marker=dict(
+ color=atoms.get_atomic_numbers(),
+ size=np.asarray(volumes) ** 2 / 10,
+ sizemode='diameter',
+ colorscale=["blue", "green", "red"])))
+
+ fig.update_layout(width=1000, height=700, showlegend=False)
+ fig.update_layout(scene_aspectmode='data',
+ scene_aspectratio=dict(x=1, y=1, z=1))
+
+ camera = {'up': {'x': 0, 'y': 1, 'z': 0},
+ 'center': {'x': 0, 'y': 0, 'z': 0},
+ 'eye': {'x': 0, 'y': 0, 'z': 1}}
+ fig.update_layout(scene_camera=camera, title=title)
+ fig.update_scenes(camera_projection_type="orthographic")
+ return fig
+
+
+[docs]def plot_supercell_polyhedra(polyhedra, indices, atoms, volumes=None, title=''):
+ """
+ plot atoms and polyhedra with plotly
+
+ Parameter
+ ---------
+ polyhedra: dict
+ dictionary of all polyhedra
+ indices: list
+ list of indices of polyhedra to plot
+ atoms: ase.Atoms
+ optional structure info to plot atoms (with correct color)
+ volumes: list
+ list of volumes, optional structure
+ title: str
+ title of plot
+
+ Returns
+ -------
+ fig: plotly.figure
+ plotly figure instance
+ """
+ data = plot_polyhedron(polyhedra, indices)
+ if volumes is None:
+ volumes = [10] * len(atoms.get_atomic_numbers())
+
+ fig = go.Figure(data=data)
+ fig.add_trace(go.Scatter3d(
+ mode='markers',
+ x=atoms.positions[:, 0], y=atoms.positions[:, 1], z=atoms.positions[:, 2],
+ marker=dict(
+ color=atoms.get_atomic_numbers(),
+ size=np.asarray(volumes)**2 / 10,
+ sizemode='diameter',
+ colorscale=["blue", "green", "red"])))
+
+ fig.update_layout(width=1000, height=700, showlegend=False)
+ fig.update_layout(scene_aspectmode='data',
+ scene_aspectratio=dict(x=1, y=1, z=1))
+
+ camera = {'up': {'x': 0, 'y': 1, 'z': 0},
+ 'center': {'x': 0, 'y': 0, 'z': 0},
+ 'eye': {'x': 0, 'y': 0, 'z': 1}}
+ fig.update_layout(scene_camera=camera, title=title)
+ fig.update_scenes(camera_projection_type="orthographic")
+ return fig
+
+
+[docs]def show_polyhedra(polyhedra, boundary_polyhedra, atoms, volumes=None, title=f''):
+ """
+ plot polyhedra and atoms of vertices with plotly
+
+ Parameter
+ ---------
+ polyhedra: dict
+ dictionary of all polyhedra
+ boundary_polyhedra: list
+ list of indices of polyhedra to plot
+ atoms: ase.Atoms
+ optional structure info to plot atoms (with correct color)
+ volumes: list
+ list of volumes, optional structure
+ title: str
+ title of plot
+
+ Returns
+ -------
+ fig: plotly.figure
+ plotly figure instance
+ """
+
+ data = plot_polyhedron(polyhedra, boundary_polyhedra)
+ atom_indices = []
+ for poly in boundary_polyhedra:
+ atom_indices.extend(polyhedra[poly]['indices'])
+ atom_indices = list(set(atom_indices))
+ atomic_numbers = []
+ atomic_volumes = []
+ for atom in atom_indices:
+ atomic_numbers.append(atoms[atom].number)
+ atomic_volumes.append(volumes[atoms[atom].index] ** 2 / 10)
+
+ if volumes is None:
+ atomic_volumes = [10] * len(atoms.get_atomic_numbers())
+ fig = go.Figure(data=data)
+
+ fig.add_trace(go.Scatter3d(
+ mode='markers',
+ x=atoms.positions[atom_indices, 0], y=atoms.positions[atom_indices, 1], z=atoms.positions[atom_indices, 2],
+ marker=dict(
+ color=atomic_numbers,
+ size=atomic_volumes,
+ sizemode='diameter',
+ colorscale=["blue", "green", "red"])))
+
+ fig.update_layout(width=1000, height=700, showlegend=False)
+ fig.update_layout(scene_aspectmode='data',
+ scene_aspectratio=dict(x=1, y=1, z=1))
+
+ camera = {'up': {'x': 1, 'y': 0, 'z': 0},
+ 'center': {'x': 0, 'y': 0, 'z': 0},
+ 'eye': {'x': 0, 'y': 0, 'z': 1}}
+ fig.update_layout(scene_camera=camera, title=title)
+ fig.update_scenes(camera_projection_type="orthographic")
+ return fig
+
+"""
+image_tools.py
+by Gerd Duscher, UTK
+part of pyTEMlib
+MIT license except where stated differently
+"""
+
+import numpy as np
+
+import matplotlib as mpl
+import matplotlib.pylab as plt
+import matplotlib.widgets as mwidgets
+# from matplotlib.widgets import RectangleSelector
+
+import sidpy
+import pyTEMlib.file_tools as ft
+import pyTEMlib.sidpy_tools
+# import pyTEMlib.probe_tools
+
+from tqdm.auto import trange, tqdm
+
+# import itertools
+from itertools import product
+
+from scipy import fftpack
+# from scipy import signal
+from scipy.interpolate import interp1d # , interp2d
+import scipy.optimize as optimization
+
+# Multidimensional Image library
+import scipy.ndimage as ndimage
+import scipy.constants as const
+
+# from scipy.spatial import Voronoi, KDTree, cKDTree
+
+import skimage
+
+import skimage.registration as registration
+# from skimage.feature import register_translation # blob_dog, blob_doh
+from skimage.feature import peak_local_max
+# from skimage.measure import points_in_poly
+
+# our blob detectors from the scipy image package
+from skimage.feature import blob_log # blob_dog, blob_doh
+
+from sklearn.feature_extraction import image
+from sklearn.utils.extmath import randomized_svd
+from sklearn.cluster import DBSCAN
+
+from collections import Counter
+
+
+_SimpleITK_present = True
+try:
+ import SimpleITK as sitk
+except ImportError:
+ sitk = False
+ _SimpleITK_present = False
+
+if not _SimpleITK_present:
+ print('SimpleITK not installed; Registration Functions for Image Stacks not available\n' +
+ 'install with: conda install -c simpleitk simpleitk ')
+
+
+# Wavelength in 1/nm
+[docs]def get_wavelength(e0):
+ """
+ Calculates the relativistic corrected de Broglie wave length of an electron
+
+ Parameters
+ ----------
+ e0: float
+ acceleration voltage in volt
+
+ Returns
+ -------
+ wave length in 1/nm
+ """
+
+ eV = const.e * e0
+ return const.h/np.sqrt(2*const.m_e*eV*(1+eV/(2*const.m_e*const.c**2)))*10**9
+
+
+[docs]def fourier_transform(dset):
+ """
+ Reads information into dictionary 'tags', performs 'FFT', and provides a smoothed FT and reciprocal
+ and intensity limits for visualization.
+
+ Parameters
+ ----------
+ dset: sidpy.Dataset
+ image
+
+ Returns
+ -------
+ fft_dset: sidpy.Dataset
+ Fourier transform with correct dimensions
+
+ Example
+ -------
+ >>> fft_dataset = fourier_transform(sidpy_dataset)
+ >>> fft_dataset.plot()
+ """
+
+ assert isinstance(dset, sidpy.Dataset), 'Expected a sidpy Dataset'
+
+ selection = []
+ image_dim = []
+ # image_dim = get_image_dims(sidpy.DimensionTypes.SPATIAL)
+
+ if dset.data_type == sidpy.DataType.IMAGE_STACK:
+ image_dim = dset.get_image_dims()
+ stack_dim = dset.get_dimensions_by_type('TEMPORAL')
+
+ if len(image_dim) != 2:
+ raise ValueError('need at least two SPATIAL dimension for an image stack')
+
+ for i in range(dset.dims):
+ if i in image_dim:
+ selection.append(slice(None))
+ if len(stack_dim) == 0:
+ stack_dim = i
+ selection.append(slice(None))
+ elif i in stack_dim:
+ stack_dim = i
+ selection.append(slice(None))
+ else:
+ selection.append(slice(0, 1))
+
+ image_stack = np.squeeze(np.array(dset)[selection])
+ new_image = np.sum(np.array(image_stack), axis=stack_dim)
+ elif dset.data_type == sidpy.DataType.IMAGE:
+ new_image = np.array(dset)
+ else:
+ return
+
+ new_image = new_image - new_image.min()
+ fft_transform = (np.fft.fftshift(np.fft.fft2(new_image)))
+
+ image_dims = pyTEMlib.sidpy_tools.get_image_dims(dset)
+
+ units_x = '1/' + dset._axes[image_dims[0]].units
+ units_y = '1/' + dset._axes[image_dims[1]].units
+
+ fft_dset = sidpy.Dataset.from_array(fft_transform)
+ fft_dset.quantity = dset.quantity
+ fft_dset.units = 'a.u.'
+ fft_dset.data_type = 'IMAGE'
+ fft_dset.source = dset.title
+ fft_dset.modality = 'fft'
+
+ fft_dset.set_dimension(0, sidpy.Dimension(np.fft.fftshift(np.fft.fftfreq(new_image.shape[0],
+ d=ft.get_slope(dset.x.values))),
+
+ name='u', units=units_x, dimension_type='RECIPROCAL',
+ quantity='reciprocal_length'))
+ fft_dset.set_dimension(1, sidpy.Dimension(np.fft.fftshift(np.fft.fftfreq(new_image.shape[1],
+ d=ft.get_slope(dset.y.values))),
+ name='v', units=units_y, dimension_type='RECIPROCAL',
+ quantity='reciprocal_length'))
+
+ return fft_dset
+
+
+[docs]def power_spectrum(dset, smoothing=3):
+ """
+ Calculate power spectrum
+
+ Parameters
+ ----------
+ dset: sidpy.Dataset
+ image
+ smoothing: int
+ Gaussian smoothing
+
+ Returns
+ -------
+ power_spec: sidpy.Dataset
+ power spectrum with correct dimensions
+
+ """
+
+ fft_transform = fourier_transform(dset) # dset.fft()
+ fft_mag = np.abs(fft_transform)
+ fft_mag2 = ndimage.gaussian_filter(fft_mag, sigma=(smoothing, smoothing), order=0)
+
+ power_spec = fft_transform.like_data(np.log(1.+fft_mag2))
+
+ # prepare mask
+ x, y = np.meshgrid(power_spec.v.values, power_spec.u.values)
+ mask = np.zeros(power_spec.shape)
+
+ mask_spot = x ** 2 + y ** 2 > 1 ** 2
+ mask = mask + mask_spot
+ mask_spot = x ** 2 + y ** 2 < 11 ** 2
+ mask = mask + mask_spot
+
+ mask[np.where(mask == 1)] = 0 # just in case of overlapping disks
+
+ minimum_intensity = np.array(power_spec)[np.where(mask == 2)].min() * 0.95
+ maximum_intensity = np.array(power_spec)[np.where(mask == 2)].max() * 1.05
+ power_spec.metadata = {'fft': {'smoothing': smoothing,
+ 'minimum_intensity': minimum_intensity, 'maximum_intensity': maximum_intensity}}
+ power_spec.title = 'power spectrum ' + power_spec.source
+
+ return power_spec
+
+
+[docs]def diffractogram_spots(dset, spot_threshold, return_center = True, eps = 0.1):
+ """Find spots in diffractogram and sort them by distance from center
+
+ Uses blob_log from scipy.spatial
+
+ Parameters
+ ----------
+ dset: sidpy.Dataset
+ diffractogram
+ spot_threshold: float
+ threshold for blob finder
+
+ Returns
+ -------
+ spots: numpy array
+ sorted position (x,y) and radius (r) of all spots
+ """
+
+ # spot detection (for future reference there is no symmetry assumed here)
+ data = np.array(np.log(1+np.abs(dset)))
+ data = data - data.min()
+ data = data/data.max()
+ # some images are strange and blob_log does not work on the power spectrum
+ try:
+ spots_random = blob_log(data, max_sigma=5, threshold=spot_threshold)
+ except ValueError:
+ spots_random = peak_local_max(np.array(data.T), min_distance=3, threshold_rel=spot_threshold)
+ spots_random = np.hstack(spots_random, np.zeros((spots_random.shape[0], 1)))
+
+ print(f'Found {spots_random.shape[0]} reflections')
+
+ # Needed for conversion from pixel to Reciprocal space
+ rec_scale = np.array([ft.get_slope(dset.u.values), ft.get_slope(dset.v.values)])
+ spots_random[:, :2] = spots_random[:, :2]*rec_scale+[dset.u.values[0], dset.v.values[0]]
+ # sort reflections
+ spots_random[:, 2] = np.linalg.norm(spots_random[:, 0:2], axis=1)
+ spots_index = np.argsort(spots_random[:, 2])
+ spots = spots_random[spots_index]
+ # third row is angles
+ spots[:, 2] = np.arctan2(spots[:, 0], spots[:, 1])
+
+ if return_center == True:
+ points = spots[:, 0:2]
+
+ # Calculate the midpoints between all points
+ reshaped_points = points[:, np.newaxis, :]
+ midpoints = (reshaped_points + reshaped_points.transpose(1, 0, 2)) / 2.0
+ midpoints = midpoints.reshape(-1, 2)
+
+ # Find the most dense cluster of midpoints
+ dbscan = DBSCAN(eps = eps, min_samples = 2)
+ labels = dbscan.fit_predict(midpoints)
+ cluster_counter = Counter(labels)
+ largest_cluster_label = max(cluster_counter, key=cluster_counter.get)
+ largest_cluster_points = midpoints[labels == largest_cluster_label]
+
+ # Average of these midpoints must be the center
+ center = np.mean(largest_cluster_points,axis=0)
+
+ return spots, center
+
+
+[docs]def adaptive_fourier_filter(dset, spots, low_pass=3, reflection_radius=0.3):
+ """
+ Use spots in diffractogram for a Fourier Filter
+
+ Parameters:
+ -----------
+ dset: sidpy.Dataset
+ image to be filtered
+ spots: np.ndarray(N,2)
+ sorted spots in diffractogram in 1/nm
+ low_pass: float
+ low pass filter in center of diffractogram in 1/nm
+ reflection_radius: float
+ radius of masked reflections in 1/nm
+
+ Output:
+ -------
+ Fourier filtered image
+ """
+
+ if not isinstance(dset, sidpy.Dataset):
+ raise TypeError('We need a sidpy.Dataset')
+ fft_transform = fourier_transform(dset)
+
+ # prepare mask
+ x, y = np.meshgrid(fft_transform.v.values, fft_transform.u.values)
+ mask = np.zeros(dset.shape)
+
+ # mask reflections
+ for spot in spots:
+ mask_spot = (x - spot[1]) ** 2 + (y - spot[0]) ** 2 < reflection_radius ** 2 # make a spot
+ mask = mask + mask_spot # add spot to mask
+
+ # mask zero region larger (low-pass filter = intensity variations)
+ mask_spot = x ** 2 + y ** 2 < low_pass ** 2
+ mask = mask + mask_spot
+ mask[np.where(mask > 1)] = 1
+ fft_filtered = np.array(fft_transform * mask)
+
+ filtered_image = dset.like_data(np.fft.ifft2(np.fft.fftshift(fft_filtered)).real)
+ filtered_image.title = 'Fourier filtered ' + dset.title
+ filtered_image.source = dset.title
+ filtered_image.metadata = {'analysis': 'adaptive fourier filtered', 'spots': spots,
+ 'low_pass': low_pass, 'reflection_radius': reflection_radius}
+ return filtered_image
+
+
+[docs]def rotational_symmetry_diffractogram(spots):
+ """ Test rotational symmetry of diffraction spots"""
+
+ rotation_symmetry = []
+ for n in [2, 3, 4, 6]:
+ cc = np.array(
+ [[np.cos(2 * np.pi / n), np.sin(2 * np.pi / n), 0], [-np.sin(2 * np.pi / n), np.cos(2 * np.pi / n), 0],
+ [0, 0, 1]])
+ sym_spots = np.dot(spots, cc)
+ dif = []
+ for p0, p1 in product(sym_spots[:, 0:2], spots[:, 0:2]):
+ dif.append(np.linalg.norm(p0 - p1))
+ dif = np.array(sorted(dif))
+
+ if dif[int(spots.shape[0] * .7)] < 0.2:
+ rotation_symmetry.append(n)
+ return rotation_symmetry
+
+#####################################################
+# Registration Functions
+#####################################################
+
+
+[docs]def complete_registration(main_dataset, storage_channel=None):
+ """Rigid and then non-rigid (demon) registration
+
+ Performs rigid and then non-rigid registration, please see individual functions:
+ - rigid_registration
+ - demon_registration
+
+ Parameters
+ ----------
+ main_dataset: sidpy.Dataset
+ dataset of data_type 'IMAGE_STACK' to be registered
+ storage_channel: h5py.Group
+ optional - location in hdf5 file to store datasets
+
+ Returns
+ -------
+ non_rigid_registered: sidpy.Dataset
+ rigid_registered_dataset: sidpy.Dataset
+
+ """
+
+ if not isinstance(main_dataset, sidpy.Dataset):
+ raise TypeError('We need a sidpy.Dataset')
+ if main_dataset.data_type.name != 'IMAGE_STACK':
+ raise TypeError('Registration makes only sense for an image stack')
+
+ print('Rigid_Registration')
+
+ rigid_registered_dataset = rigid_registration(main_dataset)
+ if storage_channel is None:
+ storage_channel = main_dataset.h5_dataset.parent.parent
+
+ registration_channel = ft.log_results(storage_channel, rigid_registered_dataset)
+
+ print('Non-Rigid_Registration')
+
+ non_rigid_registered = demon_registration(rigid_registered_dataset)
+ registration_channel = ft.log_results(storage_channel, non_rigid_registered)
+
+ return non_rigid_registered, rigid_registered_dataset
+
+
+[docs]def demon_registration(dataset, verbose=False):
+ """
+ Diffeomorphic Demon Non-Rigid Registration
+
+ Depends on:
+ simpleITK and numpy
+ Please Cite: http://www.simpleitk.org/SimpleITK/project/parti.html
+ and T. Vercauteren, X. Pennec, A. Perchant and N. Ayache
+ Diffeomorphic Demons Using ITK\'s Finite Difference Solver Hierarchy
+ The Insight Journal, http://hdl.handle.net/1926/510 2007
+
+ Parameters
+ ----------
+ dataset: sidpy.Dataset
+ stack of image after rigid registration and cropping
+ verbose: boolean
+ optional for increased output
+ Returns
+ -------
+ dem_reg: stack of images with non-rigid registration
+
+ Example
+ -------
+ dem_reg = demon_reg(stack_dataset, verbose=False)
+ """
+
+ if not isinstance(dataset, sidpy.Dataset):
+ raise TypeError('We need a sidpy.Dataset')
+ if dataset.data_type.name != 'IMAGE_STACK':
+ raise TypeError('Registration makes only sense for an image stack')
+
+ dem_reg = np.zeros(dataset.shape)
+ nimages = dataset.shape[0]
+ if verbose:
+ print(nimages)
+ # create fixed image by summing over rigid registration
+
+ fixed_np = np.average(np.array(dataset), axis=0)
+
+ if not _SimpleITK_present:
+ print('This feature is not available: \n Please install simpleITK with: conda install simpleitk -c simpleitk')
+
+ fixed = sitk.GetImageFromArray(fixed_np)
+ fixed = sitk.DiscreteGaussian(fixed, 2.0)
+
+ # demons = sitk.SymmetricForcesDemonsRegistrationFilter()
+ demons = sitk.DiffeomorphicDemonsRegistrationFilter()
+
+ demons.SetNumberOfIterations(200)
+ demons.SetStandardDeviations(1.0)
+
+ resampler = sitk.ResampleImageFilter()
+ resampler.SetReferenceImage(fixed)
+ resampler.SetInterpolator(sitk.sitkBSpline)
+ resampler.SetDefaultPixelValue(0)
+
+ done = 0
+
+ for i in trange(nimages):
+
+ moving = sitk.GetImageFromArray(dataset[i])
+ moving_f = sitk.DiscreteGaussian(moving, 2.0)
+ displacement_field = demons.Execute(fixed, moving_f)
+ out_tx = sitk.DisplacementFieldTransform(displacement_field)
+ resampler.SetTransform(out_tx)
+ out = resampler.Execute(moving)
+ dem_reg[i, :, :] = sitk.GetArrayFromImage(out)
+
+ print(':-)')
+ print('You have successfully completed Diffeomorphic Demons Registration')
+
+ demon_registered = dataset.like_data(dem_reg)
+ demon_registered.title = 'Non-Rigid Registration'
+ demon_registered.source = dataset.title
+
+ demon_registered.metadata = {'analysis': 'non-rigid demon registration'}
+ if 'input_crop' in dataset.metadata:
+ demon_registered.metadata['input_crop'] = dataset.metadata['input_crop']
+ if 'input_shape' in dataset.metadata:
+ demon_registered.metadata['input_shape'] = dataset.metadata['input_shape']
+ demon_registered.metadata['input_dataset'] = dataset.source
+ return demon_registered
+
+
+###############################
+# Rigid Registration New 05/09/2020
+
+[docs]def rigid_registration(dataset):
+ """
+ Rigid registration of image stack with pixel accuracy
+
+ Uses simple cross_correlation
+ (we determine drift from one image to next)
+
+ Parameters
+ ----------
+ dataset: sidpy.Dataset
+ sidpy dataset with image_stack dataset
+
+ Returns
+ -------
+ rigid_registered: sidpy.Dataset
+ Registered Stack and drift (with respect to center image)
+ """
+
+ if not isinstance(dataset, sidpy.Dataset):
+ raise TypeError('We need a sidpy.Dataset')
+ if dataset.data_type.name != 'IMAGE_STACK':
+ raise TypeError('Registration makes only sense for an image stack')
+
+ frame_dim = []
+ spatial_dim = []
+ selection = []
+
+ for i, axis in dataset._axes.items():
+ if axis.dimension_type.name == 'SPATIAL':
+ spatial_dim.append(i)
+ selection.append(slice(None))
+ else:
+ frame_dim.append(i)
+ selection.append(slice(0, 1))
+
+ if len(spatial_dim) != 2:
+ print('need two spatial dimensions')
+ if len(frame_dim) != 1:
+ print('need one frame dimensions')
+
+ nopix = dataset.shape[spatial_dim[0]]
+ nopiy = dataset.shape[spatial_dim[1]]
+ nimages = dataset.shape[frame_dim[0]]
+
+ print('Stack contains ', nimages, ' images, each with', nopix, ' pixels in x-direction and ', nopiy,
+ ' pixels in y-direction')
+
+ fixed = dataset[tuple(selection)].squeeze().compute()
+ fft_fixed = np.fft.fft2(fixed)
+
+ relative_drift = [[0., 0.]]
+
+ for i in trange(nimages):
+ selection[frame_dim[0]] = slice(i, i+1)
+ moving = dataset[tuple(selection)].squeeze().compute()
+ fft_moving = np.fft.fft2(moving)
+ image_product = fft_fixed * fft_moving.conj()
+ cc_image = np.fft.fftshift(np.fft.ifft2(image_product))
+ shift =np.array(ndimage.maximum_position(cc_image.real))-cc_image.shape[0]/2
+ fft_fixed = fft_moving
+ relative_drift.append(shift)
+ rig_reg, drift = rig_reg_drift(dataset, relative_drift)
+
+
+ crop_reg, input_crop = crop_image_stack(rig_reg, drift)
+
+ rigid_registered = dataset.like_data(crop_reg)
+ rigid_registered.title = 'Rigid Registration'
+ rigid_registered.source = dataset.title
+ rigid_registered.metadata = {'analysis': 'rigid sub-pixel registration', 'drift': drift,
+ 'input_crop': input_crop, 'input_shape': dataset.shape[1:]}
+
+ # if hasattr(rigid_registered, 'z'):
+ # del rigid_registered.z
+ # if hasattr(rigid_registered, 'x'):
+ # del rigid_registered.x
+ # if hasattr(rigid_registered, 'y'):
+ # del rigid_registered.y
+
+
+ # rigid_registered._axes = {}
+ rigid_registered.set_dimension(0, dataset._axes[frame_dim[0]])
+ rigid_registered.set_dimension(1, dataset._axes[spatial_dim[0]][input_crop[0]:input_crop[1]])
+ rigid_registered.set_dimension(2, dataset._axes[spatial_dim[1]][input_crop[2]:input_crop[3]])
+ return rigid_registered.rechunk({0: 'auto', 1: -1, 2: -1})
+
+[docs]def rig_reg_drift(dset, rel_drift):
+ """ Shifting images on top of each other
+
+ Uses relative drift to shift images on top of each other,
+ with center image as reference.
+ Shifting is done with shift routine of ndimage from scipy.
+ This function is used by rigid_registration routine
+
+ Parameters
+ ----------
+ dset: sidpy.Dataset
+ dataset with image_stack
+ rel_drift:
+ relative_drift from image to image as list of [shiftx, shifty]
+
+ Returns
+ -------
+ stack: numpy array
+ drift: list of drift in pixel
+ """
+
+ frame_dim = []
+ spatial_dim = []
+ selection = []
+
+ for i, axis in dset._axes.items():
+ if axis.dimension_type.name == 'SPATIAL':
+ spatial_dim.append(i)
+ selection.append(slice(None))
+ else:
+ frame_dim.append(i)
+ selection.append(slice(0, 1))
+
+ if len(spatial_dim) != 2:
+ print('need two spatial dimensions')
+ if len(frame_dim) != 1:
+ print('need one frame dimensions')
+
+ rig_reg = np.zeros([dset.shape[frame_dim[0]], dset.shape[spatial_dim[0]], dset.shape[spatial_dim[1]]])
+
+ # absolute drift
+ drift = np.array(rel_drift).copy()
+
+ drift[0] = [0, 0]
+ for i in range(drift.shape[0]):
+ drift[i] = drift[i - 1] + rel_drift[i]
+ center_drift = drift[int(drift.shape[0] / 2)]
+ drift = drift - center_drift
+ # Shift images
+ for i in range(rig_reg.shape[0]):
+ selection[frame_dim[0]] = slice(i, i+1)
+ # Now we shift
+ rig_reg[i, :, :] = ndimage.shift(dset[tuple(selection)].squeeze().compute(), [drift[i, 0], drift[i, 1]], order=3)
+ return rig_reg, drift
+
+
+[docs]def crop_image_stack(rig_reg, drift):
+ """Crop images in stack according to drift
+
+ This function is used by rigid_registration routine
+
+ Parameters
+ ----------
+ rig_reg: numpy array (N,x,y)
+ drift: list (2,B)
+
+ Returns
+ -------
+ numpy array
+ """
+
+ xpmin = int(-np.floor(np.min(np.array(drift)[:, 0])))
+ xpmax = int(rig_reg.shape[1] - np.ceil(np.max(np.array(drift)[:, 0])))
+ ypmin = int(-np.floor(np.min(np.array(drift)[:, 1])))
+ ypmax = int(rig_reg.shape[2] - np.ceil(np.max(np.array(drift)[:, 1])))
+
+ return rig_reg[:, xpmin:xpmax, ypmin:ypmax], [xpmin, xpmax, ypmin, ypmax]
+
+[docs]class ImageWithLineProfile:
+ """Image with line profile"""
+
+ def __init__(self, data, extent, title=''):
+ fig, ax = plt.subplots(1, 1)
+ self.figure = fig
+ self.title = title
+ self.line_plot = False
+ self.ax = ax
+ self.data = data
+ self.extent = extent
+ self.ax.imshow(data, extent=extent)
+ self.ax.set_title(title)
+ self.line, = self.ax.plot([0], [0], color='orange') # empty line
+ self.end_x = self.line.get_xdata()
+ self.end_y = self.line.get_ydata()
+ self.cid = self.line.figure.canvas.mpl_connect('button_press_event', self)
+
+[docs] def __call__(self, event):
+ if event.inaxes != self.line.axes:
+ return
+ self.start_x = self.end_x
+ self.start_y = self.end_y
+
+ self.line.set_data([self.start_x, event.xdata], [self.start_y, event.ydata])
+ self.line.figure.canvas.draw()
+
+ self.end_x = event.xdata
+ self.end_y = event.ydata
+
+ self.update()
+
+ def update(self):
+ if not self.line_plot:
+ self.line_plot = True
+ self.figure.clear()
+ self.ax = self.figure.subplots(2, 1)
+ self.ax[0].imshow(self.data, extent=self.extent)
+ self.ax[0].set_title(self.title)
+
+ self.line, = self.ax[0].plot([0], [0], color='orange') # empty line
+ self.line_plot, = self.ax[1].plot([], [], color='orange')
+ self.ax[1].set_xlabel('distance [nm]')
+
+ x0 = self.start_x
+ x1 = self.end_x
+ y0 = self.start_y
+ y1 = self.end_y
+ length_plot = np.sqrt((x1-x0)**2+(y1-y0)**2)
+
+ num = length_plot*(self.data.shape[0]/self.extent[1])
+ x = np.linspace(x0, x1, num)*(self.data.shape[0]/self.extent[1])
+ y = np.linspace(y0, y1, num)*(self.data.shape[0]/self.extent[1])
+
+ # Extract the values along the line, using cubic interpolation
+ zi2 = ndimage.map_coordinates(self.data.T, np.vstack((x, y)))
+
+ x_axis = np.linspace(0, length_plot, len(zi2))
+ self.x = x_axis
+ self.z = zi2
+
+ self.line_plot.set_xdata(x_axis)
+ self.line_plot.set_ydata(zi2)
+ self.ax[1].set_xlim(0, x_axis.max())
+ self.ax[1].set_ylim(zi2.min(), zi2.max())
+ self.ax[1].draw()
+
+
+[docs]def histogram_plot(image_tags):
+ """interactive histogram"""
+ nbins = 75
+ color_map_list = ['gray', 'viridis', 'jet', 'hot']
+ if 'minimum_intensity' not in image_tags:
+ image_tags['minimum_intensity'] = image_tags['plotimage'].min()
+ minimum_intensity = image_tags['minimum_intensity']
+ if 'maximum_intensity' not in image_tags:
+ image_tags['maximum_intensity'] = image_tags['plotimage'].max()
+ data = image_tags['plotimage']
+ vmin = image_tags['minimum_intensity']
+ vmax = image_tags['maximum_intensity']
+ if 'color_map' not in image_tags:
+ image_tags['color_map'] = color_map_list[0]
+ cmap = plt.cm.get_cmap(image_tags['color_map'])
+
+ colors = cmap(np.linspace(0., 1., nbins))
+
+ norm2 = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
+ hist, bin_edges = np.histogram(data, np.linspace(vmin, vmax, nbins), density=True)
+
+ width = bin_edges[1]-bin_edges[0]
+
+ def onselect(vmin, vmax):
+ ax1.clear()
+ cmap = plt.cm.get_cmap(image_tags['color_map'])
+
+ colors = cmap(np.linspace(0., 1., nbins))
+
+ norm2 = mpl.colors.Normalize(vmin=vmin, vmax=vmax)
+ hist2, bin_edges2 = np.histogram(data, np.linspace(vmin, vmax, nbins), density=True)
+
+ width2 = (bin_edges2[1]-bin_edges2[0])
+
+ for i in range(nbins-1):
+ histogram[i].xy = (bin_edges2[i], 0)
+ histogram[i].set_height(hist2[i])
+ histogram[i].set_width(width2)
+ histogram[i].set_facecolor(colors[i])
+ ax.set_xlim(vmin, vmax)
+ ax.set_ylim(0, hist2.max()*1.01)
+
+ cb1 = mpl.colorbar.ColorbarBase(ax1, cmap=cmap, norm=norm2, orientation='horizontal')
+
+ image_tags['minimum_intensity'] = vmin
+ image_tags['maximum_intensity'] = vmax
+
+ def onclick(event):
+ global event2
+ event2 = event
+ print('%s click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f' %
+ ('double' if event.dblclick else 'single', event.button,
+ event.x, event.y, event.xdata, event.ydata))
+ if event.inaxes == ax1:
+ if event.button == 3:
+ ind = color_map_list.index(image_tags['color_map'])+1
+ if ind == len(color_map_list):
+ ind = 0
+ image_tags['color_map'] = color_map_list[ind] # 'viridis'
+ vmin = image_tags['minimum_intensity']
+ vmax = image_tags['maximum_intensity']
+ else:
+ vmax = data.max()
+ vmin = data.min()
+ onselect(vmin, vmax)
+
+ fig2 = plt.figure()
+
+ ax = fig2.add_axes([0., 0.2, 0.9, 0.7])
+ ax1 = fig2.add_axes([0., 0.15, 0.9, 0.05])
+
+ histogram = ax.bar(bin_edges[0:-1], hist, width=width, color=colors, edgecolor='black', alpha=0.8)
+ onselect(vmin, vmax)
+ cb1 = mpl.colorbar.ColorbarBase(ax1, cmap=cmap, norm=norm2, orientation='horizontal')
+
+ rectprops = dict(facecolor='blue', alpha=0.5)
+
+ span = mwidgets.SpanSelector(ax, onselect, 'horizontal', rectprops=rectprops)
+
+ cid = fig2.canvas.mpl_connect('button_press_event', onclick)
+ return span
+
+
+[docs]def clean_svd(im, pixel_size=1, source_size=5):
+ """De-noising of image by using first component of single value decomposition"""
+ patch_size = int(source_size/pixel_size)
+ if patch_size < 3:
+ patch_size = 3
+ patches = image.extract_patches_2d(im, (patch_size, patch_size))
+ patches = patches.reshape(patches.shape[0], patches.shape[1]*patches.shape[2])
+
+ num_components = 32
+
+ u, s, v = randomized_svd(patches, num_components)
+ u_im_size = int(np.sqrt(u.shape[0]))
+ reduced_image = u[:, 0].reshape(u_im_size, u_im_size)
+ reduced_image = reduced_image/reduced_image.sum()*im.sum()
+ return reduced_image
+
+
+[docs]def rebin(im, binning=2):
+ """
+ rebin an image by the number of pixels in x and y direction given by binning
+
+ Parameter
+ ---------
+ image: numpy array in 2 dimensions
+
+ Returns
+ -------
+ binned image as numpy array
+ """
+ if len(im.shape) == 2:
+ return im.reshape((im.shape[0]//binning, binning, im.shape[1]//binning, binning)).mean(axis=3).mean(1)
+ else:
+ raise TypeError('not a 2D image')
+
+
+[docs]def cart2pol(points):
+ """Cartesian to polar coordinate conversion
+
+ Parameters
+ ---------
+ points: float or numpy array
+ points to be converted (Nx2)
+
+ Returns
+ -------
+ rho: float or numpy array
+ distance
+ phi: float or numpy array
+ angle
+ """
+
+ rho = np.linalg.norm(points[:, 0:2], axis=1)
+ phi = np.arctan2(points[:, 1], points[:, 0])
+
+ return rho, phi
+
+
+[docs]def pol2cart(rho, phi):
+ """Polar to Cartesian coordinate conversion
+
+ Parameters
+ ----------
+ rho: float or numpy array
+ distance
+ phi: float or numpy array
+ angle
+
+ Returns
+ -------
+ x: float or numpy array
+ x coordinates of converted points(Nx2)
+ """
+
+ x = rho * np.cos(phi)
+ y = rho * np.sin(phi)
+ return x, y
+
+
+[docs]def xy2polar(points, rounding=1e-3):
+ """ Conversion from carthesian to polar coordinates
+
+ the angles and distances are sorted by r and then phi
+ The indices of this sort is also returned
+
+ Parameters
+ ----------
+ points: numpy array
+ number of points in axis 0 first two elements in axis 1 are x and y
+ rounding: int
+ optional rounding in significant digits
+
+ Returns
+ -------
+ r, phi, sorted_indices
+ """
+
+ r, phi = cart2pol(points)
+
+ phi = phi # %np.pi # only positive angles
+ r = (np.floor(r/rounding))*rounding # Remove rounding error differences
+
+ sorted_indices = np.lexsort((phi, r)) # sort first by r and then by phi
+ r = r[sorted_indices]
+ phi = phi[sorted_indices]
+
+ return r, phi, sorted_indices
+
+
+[docs]def cartesian2polar(x, y, grid, r, t, order=3):
+ """Transform cartesian grid to polar grid
+
+ Used by warp
+ """
+
+ rr, tt = np.meshgrid(r, t)
+
+ new_x = rr*np.cos(tt)
+ new_y = rr*np.sin(tt)
+
+ ix = interp1d(x, np.arange(len(x)))
+ iy = interp1d(y, np.arange(len(y)))
+
+ new_ix = ix(new_x.ravel())
+ new_iy = iy(new_y.ravel())
+
+ return ndimage.map_coordinates(grid, np.array([new_ix, new_iy]), order=order).reshape(new_x.shape)
+
+
+[docs]def warp(diff):
+ """Takes a centered diffraction pattern (as a sidpy dataset)and warps it to a polar grid"""
+ """Centered diff can be produced with it.diffractogram_spots(return_center = True)"""
+
+ # Define original polar grid
+ nx = np.shape(diff)[0]
+ ny = np.shape(diff)[1]
+
+ # Define center pixel
+ pix2nm = np.gradient(diff.u.values)[0]
+ center_pixel = [abs(min(diff.u.values)), abs(min(diff.v.values))]//pix2nm
+
+ x = np.linspace(1, nx, nx, endpoint = True)-center_pixel[0]
+ y = np.linspace(1, ny, ny, endpoint = True)-center_pixel[1]
+ z = diff
+
+ # Define new polar grid
+ nr = int(min([center_pixel[0], center_pixel[1], diff.shape[0]-center_pixel[0], diff.shape[1]-center_pixel[1]])-1)
+ nt = 360*3
+
+ r = np.linspace(1, nr, nr)
+ t = np.linspace(0., np.pi, nt, endpoint = False)
+
+ return cartesian2polar(x,y, z, r, t, order=3)
+
+
+[docs]def calculate_ctf(wavelength, cs, defocus, k):
+ """ Calculate Contrast Transfer Function
+
+ everything in nm
+
+ Parameters
+ ----------
+ wavelength: float
+ deBroglie wavelength of electrons
+ cs: float
+ spherical aberration coefficient
+ defocus: float
+ defocus
+ k: numpy array
+ reciprocal scale
+
+ Returns
+ -------
+ ctf: numpy array
+ contrast transfer function
+
+ """
+ ctf = np.sin(np.pi*defocus*wavelength*k**2+0.5*np.pi*cs*wavelength**3*k**4)
+ return ctf
+
+
+[docs]def calculate_scherzer(wavelength, cs):
+ """
+ Calculate the Scherzer defocus. Cs is in mm, lambda is in nm
+
+ # Input and output in nm
+ """
+
+ scherzer = -1.155*(cs*wavelength)**0.5 # in m
+ return scherzer
+
+
+[docs]def get_rotation(experiment_spots, crystal_spots):
+ """Get rotation by comparing spots in diffractogram to diffraction Bragg spots
+
+ Parameter
+ ---------
+ experiment_spots: numpy array (nx2)
+ positions (in 1/nm) of spots in diffractogram
+ crystal_spots: numpy array (nx2)
+ positions (in 1/nm) of Bragg spots according to kinematic scattering theory
+
+ """
+
+ r_experiment, phi_experiment = cart2pol(experiment_spots)
+
+ # get crystal spots of same length and sort them by angle as well
+ r_crystal, phi_crystal, crystal_indices = xy2polar(crystal_spots)
+ angle_index = np.argmin(np.abs(r_experiment-r_crystal[1]) )
+ rotation_angle = phi_experiment[angle_index]%(2*np.pi) - phi_crystal[1]
+ print(phi_experiment[angle_index])
+ st = np.sin(rotation_angle)
+ ct = np.cos(rotation_angle)
+ rotation_matrix = np.array([[ct, -st], [st, ct]])
+
+ return rotation_matrix, rotation_angle
+
+
+
+[docs]def calibrate_image_scale(fft_tags, spots_reference, spots_experiment):
+ """depreciated get change of scale from comparison of spots to Bragg angles """
+ gx = fft_tags['spatial_scale_x']
+ gy = fft_tags['spatial_scale_y']
+
+ dist_reference = np.linalg.norm(spots_reference, axis=1)
+ distance_experiment = np.linalg.norm(spots_experiment, axis=1)
+
+ first_reflections = abs(distance_experiment - dist_reference.min()) < .2
+ print('Evaluate ', first_reflections.sum(), 'reflections')
+ closest_exp_reflections = spots_experiment[first_reflections]
+
+ def func(params, xdata, ydata):
+ dgx, dgy = params
+ return np.sqrt((xdata * dgx) ** 2 + (ydata * dgy) ** 2) - dist_reference.min()
+
+ x0 = [1.001, 0.999]
+ [dg, sig] = optimization.leastsq(func, x0, args=(closest_exp_reflections[:, 0], closest_exp_reflections[:, 1]))
+ return dg
+
+
+
+[docs]def align_crystal_reflections(spots, crystals):
+ """ Depreciated - use diffraction spots"""
+
+ crystal_reflections_polar = []
+ angles = []
+ exp_r, exp_phi = cart2pol(spots) # just in polar coordinates
+ spots_polar = np.array([exp_r, exp_phi])
+
+ for i in range(len(crystals)):
+ tags = crystals[i]
+ r, phi, indices = xy2polar(tags['allowed']['g']) # sorted by r and phi , only positive angles
+ # we mask the experimental values that are found already
+ angle = 0.
+
+ angle_i = np.argmin(np.abs(exp_r - r[1]))
+ angle = exp_phi[angle_i] - phi[0]
+ angles.append(angle) # Determine rotation angle
+
+ crystal_reflections_polar.append([r, angle + phi, indices])
+ tags['allowed']['g_rotated'] = pol2cart(r, angle + phi)
+ for spot in tags['allowed']['g']:
+ dif = np.linalg.norm(spots[:, 0:2]-spot[0:2], axis=1)
+ # print(dif.min())
+ if dif.min() < 1.5:
+ ind = np.argmin(dif)
+
+ return crystal_reflections_polar, angles
+
+
+# Deconvolution
+[docs]def decon_lr(o_image, probe, verbose=False):
+ """
+ # This task generates a restored image from an input image and point spread function (PSF) using
+ # the algorithm developed independently by Lucy (1974, Astron. J. 79, 745) and Richardson
+ # (1972, J. Opt. Soc. Am. 62, 55) and adapted for HST imagery by Snyder
+ # (1990, in Restoration of HST Images and Spectra, ST ScI Workshop Proceedings; see also
+ # Snyder, Hammoud, & White, JOSA, v. 10, no. 5, May 1993, in press).
+ # Additional options developed by Rick White (STScI) are also included.
+ #
+ # The Lucy-Richardson method can be derived from the maximum likelihood expression for data
+ # with a Poisson noise distribution. Thus, it naturally applies to optical imaging data such as HST.
+ # The method forces the restored image to be positive, in accord with photon-counting statistics.
+ #
+ # The Lucy-Richardson algorithm generates a restored image through an iterative method. The essence
+ # of the iteration is as follows: the (n+1)th estimate of the restored image is given by the nth estimate
+ # of the restored image multiplied by a correction image. That is,
+ #
+ # original data
+ # image = image --------------- * reflect(PSF)
+ # n+1 n image * PSF
+ # n
+
+ # where the *'s represent convolution operators and reflect(PSF) is the reflection of the PSF, i.e.
+ # reflect((PSF)(x,y)) = PSF(-x,-y). When the convolutions are carried out using fast Fourier transforms
+ # (FFTs), one can use the fact that FFT(reflect(PSF)) = conj(FFT(PSF)), where conj is the complex conjugate
+ # operator.
+ """
+
+ if len(o_image) < 1:
+ return o_image
+
+ if o_image.shape != probe.shape:
+ print('Weirdness ', o_image.shape, ' != ', probe.shape)
+
+ probe_c = np.ones(probe.shape, dtype=np.complex64)
+ probe_c.real = probe
+
+ error = np.ones(o_image.shape, dtype=np.complex64)
+ est = np.ones(o_image.shape, dtype=np.complex64)
+ source = np.ones(o_image.shape, dtype=np.complex64)
+ source.real = o_image
+
+ response_ft = fftpack.fft2(probe_c)
+
+ ap_angle = o_image.metadata['experiment']['convergence_angle'] / 1000.0 # now in rad
+
+ e0 = float(o_image.metadata['experiment']['acceleration_voltage'])
+
+ wl = get_wavelength(e0)
+ o_image.metadata['experiment']['wavelength'] = wl
+
+ over_d = 2 * ap_angle / wl
+
+ dx = o_image.x[1]-o_image.x[0]
+ dk = 1.0 / float(o_image.x[-1]) # last value of x-axis is field of view
+ screen_width = 1 / dx
+
+ aperture = np.ones(o_image.shape, dtype=np.complex64)
+ # Mask for the aperture before the Fourier transform
+ n = o_image.shape[0]
+ size_x = o_image.shape[0]
+ size_y = o_image.shape[1]
+ app_ratio = over_d / screen_width * n
+
+ theta_x = np.array(-size_x / 2. + np.arange(size_x))
+ theta_y = np.array(-size_y / 2. + np.arange(size_y))
+ t_xv, t_yv = np.meshgrid(theta_x, theta_y)
+
+ tp1 = t_xv ** 2 + t_yv ** 2 >= app_ratio ** 2
+ aperture[tp1.T] = 0.
+ # print(app_ratio, screen_width, dk)
+
+ progress = tqdm(total=500)
+ # de = 100
+ dest = 100
+ i = 0
+ while abs(dest) > 0.0001: # or abs(de) > .025:
+ i += 1
+ error_old = np.sum(error.real)
+ est_old = est.copy()
+ error = source / np.real(fftpack.fftshift(fftpack.ifft2(fftpack.fft2(est) * response_ft)))
+ est = est * np.real(fftpack.fftshift(fftpack.ifft2(fftpack.fft2(error) * np.conjugate(response_ft))))
+ # est = est_old * est
+ # est = np.real(fftpack.fftshift(fftpack.ifft2(fftpack.fft2(est)*fftpack.fftshift(aperture) )))
+
+ error_new = np.real(np.sum(np.power(error, 2))) - error_old
+ dest = np.sum(np.power((est - est_old).real, 2)) / np.sum(est) * 100
+ # print(np.sum((est.real - est_old.real)* (est.real - est_old.real) )/np.sum(est.real)*100 )
+
+ if error_old != 0:
+ de = error_new / error_old * 1.0
+ else:
+ de = error_new
+
+ if verbose:
+ print(
+ ' LR Deconvolution - Iteration: {0:d} Error: {1:.2f} = change: {2:.5f}%, {3:.5f}%'.format(i, error_new,
+ de,
+ abs(dest)))
+ if i > 500:
+ dest = 0.0
+ print('terminate')
+ progress.update(1)
+ progress.write(f"converged in {i} iterations")
+ # progress.close()
+ print('\n Lucy-Richardson deconvolution converged in ' + str(i) + ' iterations')
+ est2 = np.real(fftpack.ifft2(fftpack.fft2(est) * fftpack.fftshift(aperture)))
+ out_dataset = o_image.like_data(est2)
+ out_dataset.title = 'Lucy Richardson deconvolution'
+ out_dataset.data_type = 'image'
+ return out_dataset
+
+"""
+Input Dialog for EELS Analysis
+
+Author: Gerd Duscher
+
+"""
+import numpy as np
+import sidpy
+
+Qt_available = True
+try:
+ from PyQt5 import QtCore, QtWidgets
+except:
+ Qt_available = False
+ # print('Qt dialogs are not available')
+
+import ipywidgets
+
+from IPython.display import display
+
+from pyTEMlib.microscope import microscope
+from pyTEMlib import file_tools as ft
+from pyTEMlib import eels_dialog_utilities
+_version = 000
+
+
+if Qt_available:
+ from pyTEMlib import info_dlg
+ from pyTEMlib import interactive_eels as ieels
+ class InfoDialog(QtWidgets.QDialog):
+ """
+ Input Dialog for EELS Analysis
+
+ Opens a PyQt5 GUi Dialog that allows to set the experimental parameter necessary for a Quantification.
+
+
+ The dialog operates on a sidpy dataset
+ """
+
+ def __init__(self, datasets=None, key=None):
+ super().__init__(None, QtCore.Qt.WindowStaysOnTopHint)
+ # Create an instance of the GUI
+ self.ui = info_dlg.UiDialog(self)
+ self.set_action()
+ self.datasets = datasets
+
+ self.spec_dim = []
+ self.energy_scale = np.array([])
+ self.experiment = {}
+ self.energy_dlg = None
+ self.axis = None
+
+ self.y_scale = 1.0
+ self.change_y_scale = 1.0
+ self.show()
+
+ if self.datasets is None:
+ # make a dummy dataset for testing
+ key = 'Channel_000'
+ self.datasets={key: ft.make_dummy_dataset(sidpy.DataType.SPECTRUM)}
+ if key is None:
+ key = list(self.datasets.keys())[0]
+ self.dataset = self.datasets[key]
+ self.key = key
+ if not isinstance(self.dataset, sidpy.Dataset):
+ raise TypeError('dataset has to be a sidpy dataset')
+
+ self.set_dataset(self.dataset)
+
+ # view = self.dataset.plot()
+ if hasattr(self.dataset.view, 'axes'):
+ self.axis = self.dataset.view.axes[-1]
+ elif hasattr(self.dataset.view, 'axis'):
+ self.axis = self.dataset.view.axis
+ self.figure = self.axis.figure
+ self.plot()
+ self.update()
+
+ def set_dataset(self, dataset):
+ self.dataset = dataset
+ if not hasattr(self.dataset, '_axes'):
+ self.dataset._axes = self.dataset.axes
+ if not hasattr(self.dataset, 'meta_data'):
+ self.dataset.meta_data = {}
+
+ spec_dim = dataset.get_dimensions_by_type(sidpy.DimensionType.SPECTRAL)
+ if len(spec_dim) != 1:
+ raise TypeError('We need exactly one SPECTRAL dimension')
+ self.spec_dim = self.dataset._axes[spec_dim[0]]
+ self.energy_scale = self.spec_dim.values.copy()
+
+ minimum_info = {'offset': self.energy_scale[0],
+ 'dispersion': self.energy_scale[1] - self.energy_scale[0],
+ 'exposure_time': 0.0,
+ 'convergence_angle': 0.0, 'collection_angle': 0.0,
+ 'acceleration_voltage': 100.0, 'binning': 1, 'conversion': 1.0,
+ 'flux_ppm': -1.0, 'flux_unit': 'counts', 'current': 1.0, 'SI_bin_x': 1, 'SI_bin_y': 1}
+ if 'experiment' not in self.dataset.metadata:
+ self.dataset.metadata['experiment'] = minimum_info
+ self.experiment = self.dataset.metadata['experiment']
+
+ for key, item in minimum_info.items():
+ if key not in self.experiment:
+ self.experiment[key] = item
+ self.set_flux_list()
+
+ def set_dimension(self):
+ spec_dim = self.dataset.get_dimensions_by_type(sidpy.DimensionType.SPECTRAL)
+ self.spec_dim = self.dataset._axes[spec_dim[0]]
+ old_energy_scale = self.spec_dim
+ self.dataset.set_dimension(spec_dim[0], sidpy.Dimension(np.array(self.energy_scale),
+ name=old_energy_scale.name,
+ dimension_type=sidpy.DimensionType.SPECTRAL,
+ units='eV',
+ quantity='energy loss'))
+
+ def update(self):
+
+ self.ui.offsetEdit.setText(f"{self.experiment['offset']:.3f}")
+ self.ui.dispersionEdit.setText(f"{self.experiment['dispersion']:.3f}")
+ self.ui.timeEdit.setText(f"{self.experiment['exposure_time']:.6f}")
+
+ self.ui.convEdit.setText(f"{self.experiment['convergence_angle']:.2f}")
+ self.ui.collEdit.setText(f"{self.experiment['collection_angle']:.2f}")
+ self.ui.E0Edit.setText(f"{self.experiment['acceleration_voltage']/1000.:.2f}")
+
+ self.ui.binningEdit.setText(f"{self.experiment['binning']}")
+ self.ui.conversionEdit.setText(f"{self.experiment['conversion']:.2f}")
+ self.ui.fluxEdit.setText(f"{self.experiment['flux_ppm']:.2f}")
+ self.ui.fluxUnit.setText(f"{self.experiment['flux_unit']}")
+ self.ui.VOAEdit.setText(f"{self.experiment['current']:.2f}")
+ self.ui.statusBar.showMessage('Message in statusbar.')
+
+ def on_enter(self):
+ sender = self.sender()
+
+ if sender == self.ui.offsetEdit:
+ value = float(str(sender.displayText()).strip())
+ self.experiment['offset'] = value
+ sender.setText(f"{value:.2f}")
+ self.energy_scale = self.energy_scale - self.energy_scale[0] + value
+ self.set_dimension()
+ self.plot()
+ elif sender == self.ui.dispersionEdit:
+ value = float(str(sender.displayText()).strip())
+ self.experiment['dispersion'] = value
+ self.energy_scale = np.arange(len(self.energy_scale)) * value + self.energy_scale[0]
+ self.set_dimension()
+ self.plot()
+ sender.setText(f"{value:.3f}")
+ elif sender == self.ui.timeEdit:
+ value = float(str(sender.displayText()).strip())
+ self.experiment['exposure_time'] = value
+ sender.setText(f"{value:.2f}")
+ elif sender == self.ui.convEdit:
+ value = float(str(sender.displayText()).strip())
+ self.experiment['convergence_angle'] = value
+ sender.setText(f"{value:.2f}")
+ elif sender == self.ui.collEdit:
+ value = float(str(sender.displayText()).strip())
+ self.experiment['collection_angle'] = value
+ sender.setText(f"{value:.2f}")
+ elif sender == self.ui.E0Edit:
+ value = float(str(sender.displayText()).strip())
+ self.experiment['acceleration_voltage'] = value*1000.0
+ sender.setText(f"{value:.2f}")
+ elif sender == self.ui.fluxEdit:
+ value = float(str(sender.displayText()).strip())
+ if value == 0:
+ self.set_flux()
+ else:
+ self.experiment['flux_ppm'] = value
+ sender.setText(f"{value:.2f}")
+ elif sender == self.ui.binXEdit or sender == self.ui.binYEdit:
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ bin_x = int(self.ui.binXEdit.displayText())
+ bin_y = int(self.ui.binYEdit.displayText())
+ self.experiment['SI_bin_x'] = bin_x
+ self.experiment['SI_bin_y'] = bin_y
+ self.dataset.view.set_bin([bin_x, bin_y])
+ self.ui.binXEdit.setText(str(self.dataset.view.bin_x))
+ self.ui.binYEdit.setText(str(self.dataset.view.bin_y))
+ else:
+ print('not supported yet')
+
+ def plot(self):
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ spectrum = self.dataset.view.get_spectrum()
+ self.axis = self.dataset.view.axes[1]
+ else:
+ spectrum = np.array(self.dataset)
+ self.axis = self.dataset.view.axis
+
+ spectrum *= self.y_scale
+
+ x_limit = self.axis.get_xlim()
+ y_limit = np.array(self.axis.get_ylim())
+ self.axis.clear()
+
+
+ self.axis.plot(self.energy_scale, spectrum, label='spectrum')
+ self.axis.set_xlim(x_limit)
+ if self.change_y_scale !=1.0:
+ y_limit *= self.change_y_scale
+ self.change_y_scale = 1.0
+ self.axis.set_ylim(y_limit)
+
+ if self.y_scale != 1.:
+ self.axis.set_ylabel('scattering intensity (ppm)')
+
+ self.axis.set_xlabel('energy_loss (eV)')
+
+ self.figure.canvas.draw_idle()
+
+ def on_list_enter(self):
+ sender = self.sender()
+ if sender == self.ui.TEMList:
+ microscope.set_microscope(self.ui.TEMList.currentText())
+ self.experiment['microscope'] = microscope.name
+ self.experiment['convergence_angle'] = microscope.alpha
+ self.experiment['collection_angle'] = microscope.beta
+ self.experiment['acceleration_voltage'] = microscope.E0
+ self.update()
+
+ def set_energy_scale(self):
+ self.energy_dlg = ieels.EnergySelector(self.dataset)
+
+ self.energy_dlg.signal_selected[bool].connect(self.set_energy)
+ self.energy_dlg.show()
+
+ def set_energy(self, k):
+ spec_dim = self.dataset.get_dimensions_by_type(sidpy.DimensionType.SPECTRAL)
+ self.spec_dim = self.dataset._axes[spec_dim[0]]
+
+ self.energy_scale = self.spec_dim.values
+ self.experiment['offset'] = self.energy_scale[0]
+ self.experiment['dispersion'] = self.energy_scale[1] - self.energy_scale[0]
+ self.update()
+
+ def set_flux(self, key):
+ self.ui.statusBar.showMessage('on_set_flux')
+ new_flux = 1.0
+ title = key
+ metadata = {}
+ if key in self.datasets.keys():
+ flux_dataset = self.datasets[key]
+ if isinstance(flux_dataset, sidpy.Dataset):
+ exposure_time = -1.0
+ flux_dataset = self.datasets[key]
+ if flux_dataset.data_type.name == 'IMAGE' or 'SPECTRUM' in flux_dataset.data_type.name:
+ if 'exposure_time' in flux_dataset.metadata['experiment']:
+ if 'number_of_frames' in flux_dataset.metadata['experiment']:
+ exposure_time = flux_dataset.metadata['experiment']['single_exposure_time'] * flux_dataset.metadata['experiment']['number_of_frames']
+ else:
+ exposure_time = flux_dataset.metadata['experiment']['exposure_time']
+ else:
+ exposure_time = -1.0
+ flux_dataset.metadata['experiment']['exposure_time'] = -1
+ print('Did not find exposure time assume 1s')
+ if exposure_time > 0:
+ new_flux = np.sum(np.array(flux_dataset*1e-6))/exposure_time*self.dataset.metadata['experiment']['exposure_time']
+ title = flux_dataset.title
+ metadata = flux_dataset.metadata
+ self.experiment['flux_ppm'] = new_flux
+ self.experiment['flux_units'] = 'Mcounts '
+ self.experiment['flux_source'] = title
+ self.experiment['flux_metadata'] = metadata
+
+ self.update()
+
+ def on_check(self):
+ sender = self.sender()
+
+ if sender.objectName() == 'probability':
+ dispersion = self.energy_scale[1]-self.energy_scale[0]
+ if sender.isChecked():
+ self.y_scale = 1/self.experiment['flux_ppm']*dispersion
+ self.change_y_scale = 1/self.experiment['flux_ppm']*dispersion
+ else:
+ self.y_scale = 1.
+ self.change_y_scale = self.experiment['flux_ppm']/dispersion
+ self.plot()
+
+ def set_flux_list(self):
+ length_list = self.ui.select_flux.count()+1
+ for i in range(2, length_list):
+ self.ui.select_flux.removeItem(i)
+ for key in self.datasets.keys():
+ if isinstance(self.datasets[key], sidpy.Dataset):
+ if self.datasets[key].title != self.dataset.title:
+ self.ui.select_flux.addItem(key+': '+self.datasets[key].title)
+
+ def on_list_enter(self):
+ self.ui.statusBar.showMessage('on_list')
+ sender = self.sender()
+ if sender.objectName() == 'select_flux_list':
+ self.ui.statusBar.showMessage('list')
+ index = self.ui.select_flux.currentIndex()
+ self.ui.statusBar.showMessage('list'+str(index))
+ if index == 1:
+ ft.add_dataset_from_file(self.datasets, key_name='Reference')
+ self.set_flux_list()
+ else:
+ key = str(self.ui.select_flux.currentText()).split(':')[0]
+ self.set_flux(key)
+
+ self.update()
+
+ def set_action(self):
+ self.ui.statusBar.showMessage('action')
+ self.ui.offsetEdit.editingFinished.connect(self.on_enter)
+ self.ui.dispersionEdit.editingFinished.connect(self.on_enter)
+ self.ui.timeEdit.editingFinished.connect(self.on_enter)
+
+ self.ui.TEMList.activated[str].connect(self.on_list_enter)
+
+ self.ui.convEdit.editingFinished.connect(self.on_enter)
+ self.ui.collEdit.editingFinished.connect(self.on_enter)
+ self.ui.E0Edit.editingFinished.connect(self.on_enter)
+ self.ui.binningEdit.editingFinished.connect(self.on_enter)
+ self.ui.conversionEdit.editingFinished.connect(self.on_enter)
+ self.ui.fluxEdit.editingFinished.connect(self.on_enter)
+ self.ui.VOAEdit.editingFinished.connect(self.on_enter)
+ self.ui.energy_button.clicked.connect(self.set_energy_scale)
+ self.ui.select_flux.activated[str].connect(self.on_list_enter)
+
+ self.ui.check_probability.clicked.connect(self.on_check)
+
+ self.ui.binXEdit.editingFinished.connect(self.on_enter)
+ self.ui.binYEdit.editingFinished.connect(self.on_enter)
+
+
+[docs]def get_sidebar():
+ side_bar = ipywidgets.GridspecLayout(17, 3,width='auto', grid_gap="0px")
+
+ side_bar[0, :2] = ipywidgets.Dropdown(
+ options=[('None', 0)],
+ value=0,
+ description='Main Dataset:',
+ disabled=False)
+
+ row = 1
+ side_bar[row, :3] = ipywidgets.Button(description='Energy Scale',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Offset:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Dispersion:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+
+ row += 1
+ side_bar[row, :3] = ipywidgets.Button(description='Microscope',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Conv.Angle:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Label(value="mrad", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Coll.Angle:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Label(value="mrad", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Acc Voltage:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Label(value="keV", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+
+ side_bar[row, :3] = ipywidgets.Button(description='Quantification',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row+=1
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=[('None', 0)],
+ value=0,
+ description='Reference:',
+ disabled=False)
+ side_bar[row,2] = ipywidgets.ToggleButton(
+ description='Probability',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Changes y-axis to probability if flux is given',
+ layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Exp_Time:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Label(value="s", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Flux:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Label(value="Mcounts", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Conversion:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Label(value=r"e$^-$/counts", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Current:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Label(value="pA", layout=ipywidgets.Layout(width='100px') )
+
+ row += 1
+
+ side_bar[row, :3] = ipywidgets.Button(description='Spectrum Image',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+
+ row += 1
+ side_bar[row, :2] = ipywidgets.IntText(value=1, description='bin X:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.IntText(value=1, description='bin X:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+
+ for i in range(14, 17):
+ side_bar[i, 0].layout.display = "none"
+ return side_bar
+
+[docs]class InfoWidget(object):
+ def __init__(self, datasets=None):
+ self.datasets = datasets
+ self.dataset = None
+
+ self.sidebar = get_sidebar()
+
+ self.set_dataset()
+ self.set_action()
+
+ self.app_layout = ipywidgets.AppLayout(
+ left_sidebar=self.sidebar,
+ center=self.view.panel,
+ footer=None,#message_bar,
+ pane_heights=[0, 10, 0],
+ pane_widths=[4, 10, 0],
+ )
+ display(self.app_layout)
+
+ def get_spectrum(self):
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ spectrum = self.dataset.view.get_spectrum()
+ self.axis = self.view.axes[1]
+ else:
+ spectrum = np.array(self.dataset)
+ self.axis = self.view.axis
+
+ spectrum *= self.y_scale
+ return spectrum
+
+ def plot(self, scale=True):
+ self.energy_scale = self.dataset.energy_loss.values
+ self.view.change_y_scale = self.change_y_scale
+ self.view.y_scale = self.y_scale
+
+ self.view.plot()
+
+ def set_dataset(self, index=0):
+ spectrum_list = []
+ reference_list =[('None', -1)]
+ dataset_index = self.sidebar[0, 0].value
+ for index, key in enumerate(self.datasets.keys()):
+ if 'Reference' not in key:
+ if 'SPECTR' in self.datasets[key].data_type.name:
+ spectrum_list.append((f'{key}: {self.datasets[key].title}', index))
+ reference_list.append((f'{key}: {self.datasets[key].title}', index))
+
+ self.sidebar[0,0].options = spectrum_list
+ self.sidebar[9,0].options = reference_list
+ self.key = list(self.datasets)[dataset_index]
+ self.dataset = self.datasets[self.key]
+ if 'SPECTRUM' in self.dataset.data_type.name:
+ for i in range(14, 17):
+ self.sidebar[i, 0].layout.display = "none"
+ else:
+ for i in range(14, 17):
+ self.sidebar[i, 0].layout.display = "flex"
+ #self.sidebar[0,0].value = dataset_index #f'{self.key}: {self.datasets[self.key].title}'
+ self.sidebar[2,0].value = np.round(self.datasets[self.key].energy_loss[0], 3)
+ self.sidebar[3,0].value = np.round(self.datasets[self.key].energy_loss[1] - self.datasets[self.key].energy_loss[0], 4)
+ self.sidebar[5,0].value = np.round(self.datasets[self.key].metadata['experiment']['convergence_angle'], 1)
+ self.sidebar[6,0].value = np.round(self.datasets[self.key].metadata['experiment']['collection_angle'], 1)
+ self.sidebar[7,0].value = np.round(self.datasets[self.key].metadata['experiment']['acceleration_voltage']/1000, 1)
+ self.sidebar[10,0].value = np.round(self.datasets[self.key].metadata['experiment']['exposure_time'], 4)
+ if 'flux_ppm' not in self.datasets[self.key].metadata['experiment']:
+ self.datasets[self.key].metadata['experiment']['flux_ppm'] = 0
+ self.sidebar[11,0].value = self.datasets[self.key].metadata['experiment']['flux_ppm']
+ if 'count_conversion' not in self.datasets[self.key].metadata['experiment']:
+ self.datasets[self.key].metadata['experiment']['count_conversion'] = 1
+ self.sidebar[12,0].value = self.datasets[self.key].metadata['experiment']['count_conversion']
+ if 'beam_current' not in self.datasets[self.key].metadata['experiment']:
+ self.datasets[self.key].metadata['experiment']['beam_current'] = 0
+ self.sidebar[13,0].value = self.datasets[self.key].metadata['experiment']['beam_current']
+ if self.dataset.data_type.name =='SPECTRAL_IMAGE':
+ self.view = eels_dialog_utilities.SIPlot(self.dataset)
+ else:
+ self.view = eels_dialog_utilities.SpectrumPlot(self.dataset)
+ self.y_scale = 1.0
+ self.change_y_scale = 1.0
+
+ def cursor2energy_scale(self, value):
+ dispersion = (self.view.end_cursor.value - self.view.start_cursor.value) / (self.view.end_channel - self.view.start_channel)
+ self.datasets[self.key].energy_loss *= (self.sidebar[3, 0].value/dispersion)
+ self.sidebar[3, 0].value = dispersion
+ offset = self.view.start_cursor.value - self.view.start_channel * dispersion
+ self.datasets[self.key].energy_loss += (self.sidebar[2, 0].value-self.datasets[self.key].energy_loss[0])
+ self.sidebar[2, 0].value = offset
+ self.plot()
+
+ def set_energy_scale(self, value):
+ dispersion = self.datasets[self.key].energy_loss[1] - self.datasets[self.key].energy_loss[0]
+ self.datasets[self.key].energy_loss *= (self.sidebar[3, 0].value/dispersion)
+ self.datasets[self.key].energy_loss += (self.sidebar[2, 0].value-self.datasets[self.key].energy_loss[0])
+ self.plot()
+
+ def set_y_scale(self, value):
+ self.change_y_scale = 1/self.y_scale
+ if self.sidebar[9,2].value:
+ dispersion = self.datasets[self.key].energy_loss[1] - self.datasets[self.key].energy_loss[0]
+ self.y_scale = 1/self.datasets[self.key].metadata['experiment']['flux_ppm'] * dispersion
+ else:
+ self.y_scale = 1.0
+ self.change_y_scale *= self.y_scale
+ self.plot()
+
+ def set_flux(self, value):
+ self.datasets[self.key].metadata['experiment']['exposure_time'] = self.sidebar[10,0].value
+ if self.sidebar[9,0].value < 0:
+ self.datasets[self.key].metadata['experiment']['flux_ppm'] = 0.
+ else:
+ key = list(self.datasets.keys())[self.sidebar[9,0].value]
+ self.datasets[self.key].metadata['experiment']['flux_ppm'] = (np.array(self.datasets[key])*1e-6).sum() / self.datasets[key].metadata['experiment']['exposure_time']
+ self.datasets[self.key].metadata['experiment']['flux_ppm'] *= self.datasets[self.key].metadata['experiment']['exposure_time']
+ self.sidebar[11,0].value = np.round(self.datasets[self.key].metadata['experiment']['flux_ppm'], 2)
+
+ def set_microscope_parameter(self, value):
+ self.datasets[self.key].metadata['experiment']['convergence_angle'] = self.sidebar[5,0].value
+ self.datasets[self.key].metadata['experiment']['collection_angle'] = self.sidebar[6,0].value
+ self.datasets[self.key].metadata['experiment']['acceleration_voltage'] = self.sidebar[7,0].value*1000
+
+ def set_binning(self, value):
+ if 'SPECTRAL' in self.dataset.data_type.name:
+ bin_x = self.sidebar[15,0].value
+ bin_y = self.sidebar[16,0].value
+ self.dataset.view.set_bin([bin_x, bin_y])
+ self.datasets[self.key].metadata['experiment']['SI_bin_x'] = bin_x
+ self.datasets[self.key].metadata['experiment']['SI_bin_y'] = bin_y
+
+ def set_action(self):
+ self.sidebar[0,0].observe(self.set_dataset)
+ self.sidebar[1,0].on_click(self.cursor2energy_scale)
+ self.sidebar[2,0].observe(self.set_energy_scale, names='value')
+ self.sidebar[3,0].observe(self.set_energy_scale, names='value')
+ self.sidebar[5,0].observe(self.set_microscope_parameter)
+ self.sidebar[6,0].observe(self.set_microscope_parameter)
+ self.sidebar[7,0].observe(self.set_microscope_parameter)
+ self.sidebar[9,0].observe(self.set_flux)
+ self.sidebar[9,2].observe(self.set_y_scale)
+ self.sidebar[10,0].observe(self.set_flux)
+ self.sidebar[15,0].observe(self.set_binning)
+ self.sidebar[16,0].observe(self.set_binning)
+
+
+import numpy as np
+import sidpy
+
+
+import pyTEMlib.eels_dialog_utilities as ieels
+import pyTEMlib.file_tools as ft
+from pyTEMlib.microscope import microscope
+import ipywidgets
+import matplotlib.pylab as plt
+import matplotlib
+
+from IPython.display import display
+
+
+from pyTEMlib import file_tools
+from pyTEMlib import eels_tools
+
+[docs]def get_info_sidebar():
+ side_bar = ipywidgets.GridspecLayout(17, 3,width='auto', grid_gap="0px")
+
+ side_bar[0, :2] = ipywidgets.Dropdown(
+ options=[('None', 0)],
+ value=0,
+ description='Main Dataset:',
+ disabled=False)
+
+ row = 1
+ side_bar[row, :3] = ipywidgets.Button(description='Energy Scale',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Offset:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Dispersion:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+
+ row += 1
+ side_bar[row, :3] = ipywidgets.Button(description='Microscope',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Conv.Angle:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="mrad", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Coll.Angle:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="mrad", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Acc Voltage:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="keV", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+
+ side_bar[row, :3] = ipywidgets.Button(description='Quantification',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row+=1
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=[('None', 0)],
+ value=0,
+ description='Reference:',
+ disabled=False)
+ side_bar[row,2] = ipywidgets.ToggleButton(
+ description='Probability',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Changes y-axis to probability if flux is given',
+ layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Exp_Time:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="s", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Flux:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="Mcounts", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Conversion:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value=r"e$^-$/counts", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Current:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="pA", layout=ipywidgets.Layout(width='100px') )
+
+ row += 1
+
+ side_bar[row, :3] = ipywidgets.Button(description='Spectrum Image',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+
+ row += 1
+ side_bar[row, :2] = ipywidgets.IntText(value=1, description='bin X:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.IntText(value=1, description='bin X:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+
+ for i in range(14, 17):
+ side_bar[i, 0].layout.display = "none"
+ return side_bar
+
+from sidpy.io.interface_utils import open_file_dialog
+[docs]class EELSWidget(object):
+ def __init__(self, datasets, sidebar, tab_title = None):
+
+ self.datasets = datasets
+ self.dataset = None
+
+ if not isinstance(sidebar, list):
+ tab = ipywidgets.Tab()
+ tab.children = [ft.FileWidget(), sidebar]
+ tab.titles = ['Load', 'Info']
+ else:
+ tab = sidebar
+
+ self.sidebar = sidebar
+ with plt.ioff():
+ self.figure = plt.figure()
+
+ self.figure.canvas.toolbar_position = 'right'
+ self.figure.canvas.toolbar_visible = True
+
+ self.start_cursor = ipywidgets.FloatText(value=0, description='Start:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ self.end_cursor = ipywidgets.FloatText(value=0, description='End:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ self.panel = ipywidgets.VBox([ipywidgets.HBox([ipywidgets.Label('',layout=ipywidgets.Layout(width='100px')), ipywidgets.Label('Cursor:'),
+ self.start_cursor,ipywidgets.Label('eV'),
+ self.end_cursor, ipywidgets.Label('eV')]),
+ self.figure.canvas])
+
+
+ self.app_layout = ipywidgets.AppLayout(
+ left_sidebar=tab,
+ center=self.panel,
+ footer=None,#message_bar,
+ pane_heights=[0, 10, 0],
+ pane_widths=[4, 10, 0],
+ )
+ self.set_dataset()
+
+ display(self.app_layout)
+
+ def plot(self, scale=True):
+ self.figure.clear()
+ self.energy_scale = self.dataset.energy_loss.values
+
+ if self.dataset.data_type.name == 'SPECTRUM':
+ self.axis = self.figure.subplots(ncols=1)
+ else:
+ self.plot_spectrum_image()
+ self.axis = self.axes[-1]
+ self.spectrum = self.get_spectrum()
+
+ self.plot_spectrum()
+
+ def plot_spectrum(self):
+ self.axis.plot(self.energy_scale, self.spectrum, label='spectrum')
+ x_limit = self.axis.get_xlim()
+ y_limit = np.array(self.axis.get_ylim())
+ self.xlabel = self.datasets[self.key].labels[0]
+ self.ylabel = self.datasets[self.key].data_descriptor
+ self.axis.set_xlabel(self.datasets[self.key].labels[0])
+ self.axis.set_ylabel(self.datasets[self.key].data_descriptor)
+ self.axis.ticklabel_format(style='sci', scilimits=(-2, 3))
+ #if scale:
+ # self.axis.set_ylim(np.array(y_limit)*self.change_y_scale)
+ self.change_y_scale = 1.0
+ if self.y_scale != 1.:
+ self.axis.set_ylabel('scattering probability (ppm/eV)')
+ self.selector = matplotlib.widgets.SpanSelector(self.axis, self.line_select_callback,
+ direction="horizontal",
+ interactive=True,
+ props=dict(facecolor='blue', alpha=0.2))
+ self.axis.legend()
+ if self.dataset.data_type.name == 'SPECTRUM':
+ self.axis.set_title(self.dataset.title)
+ else:
+ self.axis.set_title(f'spectrum {self.x}, {self.y}')
+ self.figure.canvas.draw_idle()
+
+ def _update(self, ev=None):
+
+ xlim = np.array(self.axes[1].get_xlim())
+ ylim = np.array(self.axes[1].get_ylim())
+ self.axes[1].clear()
+ self.get_spectrum()
+ if len(self.energy_scale)!=self.spectrum.shape[0]:
+ self.spectrum = self.spectrum.T
+ self.axes[1].plot(self.energy_scale, self.spectrum.compute(), label='experiment')
+
+ self.axes[1].set_title(f'spectrum {self.x}, {self.y}')
+ self.figure.tight_layout()
+ self.selector = matplotlib.widgets.SpanSelector(self.axis, self.line_select_callback,
+ direction="horizontal",
+ interactive=True,
+ props=dict(facecolor='blue', alpha=0.2))
+
+ self.axes[1].set_xlim(xlim)
+ self.axes[1].set_ylim(ylim*self.change_y_scale)
+ self.axes[1].set_xlabel(self.xlabel)
+ self.axes[1].set_ylabel(self.ylabel)
+ self.change_y_scale = 1.0
+ self.figure.canvas.draw_idle()
+
+ def _onclick(self, event):
+ self.event = event
+ if event.inaxes in [self.axes[0]]:
+ x = int(event.xdata)
+ y = int(event.ydata)
+
+ x = int(x - self.rectangle[0])
+ y = int(y - self.rectangle[2])
+
+ if x >= 0 and y >= 0:
+ if x <= self.rectangle[1] and y <= self.rectangle[3]:
+ self.x = int(x / (self.rect.get_width() / self.bin_x))
+ self.y = int(y / (self.rect.get_height() / self.bin_y))
+ image_dims = self.dataset.get_dimensions_by_type(sidpy.DimensionType.SPATIAL)
+
+ if self.x + self.bin_x > self.dataset.shape[image_dims[0]]:
+ self.x = self.dataset.shape[image_dims[0]] - self.bin_x
+ if self.y + self.bin_y > self.dataset.shape[image_dims[1]]:
+ self.y = self.dataset.shape[image_dims[1]] - self.bin_y
+
+ self.rect.set_xy([self.x * self.rect.get_width() / self.bin_x + self.rectangle[0],
+ self.y * self.rect.get_height() / self.bin_y + self.rectangle[2]])
+ # self.get_spectrum()
+ self._update()
+ else:
+ if event.dblclick:
+ bottom = float(self.spectrum.min())
+ if bottom < 0:
+ bottom *= 1.02
+ else:
+ bottom *= 0.98
+ top = float(self.spectrum.max())
+ if top > 0:
+ top *= 1.02
+ else:
+ top *= 0.98
+ self.axis.set_ylim(bottom=bottom, top=top)
+
+ def get_spectrum(self):
+ if self.dataset.data_type == sidpy.DataType.SPECTRUM:
+ self.spectrum = self.dataset.copy()
+ else:
+ image_dims = self.dataset.get_dimensions_by_type(sidpy.DimensionType.SPATIAL)
+ if self.x > self.dataset.shape[image_dims[0]] - self.bin_x:
+ self.x = self.dataset.shape[image_dims[0]] - self.bin_x
+ if self.y > self.dataset.shape[image_dims[1]] - self.bin_y:
+ self.y = self.dataset.shape[image_dims[1]] - self.bin_y
+ selection = []
+ self.axis.clear()
+ for dim, axis in self.dataset._axes.items():
+ # print(dim, axis.dimension_type)
+ if axis.dimension_type == sidpy.DimensionType.SPATIAL:
+ if dim == image_dims[0]:
+ selection.append(slice(self.x, self.x + self.bin_x))
+ else:
+ selection.append(slice(self.y, self.y + self.bin_y))
+
+ elif axis.dimension_type == sidpy.DimensionType.SPECTRAL:
+ selection.append(slice(None))
+ elif axis.dimension_type == sidpy.DimensionType.CHANNEL:
+ selection.append(slice(None))
+ else:
+ selection.append(slice(0, 1))
+
+ self.spectrum = self.dataset[tuple(selection)].mean(axis=tuple(image_dims))
+
+ self.spectrum *= self.y_scale
+
+ return self.spectrum.squeeze()
+
+ def plot_spectrum_image(self):
+ self.axes = self.figure.subplots(ncols=2)
+ self.axis = self.axes[-1]
+
+ spec_dim = self.dataset.get_dimensions_by_type(sidpy.DimensionType.SPECTRAL)
+ if len(spec_dim) != 1:
+ raise ValueError('Only one spectral dimension')
+
+ channel_dim = self.dataset.get_dimensions_by_type(sidpy.DimensionType.CHANNEL)
+ channel_dim =[]
+ if len(channel_dim) > 1:
+ raise ValueError('Maximal one channel dimension')
+
+ if len(channel_dim) > 0:
+ self.image = self.dataset.mean(axis=(spec_dim[0] ,channel_dim[0]))
+ else:
+ self.image = self.dataset.mean(axis=(spec_dim[0]))
+
+ self.rect = matplotlib.patches.Rectangle((0, 0), self.bin_x, self.bin_y, linewidth=1, edgecolor='r',
+ facecolor='red', alpha=0.2)
+ size_x = self.image.shape[0]
+ size_y = self.image.shape[1]
+ self.extent = [0, size_x, size_y, 0]
+ self.rectangle = [0, size_x, 0, size_y]
+ self.axes[0].imshow(self.image.T, extent=self.extent)
+ self.axes[0].set_aspect('equal')
+ self.axes[0].add_patch(self.rect)
+ self.cid = self.axes[0].figure.canvas.mpl_connect('button_press_event', self._onclick)
+
+
+ def line_select_callback(self, x_min, x_max):
+ self.start_cursor.value = np.round(x_min, 3)
+ self.end_cursor.value = np.round(x_max, 3)
+ self.start_channel = np.searchsorted(self.datasets[self.key].energy_loss, self.start_cursor.value)
+ self.end_channel = np.searchsorted(self.datasets[self.key].energy_loss, self.end_cursor.value)
+
+ def set_dataset(self, index=0):
+
+ if len(self.datasets) == 0:
+ data_set = sidpy.Dataset.from_array([0, 1], name='generic')
+ data_set.set_dimension(0, sidpy.Dimension([0,1], 'energy_loss', units='channel', quantity='generic',
+ dimension_type='spectral'))
+ data_set.data_type = 'spectrum'
+ data_set.metadata= {'experiment':{'convergence_angle': 0,
+ 'collection_angle': 0,
+ 'acceleration_voltage':0,
+ 'exposure_time':0}}
+ self.datasets={'Channel_000': data_set}
+ index = 0
+
+ dataset_index = index
+
+ self.key = list(self.datasets)[dataset_index]
+ self.dataset = self.datasets[self.key]
+
+ self._udpate_sidbar()
+ self.y_scale = 1.0
+ self.change_y_scale = 1.0
+ self.x = 0
+ self.y = 0
+ self.bin_x = 1
+ self.bin_y = 1
+ self.count = 0
+
+ self.plot()
+
+ def _udpate_sidbar(self):
+ pass
+
+
+ def set_energy_scale(self, value):
+ dispersion = self.datasets[self.key].energy_loss[1] - self.datasets[self.key].energy_loss[0]
+ self.datasets[self.key].energy_loss *= (self.sidebar[3, 0].value/dispersion)
+ self.datasets[self.key].energy_loss += (self.sidebar[2, 0].value-self.datasets[self.key].energy_loss[0])
+ self.plot()
+
+ def set_y_scale(self, value):
+ self.count += 1
+ self.change_y_scale = 1.0/self.y_scale
+ if self.sidebar[9,2].value:
+ dispersion = self.datasets[self.key].energy_loss[1] - self.datasets[self.key].energy_loss[0]
+ self.y_scale = 1/self.datasets[self.key].metadata['experiment']['flux_ppm'] * dispersion
+ self.ylabel='scattering probability (ppm)'
+ else:
+ self.y_scale = 1.0
+ self.ylabel='intensity (counts)'
+ self.change_y_scale *= self.y_scale
+ self._update()
+
+[docs]class InfoWidget(EELSWidget):
+ def __init__(self, datasets):
+
+ sidebar = get_info_sidebar()
+ super().__init__(datasets, sidebar)
+ self.set_action()
+
+ def set_flux(self, value):
+ self.datasets[self.key].metadata['experiment']['exposure_time'] = self.sidebar[10,0].value
+ if self.sidebar[9,0].value < 0:
+ self.datasets[self.key].metadata['experiment']['flux_ppm'] = 0.
+ else:
+ key = list(self.datasets.keys())[self.sidebar[9,0].value]
+ self.datasets[self.key].metadata['experiment']['flux_ppm'] = (np.array(self.datasets[key])*1e-6).sum() / self.datasets[key].metadata['experiment']['exposure_time']
+ self.datasets[self.key].metadata['experiment']['flux_ppm'] *= self.datasets[self.key].metadata['experiment']['exposure_time']
+ self.sidebar[11,0].value = np.round(self.datasets[self.key].metadata['experiment']['flux_ppm'], 2)
+
+ def set_microscope_parameter(self, value):
+ self.datasets[self.key].metadata['experiment']['convergence_angle'] = self.sidebar[5,0].value
+ self.datasets[self.key].metadata['experiment']['collection_angle'] = self.sidebar[6,0].value
+ self.datasets[self.key].metadata['experiment']['acceleration_voltage'] = self.sidebar[7,0].value*1000
+
+ def cursor2energy_scale(self, value):
+ dispersion = (self.end_cursor.value - self.start_cursor.value) / (self.end_channel - self.start_channel)
+ self.datasets[self.key].energy_loss *= (self.sidebar[3, 0].value/dispersion)
+ self.sidebar[3, 0].value = dispersion
+ offset = self.start_cursor.value - self.start_channel * dispersion
+ self.datasets[self.key].energy_loss += (self.sidebar[2, 0].value-self.datasets[self.key].energy_loss[0])
+ self.sidebar[2, 0].value = offset
+ self.plot()
+
+ def set_binning(self, value):
+ if 'SPECTRAL' in self.dataset.data_type.name:
+ bin_x = self.sidebar[15,0].value
+ bin_y = self.sidebar[16,0].value
+ self.dataset.view.set_bin([bin_x, bin_y])
+ self.datasets[self.key].metadata['experiment']['SI_bin_x'] = bin_x
+ self.datasets[self.key].metadata['experiment']['SI_bin_y'] = bin_y
+
+ def _udpate_sidbar(self):
+ spectrum_list = []
+ reference_list =[('None', -1)]
+ for index, key in enumerate(self.datasets.keys()):
+ if 'Reference' not in key:
+ if 'SPECTR' in self.datasets[key].data_type.name:
+ spectrum_list.append((f'{key}: {self.datasets[key].title}', index))
+ reference_list.append((f'{key}: {self.datasets[key].title}', index))
+
+ self.sidebar[0,0].options = spectrum_list
+ self.sidebar[9,0].options = reference_list
+
+ if 'SPECTRUM' in self.dataset.data_type.name:
+ for i in range(14, 17):
+ self.sidebar[i, 0].layout.display = "none"
+ else:
+ for i in range(14, 17):
+ self.sidebar[i, 0].layout.display = "flex"
+ #self.sidebar[0,0].value = dataset_index #f'{self.key}: {self.datasets[self.key].title}'
+ self.sidebar[2,0].value = np.round(self.datasets[self.key].energy_loss[0], 3)
+ self.sidebar[3,0].value = np.round(self.datasets[self.key].energy_loss[1] - self.datasets[self.key].energy_loss[0], 4)
+ self.sidebar[5,0].value = np.round(self.datasets[self.key].metadata['experiment']['convergence_angle'], 1)
+ self.sidebar[6,0].value = np.round(self.datasets[self.key].metadata['experiment']['collection_angle'], 1)
+ self.sidebar[7,0].value = np.round(self.datasets[self.key].metadata['experiment']['acceleration_voltage']/1000, 1)
+ self.sidebar[10,0].value = np.round(self.datasets[self.key].metadata['experiment']['exposure_time'], 4)
+ if 'flux_ppm' not in self.datasets[self.key].metadata['experiment']:
+ self.datasets[self.key].metadata['experiment']['flux_ppm'] = 0
+ self.sidebar[11,0].value = self.datasets[self.key].metadata['experiment']['flux_ppm']
+ if 'count_conversion' not in self.datasets[self.key].metadata['experiment']:
+ self.datasets[self.key].metadata['experiment']['count_conversion'] = 1
+ self.sidebar[12,0].value = self.datasets[self.key].metadata['experiment']['count_conversion']
+ if 'beam_current' not in self.datasets[self.key].metadata['experiment']:
+ self.datasets[self.key].metadata['experiment']['beam_current'] = 0
+ self.sidebar[13,0].value = self.datasets[self.key].metadata['experiment']['beam_current']
+
+ def update_dataset(self):
+ dataset_index = self.sidebar[0, 0].value
+ self.set_dataset(dataset_index)
+
+ def set_action(self):
+ self.sidebar[0,0].observe(self.update_dataset)
+ self.sidebar[1,0].on_click(self.cursor2energy_scale)
+ self.sidebar[2,0].observe(self.set_energy_scale, names='value')
+ self.sidebar[3,0].observe(self.set_energy_scale, names='value')
+ self.sidebar[5,0].observe(self.set_microscope_parameter)
+ self.sidebar[6,0].observe(self.set_microscope_parameter)
+ self.sidebar[7,0].observe(self.set_microscope_parameter)
+ self.sidebar[9,0].observe(self.set_flux)
+ self.sidebar[9,2].observe(self.set_y_scale, names='value')
+ self.sidebar[10,0].observe(self.set_flux)
+ self.sidebar[15,0].observe(self.set_binning)
+ self.sidebar[16,0].observe(self.set_binning)
+
+[docs]def get_low_loss_sidebar():
+ side_bar = ipywidgets.GridspecLayout(17, 3,width='auto', grid_gap="0px")
+
+ side_bar[0, :2] = ipywidgets.Dropdown(
+ options=[('None', 0)],
+ value=0,
+ description='Main Dataset:',
+ disabled=False)
+
+ row = 1
+ side_bar[row, :3] = ipywidgets.Button(description='Fix Energy Scale',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Offset:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Dispersion:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+
+ row += 1
+ side_bar[row, :3] = ipywidgets.Button(description='Resolution_function',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.3, description='Fit Window:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row,:2] = ipywidgets.ToggleButton(
+ description='Show Resolution Function',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Changes y-axis to probability if flux is given',
+ layout=ipywidgets.Layout(width='100px'))
+ side_bar[row,2] = ipywidgets.ToggleButton(
+ description='Probability',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Changes y-axis to probability if flux is given',
+ layout=ipywidgets.Layout(width='100px'))
+ row += 2
+
+ side_bar[row, :3] = ipywidgets.Button(description='Drude Fit',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row+=1
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=[('None', 0)],
+ value=0,
+ description='Reference:',
+ disabled=False)
+ side_bar[row,2] = ipywidgets.ToggleButton(
+ description='Probability',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Changes y-axis to probability if flux is given',
+ layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Exp_Time:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="s", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Flux:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="Mcounts", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Conversion:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value=r"e$^-$/counts", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Current:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="pA", layout=ipywidgets.Layout(width='100px') )
+
+ row += 1
+
+ side_bar[row, :3] = ipywidgets.Button(description='Spectrum Image',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+
+ row += 1
+ side_bar[row, :2] = ipywidgets.IntText(value=1, description='bin X:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.IntText(value=1, description='bin X:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+
+ for i in range(14, 17):
+ pass
+ # side_bar[i, 0].layout.display = "none"
+ return side_bar
+
+[docs]class LowLossWidget(EELSWidget):
+ def __init__(self, datasets):
+ sidebar = get_low_loss_sidebar()
+ super().__init__(datasets, sidebar)
+ self.sidebar[3,0].value = self.energy_scale[0]
+ self.sidebar[4,0].value = self.energy_scale[1] - self.energy_scale[0]
+
+ self.set_action()
+
+ def _udpate_sidbar(self):
+ spectrum_list = []
+ reference_list =[('None', -1)]
+ for index, key in enumerate(self.datasets.keys()):
+ if 'Reference' not in key:
+ if 'SPECTR' in self.datasets[key].data_type.name:
+ spectrum_list.append((f'{key}: {self.datasets[key].title}', index))
+ reference_list.append((f'{key}: {self.datasets[key].title}', index))
+
+ self.sidebar[0,0].options = spectrum_list
+ self.sidebar[9,0].options = reference_list
+
+ if 'SPECTRUM' in self.dataset.data_type.name:
+ for i in range(14, 17):
+ self.sidebar[i, 0].layout.display = "none"
+ else:
+ for i in range(14, 17):
+ self.sidebar[i, 0].layout.display = "flex"
+
+ def get_resolution_function(self, value):
+ self.datasets['resolution_functions'] = eels_tools.get_resolution_functions(self.dataset, zero_loss_fit_width=self.sidebar[5,0].value)
+ if 'low_loss' not in self.dataset.metadata:
+ self.dataset.metadata['low_loss'] = {}
+ self.dataset.metadata['low_loss'].update(self.datasets['resolution_functions'].metadata['low_loss'])
+ self.sidebar[6,0].value = True
+
+ def update_dataset(self):
+ dataset_index = self.sidebar[0, 0].value
+ self.set_dataset(dataset_index)
+
+ def set_action(self):
+ self.sidebar[0,0].observe(self.update_dataset)
+ self.sidebar[1,0].on_click(self.fix_energy_scale)
+ self.sidebar[2,0].observe(self.set_energy_scale, names='value')
+ self.sidebar[3,0].observe(self.set_energy_scale, names='value')
+ self.sidebar[4,0].on_click(self.get_resolution_function)
+ self.sidebar[6,2].observe(self.set_y_scale, names='value')
+ self.sidebar[6,0].observe(self._update, names='value')
+
+ def fix_energy_scale(self, value=0):
+ self.dataset = eels_tools.shift_on_same_scale(self.dataset)
+ self.datasets[self.key] = self.dataset
+ if 'resolution_functions' in self.datasets:
+ self.datasets['resolution_functions'] = eels_tools.shift_on_same_scale(self.datasets['resolution_functions'])
+ self._update()
+
+
+ def set_y_scale(self, value):
+ self.change_y_scale = 1.0/self.y_scale
+ if self.sidebar[6,2].value:
+ dispersion = self.dataset.energy_loss[1] - self.dataset.energy_loss[0]
+ if self.dataset.data_type.name == 'SPECTRUM':
+ sum = self.dataset.sum()
+ else:
+ image_dims = self.dataset.get_dimensions_by_type(sidpy.DimensionType.SPATIAL)
+ sum = np.average(self.dataset, axis=image_dims).sum()
+
+ self.y_scale = 1/sum * dispersion * 1e6
+ # self.datasets[self.key].metadata['experiment']['flux_ppm'] * dispersion
+ self.ylabel='scattering probability (ppm)'
+ else:
+ self.y_scale = 1.0
+ self.ylabel='intensity (counts)'
+ self.change_y_scale *= self.y_scale
+ self._update()
+
+ def _update(self, ev=0):
+ super()._update(ev)
+ if self.sidebar[6,0].value:
+ if 'resolution_functions' in self.datasets:
+ resolution_function = self.get_additional_spectrum('resolution_functions')
+ self.axis.plot(self.energy_scale, resolution_function, label='resolution_function')
+ self.axis.legend()
+
+ def get_additional_spectrum(self, key):
+ if key not in self.datasets.keys():
+ return
+
+ if self.datasets[key].data_type == sidpy.DataType.SPECTRUM:
+ self.spectrum = self.datasets[key].copy()
+ else:
+ image_dims = self.datasets[key].get_dimensions_by_type(sidpy.DimensionType.SPATIAL)
+ selection = []
+ for dim, axis in self.datasets[key]._axes.items():
+ # print(dim, axis.dimension_type)
+ if axis.dimension_type == sidpy.DimensionType.SPATIAL:
+ if dim == image_dims[0]:
+ selection.append(slice(self.x, self.x + self.bin_x))
+ else:
+ selection.append(slice(self.y, self.y + self.bin_y))
+
+ elif axis.dimension_type == sidpy.DimensionType.SPECTRAL:
+ selection.append(slice(None))
+ elif axis.dimension_type == sidpy.DimensionType.CHANNEL:
+ selection.append(slice(None))
+ else:
+ selection.append(slice(0, 1))
+
+ self.spectrum = self.datasets[key][tuple(selection)].mean(axis=tuple(image_dims))
+
+ self.spectrum *= self.y_scale
+
+ return self.spectrum.squeeze()
+
+ def set_binning(self, value):
+ if 'SPECTRAL' in self.dataset.data_type.name:
+ bin_x = self.sidebar[15,0].value
+ bin_y = self.sidebar[16,0].value
+ self.dataset.view.set_bin([bin_x, bin_y])
+ self.datasets[self.key].metadata['experiment']['SI_bin_x'] = bin_x
+ self.datasets[self.key].metadata['experiment']['SI_bin_y'] = bin_y
+
+
+"""
+kinematic_scattering
+Copyright by Gerd Duscher
+
+The University of Tennessee, Knoxville
+Department of Materials Science & Engineering
+
+Sources:
+ Scattering Theory:
+ Zuo and Spence, "Advanced TEM", 2017
+
+ Spence and Zuo, Electron Microdiffraction, Plenum 1992
+
+ Atomic Form Factor:
+ Kirkland: Advanced Computing in Electron Microscopy 2nd edition
+ Appendix C
+
+Units:
+ everything is in SI units, except length which is given in Angstrom.
+
+Usage:
+ See the notebooks for examples of these routines
+
+All the input and output is done through a ase.Atoms object and the dictionary in the info attribute
+"""
+
+# numerical packages used
+import numpy as np
+import scipy.constants as const
+import itertools
+
+# plotting package used
+import matplotlib.pylab as plt # basic plotting
+
+import pyTEMlib.file_tools as ft
+from pyTEMlib.crystal_tools import *
+from pyTEMlib.diffraction_plot import *
+
+_version_ = "0.2022.1.0"
+
+print('Using kinematic_scattering library version {_version_ } by G.Duscher')
+
+inputKeys = ['acceleration_voltage_V', 'zone_hkl', 'Sg_max', 'hkl_max']
+optional_inputKeys = ['crystal', 'lattice_parameter_nm', 'convergence_angle_mrad', 'mistilt', 'thickness',
+ 'dynamic correction', 'dynamic correction K0']
+
+
+[docs]def read_poscar(filename):
+ print('read_poscar and read_cif moved to file_tools, \n'
+ 'please use that library in the future!')
+ ft.read_poscar(filename)
+
+
+[docs]def example(verbose=True):
+ """
+ same as Zuo_fig_3_18
+ """
+ print('\n##########################')
+ print('# Start of Example Input #')
+ print('##########################\n')
+ print('Define only mandatory input: ', inputKeys)
+ print(' Kinematic diffraction routine will set optional input : ', optional_inputKeys)
+
+ return Zuo_fig_3_18(verbose=verbose)
+
+
+[docs]def Zuo_fig_3_18(verbose=True):
+ """
+ Input for Figure 3.18 in Zuo and Spence \"Advanced TEM\", 2017
+
+ This input acts as an example as well as a reference
+
+ Parameters:
+ -----------
+ verbose: boolean:
+ optional to see output
+ Returns:
+ -------
+ atoms: ase.Atoms
+ Silicon crystal structure
+ e
+ dictionary: tags is the dictionary of all input and output parameter needed to reproduce that figure.
+ """
+
+ # INPUT
+ # Create Silicon structure (Could be produced with Silicon routine)
+ if verbose:
+ print('Sample Input for Figure 3.18 in Zuo and Spence \"Advanced TEM\", 2017')
+ import ase
+ import ase.build
+ a = 5.14 # A
+ atoms = ase.build.bulk('Si', 'diamond', a=a, cubic=True)
+
+ experiment = {'acceleration_voltage_V': 99.2 * 1000.0, # V
+ 'convergence_angle_mrad': 7.15, # mrad;
+ 'zone_hkl': np.array([-2, 2, 1]),
+ 'mistilt': np.array([0, 0, 0]), # mistilt in degrees
+ 'Sg_max': .03, # 1/A maximum allowed excitation error
+ 'hkl_max': 9 # Highest evaluated Miller indices
+ }
+ # Define Experimental Conditions
+ if verbose:
+ print('###########################')
+ print('# Experimental Conditions #')
+ print('###########################')
+
+ for key, value in experiment.items():
+ print(f'tags[\'{key}\'] =', value)
+
+ print('##################')
+ print('# Output Options #')
+ print('##################')
+
+ # Output options
+ output = {'background': 'black', # 'white' 'grey'
+ 'color_map': 'plasma',
+ 'plot_HOLZ': True,
+ 'plot_HOLZ_excess': True,
+ 'plot_Kikuchi': True,
+ 'plot_reflections': True,
+ 'label_HOLZ': False,
+ 'label_Kikuchi': False,
+ 'label_reflections': False,
+ 'label_color': 'black',
+ 'label_size': 10,
+ 'color_Laue_Zones': ['red', 'blue', 'green', 'blue', 'green'], # for OLZ give a sequence
+ 'color_Kikuchi': 'green',
+ 'linewidth_HOLZ': -1, # -1: linewidth according to intensity (structure factor F^2)
+ 'linewidth_Kikuchi': -1, # -1: linewidth according to intensity (structure factor F^2)
+ 'color_reflections': 'intensity', # 'Laue Zone'
+ 'color_zero': 'white', # 'None', 'white', 'blue'
+ 'color_ring_zero': 'None' # 'Red' #'white' #, 'None'
+ }
+
+ if verbose:
+ for key, value in output.items():
+ print(f'tags[\'{key}\'] =', value)
+ print('########################')
+ print('# End of Example Input #')
+ print('########################\n\n')
+
+ if atoms.info is None:
+ atoms.info = {}
+ atoms.info['experimental'] = experiment
+ atoms.info['output'] = output
+
+ return atoms
+
+
+[docs]def zone_mistilt(zone, angles):
+ """ Rotation of zone axis by mistilt
+
+ Parameters
+ ----------
+ zone: list or numpy array of int
+ zone axis in Miller indices
+ angles: ist or numpy array of float
+ list of mistilt angles in degree
+
+ Returns
+ -------
+ new_zone_axis: np.ndarray (3)
+ new tilted zone axis
+ """
+
+ if not isinstance(angles, (np.ndarray, list)):
+ raise TypeError('angles must be a list of float of length 3')
+ if len(angles) != 3:
+ raise TypeError('angles must be a list of float of length 3')
+ if not isinstance(zone, (np.ndarray, list)):
+ raise TypeError('Miller indices must be a list of int of length 3')
+
+ alpha, beta, gamma = np.radians(angles)
+
+ # first we rotate alpha about x-axis
+ c, s = np.cos(alpha), np.sin(alpha)
+ rot_x = np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
+
+ # second we rotate beta about y-axis
+ c, s = np.cos(beta), np.sin(beta)
+ rot_y = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
+
+ # third we rotate gamma about z-axis
+ c, s = np.cos(gamma), np.sin(gamma)
+ rot_z = np.array([[c, -s, 0], [s, c, 0], [0, 0, 1]])
+
+ return np.dot(np.dot(np.dot(zone, rot_x), rot_y), rot_z)
+
+
+[docs]def get_metric_tensor(matrix):
+ """The metric tensor of the lattice."""
+ metric_tensor2 = np.dot(matrix, matrix.T)
+ return metric_tensor2
+
+
+[docs]def vector_norm(g):
+ """ Length of vector
+
+ depreciated - use np.linalg.norm
+ """
+ g = np.array(g)
+ return np.sqrt(g[:, 0] ** 2 + g[:, 1] ** 2 + g[:, 2] ** 2)
+
+
+[docs]def get_wavelength(acceleration_voltage):
+ """
+ Calculates the relativistic corrected de Broglie wavelength of an electron in Angstrom
+
+ Parameter:
+ ---------
+ acceleration_voltage: float
+ acceleration voltage in volt
+ Returns:
+ -------
+ wavelength: float
+ wave length in Angstrom
+ """
+ if not isinstance(acceleration_voltage, (int, float)):
+ raise TypeError('Acceleration voltage has to be a real number')
+ eU = const.e * acceleration_voltage
+ return const.h/np.sqrt(2*const.m_e*eU*(1+eU/(2*const.m_e*const.c**2)))*10**10
+
+
+[docs]def find_nearest_zone_axis(tags):
+ """Test all zone axis up to a maximum of hkl_max"""
+
+ hkl_max = 5
+ # Make all hkl indices
+ h = np.linspace(-hkl_max, hkl_max, 2 * hkl_max + 1) # all evaluated single Miller Indices
+ hkl = np.array(list(itertools.product(h, h, h))) # all evaluated Miller indices
+
+ # delete [0,0,0]
+ index = int(len(hkl) / 2)
+ zones_hkl = np.delete(hkl, index, axis=0) # delete [0,0,0]
+
+ # make zone axis in reciprocal space
+ zones_g = np.dot(zones_hkl, tags['reciprocal_unit_cell']) # all evaluated reciprocal_unit_cell points
+
+ # make zone axis in microscope coordinates of reciprocal space
+ zones_g = np.dot(zones_g, tags['rotation_matrix']) # rotate these reciprocal_unit_cell points
+
+ # calculate angles with z-axis
+ zones_g_norm = vector_norm(zones_g)
+ z_axis = np.array([0, 0, 1])
+
+ zones_angles = np.abs(np.arccos(np.dot((zones_g.T / zones_g_norm).T, z_axis)))
+
+ # get smallest angle
+ smallest = (zones_angles - zones_angles.min()) < 0.001
+ if smallest.sum() > 1: # multiples of Miller index of zone axis have same angle
+ zone = zones_hkl[smallest]
+ zone_index = abs(zone).sum(axis=1)
+ ind = zone_index.argmin()
+ zone_hkl = zone[ind]
+ else:
+ zone_hkl = zones_hkl[smallest][0]
+
+ tags['nearest_zone_axis'] = zone_hkl
+
+ # get other zone axes up to 5 degrees away
+ others = np.logical_not(smallest)
+ next_smallest = (zones_angles[others]) < np.deg2rad(5.)
+ ind = np.argsort((zones_angles[others])[next_smallest])
+
+ tags['next_nearest_zone_axes'] = ((zones_hkl[others])[next_smallest])[ind]
+
+ return zone_hkl
+
+
+[docs]def find_angles(zone):
+ """Microscope stage coordinates of zone"""
+
+ # rotation around y-axis
+ r = np.sqrt(zone[1] ** 2 + zone[2] ** 2)
+ alpha = np.arctan(zone[0] / r)
+ if zone[2] < 0:
+ alpha = np.pi - alpha
+ # rotation around x-axis
+ if zone[2] == 0:
+ beta = np.pi / 2 * np.sign(zone[1])
+ else:
+ beta = (np.arctan(zone[1] / zone[2]))
+ return alpha, beta
+
+
+[docs]def stage_rotation_matrix(alpha, beta):
+ """ Microscope stage coordinate system """
+
+ # FIRST we rotate beta about x-axis
+ c, s = np.cos(beta), np.sin(beta)
+ rot_x = np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
+ # second we rotate alpha about y-axis
+ c, s = np.cos(alpha), np.sin(alpha)
+ rot_y = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
+
+ return np.dot(rot_x, rot_y)
+
+
+# ##################
+# Determine rotation matrix to tilt zone axis onto z-axis
+# We determine spherical coordinates to do that
+# ##################
+
+[docs]def get_rotation_matrix(tags):
+ """zone axis in global coordinate system"""
+
+ zone_hkl = tags['zone_hkl']
+ zone = np.dot(zone_hkl, tags['reciprocal_unit_cell'])
+
+ # angle of zone with Z around x,y:
+ alpha, beta = find_angles(zone)
+
+ alpha = alpha + tags['mistilt_alpha']
+ beta = beta + tags['mistilt_beta']
+
+ tags['y-axis rotation alpha'] = alpha
+ tags['x-axis rotation beta'] = beta
+
+ tags['rotation_matrix'] = rotation_matrix = stage_rotation_matrix(alpha, -beta)
+
+ # the rotation now makes z-axis coincide with plane normal
+
+ zone_nearest = find_nearest_zone_axis(tags)
+ tags['nearest_zone_axis'] = zone_nearest
+
+ # tilt angles of coordinates of nearest zone
+ zone_nearest = np.dot(zone_nearest, tags['reciprocal_unit_cell'])
+
+ alpha_nearest, beta_nearest = find_angles(zone_nearest)
+
+ # calculate mistilt of nearest zone axis
+ tags['mistilt_nearest_zone alpha'] = alpha - alpha_nearest
+ tags['mistilt_nearest_zone beta'] = beta - beta_nearest
+
+ tags['nearest_zone_axes'] = {}
+ tags['nearest_zone_axes']['0'] = {}
+ tags['nearest_zone_axes']['0']['hkl'] = tags['nearest_zone_axis']
+ tags['nearest_zone_axes']['0']['mistilt_alpha'] = alpha - alpha_nearest
+ tags['nearest_zone_axes']['0']['mistilt_beta'] = beta - beta_nearest
+
+ # find polar coordinates of next nearest zones
+ tags['nearest_zone_axes']['amount'] = len(tags['next_nearest_zone_axes']) + 1
+
+ for i in range(len(tags['next_nearest_zone_axes'])):
+ zone_n = tags['next_nearest_zone_axes'][i]
+ tags['nearest_zone_axes'][str(i + 1)] = {}
+ tags['nearest_zone_axes'][str(i + 1)]['hkl'] = zone_n
+
+ zone_near = np.dot(zone_n, tags['reciprocal_unit_cell'])
+ # zone_near_g = np.dot(zone_near,rotation_matrix)
+
+ tags['nearest_zone_axes'][str(i + 1)]['g'] = zone_near
+ alpha_nearest, beta_nearest = find_angles(zone_near)
+
+ tags['nearest_zone_axes'][str(i + 1)]['mistilt_alpha'] = alpha - alpha_nearest
+ tags['nearest_zone_axes'][str(i + 1)]['mistilt_beta'] = beta - beta_nearest
+ # print('other' , i, np.rad2deg([alpha, alpha_nearest, beta, beta_nearest]))
+
+ return rotation_matrix
+
+
+[docs]def check_sanity(atoms, verbose_level=0):
+ """
+ Check sanity of input parameters
+ """
+ stop = False
+ output = atoms.info['output']
+ tags = atoms.info['experimental']
+ for key in ['acceleration_voltage_V']:
+ if key not in tags:
+ print(f'Necessary parameter {key} not defined')
+ stop = True
+ if 'SpotPattern' not in output:
+ output['SpotPattern'] = False
+ if output['SpotPattern']:
+ if 'zone_hkl' not in tags:
+ print(' No zone_hkl defined')
+ stop = True
+ if 'Sg_max' not in tags:
+ print(' No Sg_max defined')
+ stop = True
+ if 'hkl_max' not in tags:
+ print(' No hkl_max defined')
+ stop = True
+
+ if stop:
+ print('Input is not complete, stopping')
+ print('Try \'example()\' for example input')
+ return False
+ ############################################
+ # Check optional input
+ ############################################
+
+ if output['SpotPattern']:
+ if 'mistilt_alpha degree' not in tags:
+ # mistilt is in microscope coordinates
+ tags['mistilt_alpha'] = tags['mistilt_alpha degree'] = 0.0
+ if verbose_level > 0:
+ print('Setting undefined input: tags[\'mistilt_alpha\'] = 0.0 ')
+ else:
+ tags['mistilt_alpha'] = np.deg2rad(tags['mistilt_alpha degree'])
+
+ if 'mistilt_beta degree' not in tags:
+ # mistilt is in microscope coordinates
+ tags['mistilt_beta'] = tags['mistilt_beta degree'] = 0.0
+ if verbose_level > 0:
+ print('Setting undefined input: tags[\'mistilt_beta\'] = 0.0')
+ else:
+ tags['mistilt_beta'] = np.deg2rad(tags['mistilt_beta degree'])
+
+ if 'convergence_angle_mrad' not in tags:
+ tags['convergence_angle_mrad'] = 0.
+ if verbose_level > 0:
+ print('Setting undefined input: tags[\'convergence_angle_mrad\'] = 0')
+
+ if 'thickness' not in tags:
+ tags['thickness'] = 0.
+ if verbose_level > 0:
+ print('Setting undefined input: tags[\'thickness\'] = 0')
+ if 'dynamic correction' not in tags:
+ tags['dynamic correction'] = 0.
+ if verbose_level > 0:
+ print('Setting undefined input: tags[\'dynamic correction\'] = False')
+ if 'dynamic correction K0' not in tags:
+ tags['dynamic correction K0'] = 0.
+ if verbose_level > 0:
+ print('Setting undefined input: tags[\'dynamic correction k0\'] = False')
+ return not stop
+
+
+[docs]def scattering_matrix(tags, verbose_level=1):
+ """ Scattering matrix"""
+ if not check_sanity(tags, verbose_level):
+ return
+ # ##
+ # Pair distribution Function
+ # ##
+ unit_cell = np.array(tags['unit_cell'])
+ base = tags['base']
+
+ atom_coordinates = np.dot(base, unit_cell)
+
+ n = 20
+ x = np.linspace(-n, n, 2 * n + 1) # all evaluated multiples of x
+ xyz = np.array(list(itertools.product(x, x, x))) # all evaluated multiples in all direction
+
+ mat = np.dot(xyz, unit_cell) # all evaluated unit_cells
+
+ atom = {}
+
+ for i in range(len(atom_coordinates)):
+ distances = np.linalg.norm(mat + atom_coordinates[i], axis=1)
+ if i == 0:
+ all_distances = distances
+ else:
+ all_distances = np.append(all_distances, distances)
+ unique, counts = np.unique(distances, return_counts=True)
+
+ atom[str(i)] = dict(zip(unique, counts))
+ print(atom[str(i)])
+
+ all_distances = np.append(all_distances, distances)
+ unique, counts = np.unique(all_distances, return_counts=True)
+
+ plt.plot(unique, counts)
+ plt.show()
+
+
+[docs]def ring_pattern_calculation(atoms, verbose=False):
+ """
+ Calculate the ring diffraction pattern of a crystal structure
+
+ Parameters
+ ----------
+ atoms: Crystal
+ crystal structure
+ verbose: verbose print-outs
+ set to False
+ Returns
+ -------
+ tags: dict
+ dictionary with diffraction information added
+ """
+
+ # Check sanity
+ if not check_sanity(atoms, verbose):
+ return
+
+ tags = atoms.info['experimental']
+ # wavelength
+ tags['wave_length'] = get_wavelength(tags['acceleration_voltage_V'])
+
+ # volume of unit_cell
+ unit_cell = atoms.cell.array
+ metric_tensor = get_metric_tensor(unit_cell) # converts hkl to g vectors and back
+ tags['metric_tensor'] = metric_tensor
+ # volume_unit_cell = np.sqrt(np.linalg.det(metric_tensor))
+
+ # reciprocal_unit_cell
+
+ # We use the linear algebra package of numpy to invert the unit_cell "matrix"
+ reciprocal_unit_cell = atoms.cell.reciprocal() # np.linalg.inv(unit_cell).T # transposed of inverted unit_cell
+ tags['reciprocal_unit_cell'] = reciprocal_unit_cell
+ # inverse_metric_tensor = get_metric_tensor(reciprocal_unit_cell)
+
+ hkl_max = tags['hkl_max']
+
+ h = np.linspace(-hkl_max, hkl_max, 2 * hkl_max + 1) # all evaluated single Miller Indices
+ hkl = np.array(list(itertools.product(h, h, h))) # all evaluated Miller indices
+
+ # delete [0,0,0]
+ index_center = int(len(hkl) / 2)
+ hkl = np.delete(hkl, index_center, axis=0) # delete [0,0,0]
+
+ g_hkl = np.dot(hkl, reciprocal_unit_cell) # all evaluated reciprocal_unit_cell points
+
+ ##################################
+ # Calculate Structure Factors
+ #################################
+
+ structure_factors = []
+ for j in range(len(g_hkl)):
+ F = 0
+ for b in range(len(atoms)):
+ f = feq(atoms[b].symbol, np.linalg.norm(g_hkl[j]))
+ F += f * np.exp(-2 * np.pi * 1j * (hkl[j] * atoms.get_scaled_positions()[b]).sum())
+
+ structure_factors.append(F)
+
+ F = structure_factors = np.array(structure_factors)
+
+ # Sort reflection in allowed and forbidden #
+
+ allowed = np.absolute(F) > 0.000001 # allowed within numerical error
+
+ if verbose:
+ print('Of the {0} possible reflection {1} are allowed.'.format(hkl.shape[0], allowed.sum()))
+
+ # information of allowed reflections
+ hkl_allowed = hkl[allowed][:]
+ g_allowed = g_hkl[allowed, :]
+ F_allowed = F[allowed]
+ g_norm_allowed = vector_norm(g_allowed) # length of all vectors = 1/
+
+ ind = np.argsort(g_norm_allowed)
+ g_norm_sorted = g_norm_allowed[ind]
+ hkl_sorted = hkl_allowed[ind][:]
+ F_sorted = F_allowed[ind]
+
+ unique, counts = np.unique(np.around(g_norm_sorted, decimals=5), return_counts=True)
+ if verbose:
+ print('Of the {0} allowed reflection {1} have unique distances.'.format(allowed.sum(), len(unique)))
+
+ reflections_d = []
+ reflections_m = []
+ reflections_F = []
+
+ start = 0
+ for i in range(len(unique)):
+ end = start + counts[i]
+ hkl_max = np.argmax(hkl_sorted[start:end].sum(axis=1))
+
+ reflections_d.append(g_norm_sorted[start])
+ reflections_m.append(hkl_sorted[start + hkl_max])
+ reflections_F.append(F_sorted[start]) # :end].sum())
+
+ start = end
+
+ if verbose:
+ print('\n\n [hkl] \t 1/d [1/nm] \t d [nm] \t F^2 ')
+ for i in range(len(unique)):
+ print(' {0} \t {1:.2f} \t {2:.4f} \t {3:.2f} '
+ .format(reflections_m[i], unique[i]*10., 1 / unique[i]/10., np.real(reflections_F[i]) ** 2))
+
+ atoms.info['Ring_Pattern'] = {}
+ atoms.info['Ring_Pattern']['allowed'] = {}
+ atoms.info['Ring_Pattern']['allowed']['hkl'] = reflections_m
+ atoms.info['Ring_Pattern']['allowed']['g norm'] = unique
+ atoms.info['Ring_Pattern']['allowed']['structure factor'] = reflections_F
+ atoms.info['Ring_Pattern']['allowed']['multiplicity'] = counts
+
+ atoms.info['Ring_Pattern']['profile_x'] = np.linspace(0, unique.max(), 2048)
+ step_size = atoms.info['Ring_Pattern']['profile_x'][1]
+ intensity = np.zeros(2048)
+ x_index = [(unique / step_size + 0.5).astype(int)]
+ intensity[x_index] = np.array(np.real(reflections_F)) * np.array(np.real(reflections_F))
+ atoms.info['Ring_Pattern']['profile_y delta'] = intensity
+
+ def gaussian(xx, pp):
+ s1 = pp[2] / 2.3548
+ prefactor = 1.0 / np.sqrt(2 * np.pi * s1 ** 2)
+ y = (pp[1] * prefactor) * np.exp(-(xx - pp[0]) ** 2 / (2 * s1 ** 2))
+ return y
+
+ if 'thickness' in tags:
+ if tags['thickness'] > 0:
+ x = np.linspace(-1024, 1023, 2048) * step_size
+ p = [0.0, 1, 2 / tags['thickness']]
+
+ gauss = gaussian(x, p)
+ intensity = np.convolve(np.array(intensity), np.array(gauss), mode='same')
+ atoms.info['Ring_Pattern']['profile_y'] = intensity
+
+ # Make pretty labels
+ hkl_allowed = reflections_m
+ hkl_label = make_pretty_labels(hkl_allowed)
+ atoms.info['Ring_Pattern']['allowed']['label'] = hkl_label
+
+
+[docs]def get_dynamically_allowed(atoms, verbose=False):
+ if not isinstance(atoms, ase.Atoms):
+ print('we need an ase atoms object as input')
+ if 'diffraction' not in atoms.info:
+ print('Run the kinematic_scattering function first')
+
+ # Dynamically Allowed Reflection
+
+ dif = atoms.info['diffraction']
+ hkl_allowed = dif['allowed']['hkl']
+ hkl_forbidden = dif['forbidden']['hkl']
+ indices = range(len(hkl_allowed))
+ combinations = [list(x) for x in itertools.permutations(indices, 2)]
+ hkl_forbidden = hkl_forbidden.tolist()
+ dynamically_allowed = np.zeros(len(hkl_forbidden), dtype=bool)
+ for [i, j] in combinations:
+ possible = (hkl_allowed[i] + hkl_allowed[j]).tolist()
+ if possible in hkl_forbidden:
+ dynamically_allowed[hkl_forbidden.index(possible)] = True
+ dif['forbidden']['dynamically_allowed'] = dynamically_allowed
+
+ if verbose:
+ print(f"Of the {len(hkl_forbidden)} forbidden reflection {dynamically_allowed.sum()} "
+ f"can be dynamically activated.")
+ # print(dif['forbidden']['hkl'][dynamically_allowed])
+
+
+[docs]def kinematic_scattering(atoms, verbose=False):
+ """
+ All kinematic scattering calculation
+
+ Calculates Bragg spots, Kikuchi lines, excess, and deficient HOLZ lines
+
+ Parameters
+ ----------
+ atoms: ase.Atoms
+ object with crystal structure:
+ and with experimental parameters in info attribute:
+ 'acceleration_voltage_V', 'zone_hkl', 'Sg_max', 'hkl_max'
+ Optional parameters are:
+ 'mistilt', convergence_angle_mrad', and 'crystal_name'
+ verbose = True will give extended output of the calculation
+ verbose: boolean
+ default is False
+
+ Returns
+ -------
+ atoms:
+ There are three sub_dictionaries in info attribute:
+ ['allowed'], ['forbidden'], and ['HOLZ']
+ ['allowed'] and ['forbidden'] dictionaries contain:
+ ['Sg'], ['hkl'], ['g'], ['structure factor'], ['intensities'],
+ ['ZOLZ'], ['FOLZ'], ['SOLZ'], ['HOLZ'], ['HHOLZ'], ['label'], and ['Laue_zone']
+ the ['HOLZ'] dictionary contains:
+ ['slope'], ['distance'], ['theta'], ['g_deficient'], ['g_excess'], ['hkl'], ['intensities'],
+ ['ZOLZ'], ['FOLZ'], ['SOLZ'], ['HOLZ'], and ['HHOLZ']
+ Please note that the Kikuchi lines are the HOLZ lines of ZOLZ
+
+ There are also a few parameters stored in the main dictionary:
+ ['wave_length_nm'], ['reciprocal_unit_cell'], ['inner_potential_V'], ['incident_wave_vector'],
+ ['volume'], ['theta'], ['phi'], and ['incident_wave_vector_vacuum']
+ """
+
+ # Check sanity
+ if atoms.info is None:
+ atoms.info = {'output': {}, 'experimental': {}}
+ elif 'output' in atoms.info:
+ output = atoms.info['output']
+ else:
+ output = atoms.info['output'] = {}
+
+ output['SpotPattern'] = True
+
+ if 'experimental' not in atoms.info:
+ tags = atoms.info['experimental'] = {}
+
+ if not check_sanity(atoms):
+ print('Input is not complete, stopping')
+ print('Try \'example()\' for example input')
+ return
+
+ tags = atoms.info['experimental']
+
+ tags['wave_length'] = get_wavelength(tags['acceleration_voltage_V'])
+
+ # ###########################################
+ # reciprocal_unit_cell
+ # ###########################################
+ unit_cell = atoms.cell.array
+ tags['unit_cell'] = unit_cell
+ metric_tensor = get_metric_tensor(unit_cell) # converts hkl to g vectors and back
+ tags['metric_tensor'] = metric_tensor
+ volume_unit_cell = atoms.cell.volume
+
+ # We use the linear algebra package of numpy to invert the unit_cell "matrix"
+ reciprocal_unit_cell = atoms.cell.reciprocal() # np.linalg.inv(unit_cell).T # transposed of inverted unit_cell
+ tags['reciprocal_unit_cell'] = reciprocal_unit_cell
+ inverse_metric_tensor = get_metric_tensor(reciprocal_unit_cell)
+
+ # ###########################################
+ # Incident wave vector k0 in vacuum and material
+ # ###########################################
+
+ # Incident wave vector K0 in vacuum and material
+ u0 = 0.0 # in (Ang)
+ # atom form factor of zero reflection angle is the inner potential in 1/A
+ for i in range(len(atoms)):
+ u0 += feq(atoms[i].symbol, 0.0)
+
+ angstrom_conversion = 1.0e10 # So [1A (in m)] * angstrom_conversion = 1
+ # NanometerConversion = 1.0e9
+
+ scattering_factor_to_volts = (const.h ** 2) * (1e10 ** 2) / (2 * np.pi * const.m_e * const.e) * volume_unit_cell
+ tags['inner_potential_V'] = u0 * scattering_factor_to_volts
+ if verbose:
+ print(f'The inner potential is {u0:.1f} V')
+
+ # Calculating incident wave vector magnitude 'k0' in material
+ wl = tags['wave_length']
+ tags['incident_wave_vector_vacuum'] = 1 / wl
+
+ k_0 = tags['incident_wave_vector'] = np.sqrt(1 / wl**2 + u0/volume_unit_cell) # 1/Ang
+
+ tags['convergence_angle_A-1'] = k_0*np.sin(tags['convergence_angle_mrad']/1000.)
+
+ if verbose:
+ print(f"Using an acceleration voltage of {tags['acceleration_voltage_V']/1000:.1f}kV")
+ print(f'Magnitude of incident wave vector in material: {k_0:.4f} 1/Ang and in vacuum: {1/wl:.4f} 1/Ang')
+ print(f"Which is an wave length of {1/k_0 * 100.:.3f} pm in the material and {wl * 100.:.3f} pm "
+ f"in the vacuum")
+ print(f"The convergence angle of {tags['convergence_angle_mrad']:.1f}mrad "
+ f"= {tags['convergence_angle_A-1']:.2f} 1/A")
+ print(f"Magnitude of incident wave vector in material: {k_0:.1f} 1/A which is a wavelength {100/k_0:.3f} pm")
+
+ # ############
+ # Rotate
+ # ############
+
+ # get rotation matrix to rotate zone axis onto z-axis
+ rotation_matrix = get_rotation_matrix(tags)
+
+ if verbose:
+ print(f"Rotation alpha {np.rad2deg(tags['y-axis rotation alpha']):.1f} degree, "
+ f" beta {np.rad2deg(tags['x-axis rotation beta']):.1f} degree")
+ print(f"from zone axis {tags['zone_hkl']}")
+ print(f"Tilting {1} by {np.rad2deg(tags['mistilt_alpha']):.2f} "
+ f" in alpha and {np.rad2deg(tags['mistilt_beta']):.2f} in beta direction results in :")
+ # list(tags['zone_hkl'])
+ #
+ # print(f"zone axis {list(tags['nearest_zone_axis'])} with a mistilt of "
+ # f"{np.rad2deg(tags['mistilt_nearest_zone alpha']):.2f} in alpha "
+ # f"and {np.rad2deg(tags['mistilt_nearest_zone beta']):.2f} in beta direction")
+ nearest = tags['nearest_zone_axes']
+ print('Next nearest zone axes are:')
+ for i in range(1, nearest['amount']):
+ print(f"{nearest[str(i)]['hkl']}: mistilt: {np.rad2deg(nearest[str(i)]['mistilt_alpha']):6.2f}, "
+ f"{np.rad2deg(nearest[str(i)]['mistilt_beta']):6.2f}")
+ # rotate incident wave vector
+ k0_unit_vector = np.array([0, 0, 1]) # incident unit wave vector
+ k0_vector = k0_unit_vector * k_0 # incident wave vector
+ cent = k0_vector # center of Ewald sphere
+
+ if verbose:
+ print('Center of Ewald sphere ', k0_vector)
+
+ # #######################
+ # Find all Miller indices whose reciprocal point lies near the Ewald sphere with radius k_0
+ # within a maximum excitation error Sg
+ # #######################
+
+ hkl_max = tags['hkl_max']
+ Sg_max = tags['Sg_max'] # 1/Ang maximum allowed excitation error
+
+ h = np.linspace(-hkl_max, hkl_max, 2*hkl_max+1) # all evaluated single Miller Indices
+ hkl = np.array(list(itertools.product(h, h, h))) # all evaluated Miller indices
+ g_non_rot = np.dot(hkl, reciprocal_unit_cell) # all evaluated reciprocal_unit_cell points
+ g_norm = np.linalg.norm(g_non_rot, axis=1) # length of all vectors
+ not_zero = g_norm > 0
+ g_non_rot = g_non_rot[not_zero] # zero reflection will make problems further on, so we exclude it.
+ g_norm = g_norm[not_zero]
+ hkl_all = hkl[not_zero]
+ g = np.dot(g_non_rot, rotation_matrix)
+
+ # #######################
+ # Calculate excitation errors for all reciprocal_unit_cell points
+ # #######################
+
+ # Zuo and Spence, 'Adv TEM', 2017 -- Eq 3:14
+ S = (k_0**2-np.linalg.norm(g - k0_vector, axis=1)**2)/(2*k_0)
+
+ # g_mz = g - k0_vector
+ # in_sqrt = g_mz[:, 2]**2 + np.linalg.norm(g_mz, axis=1)**2 - k_0**2
+ # in_sqrt[in_sqrt < 0] = 0.
+ # S = -g_mz[:, 2] - np.sqrt(in_sqrt)
+
+ # #######################
+ # Determine reciprocal_unit_cell points with excitation error less than the maximum allowed one: Sg_max
+ # #######################
+
+ reflections = abs(S) < Sg_max # This is now a boolean array with True for all possible reflections
+
+ Sg = S[reflections]
+ g_hkl = g[reflections]
+ g_hkl_non_rot = g_non_rot[reflections]
+ hkl = hkl_all[reflections]
+ g_norm = g_norm[reflections]
+
+ if verbose:
+ print('Of the {0} tested reciprocal_unit_cell points, {1} have an excitation error less than {2:.2f} 1/nm'.
+ format(len(g), len(g_hkl), Sg_max))
+
+ # #################################
+ # Calculate Structure Factors
+ # ################################
+
+ structure_factors = []
+ for j in range(len(g_hkl)):
+ F = 0
+ for b in range(len(atoms)):
+ f = feq(atoms[b].symbol, g_norm[j]) # Atomic form factor for element and momentum change (g vector)
+ F += f * np.exp(-2*np.pi*1j*(g_hkl_non_rot[j]*atoms.positions[b]).sum())
+ structure_factors.append(F)
+ F = structure_factors = np.array(structure_factors)
+
+ # ###########################################
+ # Sort reflection in allowed and forbidden #
+ # ###########################################
+
+ allowed = np.absolute(F) > 0.000001 # allowed within numerical error
+
+ if verbose:
+ print('Of the {0} possible reflection {1} are allowed.'.format(hkl.shape[0], allowed.sum()))
+
+ # information of allowed reflections
+ s_g_allowed = Sg[allowed]
+ hkl_allowed = hkl[allowed][:]
+ g_allowed = g_hkl[allowed, :]
+ F_allowed = F[allowed]
+ g_norm_allowed = g_norm[allowed]
+
+ atoms.info['diffraction'] = {}
+ dif = atoms.info['diffraction']
+ dif['allowed'] = {}
+ dif['allowed']['Sg'] = s_g_allowed
+ dif['allowed']['hkl'] = hkl_allowed
+ dif['allowed']['g'] = g_allowed
+ dif['allowed']['structure factor'] = F_allowed
+
+ # Calculate Extinction Distance Reimer 7.23
+ # - makes only sense for non-zero F
+
+ xi_g = np.real(np.pi * volume_unit_cell * k_0 / F_allowed)
+
+ # Calculate Intensity of beams Reimer 7.25
+ if 'thickness' not in tags:
+ tags['thickness'] = 0.
+ thickness = tags['thickness']
+ if thickness > 0.1:
+ I_g = np.real(np.pi ** 2 / xi_g ** 2 * np.sin(np.pi * thickness * s_g_allowed) ** 2 / (np.pi * s_g_allowed)**2)
+ dif['allowed']['Ig'] = I_g
+
+ dif['allowed']['intensities'] = intensities = np.real(F_allowed) ** 2
+
+ # Calculate Extinction Distance Reimer 7.23
+ # - makes only sense for non-zero F
+
+ xi_g = np.real(np.pi * volume_unit_cell * k_0 / F_allowed)
+
+ # ###########################
+ # Calculate Intensities (of allowed reflections)
+ # ###########################
+
+ # Calculate Intensity of beams Reimer 7.25
+ if 'thickness' not in tags:
+ tags['thickness'] = 0.
+ thickness = tags['thickness']
+ if thickness > 0.1:
+ I_g = np.real(np.pi ** 2 / xi_g ** 2 * np.sin(np.pi * thickness * s_g_allowed) ** 2 / (np.pi * s_g_allowed)**2)
+ dif['allowed']['Ig'] = I_g
+
+ dif['allowed']['intensities'] = intensities = np.real(F_allowed) ** 2
+
+ # information of forbidden reflections
+ forbidden = np.logical_not(allowed)
+ Sg_forbidden = Sg[forbidden]
+ hkl_forbidden = hkl[forbidden]
+ g_forbidden = g_hkl[forbidden]
+ F_forbidden = F[forbidden]
+
+ dif['forbidden'] = {}
+ dif['forbidden']['Sg'] = Sg_forbidden
+ dif['forbidden']['hkl'] = hkl_forbidden
+ dif['forbidden']['g'] = g_forbidden
+
+ # ##########################
+ # Make pretty labels
+ # ##########################
+ hkl_label = make_pretty_labels(hkl_allowed)
+ dif['allowed']['label'] = hkl_label
+ hkl_label = make_pretty_labels(hkl_forbidden)
+ dif['forbidden']['label'] = hkl_label
+
+ # Center of Laue Circle
+ laue_circle = np.dot(tags['nearest_zone_axis'], tags['reciprocal_unit_cell'])
+ laue_circle = np.dot(laue_circle, rotation_matrix)
+ laue_circle = laue_circle / np.linalg.norm(laue_circle) * k_0
+ laue_circle[2] = 0
+
+ dif['Laue_circle'] = laue_circle
+ if verbose:
+ print('Laue_circle', laue_circle)
+
+ # ###########################
+ # Calculate Laue Zones (of allowed reflections)
+ # ###########################
+ # Below is the expression given in most books.
+ # However, that would only work for orthogonal crystal systems
+ # Laue_Zone = abs(np.dot(hkl_allowed,tags['zone_hkl'])) # works only for orthogonal systems
+
+ # This expression works for all crystal systems
+ # Remember we have already tilted, and so the dot product is trivial and gives only the z-component.
+ length_zone_axis = np.linalg.norm(np.dot(tags['zone_hkl'], tags['unit_cell']))
+ laue_zone = abs(np.dot(hkl_allowed, tags['nearest_zone_axis']))
+ dif['allowed']['Laue_Zone'] = laue_zone
+
+ ZOLZ_forbidden = abs(np.floor(g_forbidden[:, 2]*length_zone_axis+0.5)) == 0
+
+ dif['forbidden']['Laue_Zone'] = ZOLZ_forbidden
+ ZOLZ = laue_zone == 0
+ FOLZ = laue_zone == 1
+ SOLZ = laue_zone == 2
+ HOLZ = laue_zone > 0
+ HOLZp = laue_zone > 2
+
+ dif['allowed']['ZOLZ'] = ZOLZ
+ dif['allowed']['FOLZ'] = FOLZ
+ dif['allowed']['SOLZ'] = SOLZ
+ dif['allowed']['HOLZ'] = HOLZ
+ dif['allowed']['HOLZ_plus'] = dif['allowed']['HHOLZ'] = HOLZp
+
+ if verbose:
+ print(' There are {0} allowed reflections in the zero order Laue Zone'.format(ZOLZ.sum()))
+ print(' There are {0} allowed reflections in the first order Laue Zone'.format((laue_zone == 1).sum()))
+ print(' There are {0} allowed reflections in the second order Laue Zone'.format((laue_zone == 2).sum()))
+ print(' There are {0} allowed reflections in the other higher order Laue Zones'.format((laue_zone > 2).sum()))
+
+ if verbose == 2:
+ print(' hkl \t Laue zone \t Intensity (*1 and \t log) \t length \n')
+ for i in range(len(hkl_allowed)):
+ print(' {0} \t {1} \t {2:.3f} \t {3:.3f} \t {4:.3f} '.format(hkl_allowed[i], g_allowed[i],
+ intensities[i], np.log(intensities[i]+1),
+ g_norm_allowed[i]))
+
+ # ##########################
+ # Dynamically Activated forbidden reflections
+ # ##########################
+
+ double_diffraction = (np.sum(np.array(list(itertools.combinations(hkl_allowed[ZOLZ], 2))), axis=1))
+
+ dynamical_allowed = []
+ still_forbidden = []
+ for i, hkl in enumerate(hkl_forbidden):
+ if ZOLZ_forbidden[i]:
+ if hkl.tolist() in double_diffraction.tolist():
+ dynamical_allowed.append(i)
+ else:
+ still_forbidden.append(i)
+ dif['forbidden']['dynamically_activated'] = dynamical_allowed
+ dif['forbidden']['forbidden'] = dynamical_allowed
+ if verbose:
+ print('Length of zone axis vector in real space {0} nm'.format(np.round(length_zone_axis, 3)))
+ print(f'There are {len(dynamical_allowed)} forbidden but dynamical activated diffraction spots:')
+ # print(tags['forbidden']['hkl'][dynamical_allowed])
+
+ # ###################################
+ # Calculate HOLZ and Kikuchi Lines #
+ # ###################################
+
+ # Dynamic Correction
+
+ # Equation Spence+Zuo 3.86a
+ gamma_1 = - 1./(2.*k_0) * (intensities / (2.*k_0*s_g_allowed)).sum()
+ # print('gamma_1',gamma_1)
+
+ # Equation Spence+Zuo 3.84
+ Kg = k_0 - k_0*gamma_1/(g_allowed[:, 2]+1e-15)
+ Kg[ZOLZ] = k_0
+
+ # Calculate angle between K0 and deficient cone vector
+ # For dynamic calculations K0 is replaced by Kg
+ Kg[:] = k_0
+ d_theta = np.arcsin(g_norm_allowed/Kg/2.)-np.arcsin(np.abs(g_allowed[:, 2])/g_norm_allowed)
+
+ # calculate length of distance of deficient cone to K0 in ZOLZ plane
+ gd_length = 2*np.sin(d_theta/2)*k_0
+
+ # Calculate nearest point of HOLZ and Kikuchi lines
+ g_closest = g_allowed.copy()
+ g_closest = g_closest*(gd_length/np.linalg.norm(g_closest, axis=1))[:, np.newaxis]
+
+ g_closest[:, 2] = 0.
+
+ # calculate and save line in Hough space coordinates (distance and theta)
+ slope = g_closest[:, 0]/(g_closest[:, 1]+1e-10)
+ distance = gd_length
+ theta = np.arctan2(g_allowed[:, 0], g_allowed[:, 1])
+
+ dif['HOLZ'] = {}
+ dif['HOLZ']['slope'] = slope
+ # a line is now given by
+
+ dif['HOLZ']['distance'] = distance
+ dif['HOLZ']['theta'] = theta
+
+ dif['HOLZ']['g_deficient'] = g_closest
+ dif['HOLZ']['g_excess'] = g_closest+g_allowed
+
+ dif['HOLZ']['ZOLZ'] = ZOLZ
+ dif['HOLZ']['HOLZ'] = HOLZ
+ dif['HOLZ']['FOLZ'] = FOLZ
+ dif['HOLZ']['SOLZ'] = SOLZ
+ dif['HOLZ']['HHOLZ'] = HOLZp # even higher HOLZ
+
+ dif['HOLZ']['hkl'] = dif['allowed']['hkl']
+ dif['HOLZ']['intensities'] = intensities
+
+ ####################################
+ # Calculate HOLZ and Kikuchi Lines #
+ ####################################
+
+ tags_kikuchi = tags.copy()
+ tags_kikuchi['mistilt_alpha'] = 0
+ tags_kikuchi['mistilt_beta'] = 0
+
+ for i in range(1): # tags['nearest_zone_axes']['amount']):
+
+ zone_tags = tags['nearest_zone_axes'][str(i)]
+ tags_kikuchi['zone_hkl'] = zone_tags['hkl']
+ if verbose:
+ print('Calculating Kikuchi lines for zone: ', zone_tags['hkl'])
+
+ tags_kikuchi['Laue_circle'] = laue_circle
+ # Rotate to nearest zone axis
+ rotation_matrix = get_rotation_matrix(tags_kikuchi)
+
+ g_kikuchi_all = np.dot(g_non_rot, rotation_matrix)
+
+ ZOLZ = abs(g_kikuchi_all[:, 2]) < .1
+
+ g_kikuchi = g_kikuchi_all[ZOLZ]
+ S = (k_0**2-np.linalg.norm(g_kikuchi - k0_vector, axis=1)**2)/(2*k_0)
+ reflections = abs(S) < .01 # This is now a boolean array with True for all possible reflections
+ g_kikuchi = g_kikuchi[reflections]
+ hkl_kikuchi = (hkl_all[ZOLZ])[reflections]
+
+ structure_factors = []
+ for j in range(len(g_kikuchi)):
+ F = 0
+ for b in range(len(atoms)):
+ f = feq(atoms[b].symbol, np.linalg.norm(g_kikuchi[j]))
+ F += f * np.exp(-2 * np.pi * 1j * (g_kikuchi[j] * atoms.positions[b]).sum())
+ structure_factors.append(F)
+
+ F = np.array(structure_factors)
+
+ allowed_kikuchi = np.absolute(F) > 0.000001
+
+ g_kikuchi = g_kikuchi[allowed_kikuchi]
+ hkl_kikuchi = hkl_kikuchi[allowed_kikuchi]
+
+ gd2 = g_kikuchi / 2.
+ gd2[:, 2] = 0.
+
+ # calculate and save line in Hough space coordinates (distance and theta)
+ slope2 = gd2[:, 0] / (gd2[:, 1] + 1e-20)
+ distance2 = np.sqrt(gd2[:, 0] * gd2[:, 0] + gd2[:, 1] * gd2[:, 1])
+ theta2 = np.arctan(slope2)
+
+ dif['Kikuchi'] = {}
+ dif['Kikuchi']['slope'] = slope2
+ dif['Kikuchi']['distance'] = distance2
+ dif['Kikuchi']['theta'] = theta2
+ dif['Kikuchi']['hkl'] = hkl_kikuchi
+ dif['Kikuchi']['g_hkl'] = g_kikuchi
+ dif['Kikuchi']['g_deficient'] = gd2
+ dif['Kikuchi']['min_dist'] = gd2 + laue_circle
+
+ if verbose:
+ print('pyTEMlib\'s \"kinematic_scattering\" finished')
+
+
+[docs]def kinematic_scattering2(atoms, verbose=False):
+ """
+ All kinematic scattering calculation
+
+ Calculates Bragg spots, Kikuchi lines, excess, and deficient HOLZ lines
+
+ Parameters
+ ----------
+ atoms: ase.Atoms
+ object with crystal structure:
+ and with experimental parameters in info attribute:
+ 'acceleration_voltage_V', 'zone_hkl', 'Sg_max', 'hkl_max'
+ Optional parameters are:
+ 'mistilt', convergence_angle_mrad', and 'crystal_name'
+ verbose = True will give extended output of the calculation
+ verbose: boolean
+ default is False
+
+ Returns
+ -------
+ ato,s:
+ There are three sub_dictionaries in info attribute:
+ ['allowed'], ['forbidden'], and ['HOLZ']
+ ['allowed'] and ['forbidden'] dictionaries contain:
+ ['Sg'], ['hkl'], ['g'], ['structure factor'], ['intensities'],
+ ['ZOLZ'], ['FOLZ'], ['SOLZ'], ['HOLZ'], ['HHOLZ'], ['label'], and ['Laue_zone']
+ the ['HOLZ'] dictionary contains:
+ ['slope'], ['distance'], ['theta'], ['g_deficient'], ['g_excess'], ['hkl'], ['intensities'],
+ ['ZOLZ'], ['FOLZ'], ['SOLZ'], ['HOLZ'], and ['HHOLZ']
+ Please note that the Kikuchi lines are the HOLZ lines of ZOLZ
+
+ There are also a few parameters stored in the main dictionary:
+ ['wave_length_nm'], ['reciprocal_unit_cell'], ['inner_potential_V'], ['incident_wave_vector'],
+ ['volume'], ['theta'], ['phi'], and ['incident_wave_vector_vacuum']
+ """
+
+ # Check sanity
+ if atoms.info is None:
+ atoms.info = {'output': {}, 'experimental': {}}
+ elif 'output' in atoms.info:
+ output = atoms.info['output']
+ else:
+ output = atoms.info['output'] = {}
+
+ output['SpotPattern'] = True
+
+ if 'experimental' not in atoms.info:
+ tags = atoms.info['experimental'] = {}
+
+ if not check_sanity(atoms):
+ print('Input is not complete, stopping')
+ print('Try \'example()\' for example input')
+ return
+
+ tags = atoms.info['experimental']
+
+ # wavelength
+ tags['wave_length'] = get_wavelength(tags['acceleration_voltage_V'])
+
+ # volume of unit_cell
+ unit_cell = atoms.cell.array
+ metric_tensor = get_metric_tensor(unit_cell) # converts hkl to g vectors and back
+ tags['metric_tensor'] = metric_tensor
+ volume_unit_cell = atoms.cell.volume
+
+ # reciprocal_unit_cell
+
+ # We use the linear algebra package of numpy to invert the unit_cell "matrix"
+ reciprocal_unit_cell = atoms.cell.reciprocal() # np.linalg.inv(unit_cell).T # transposed of inverted unit_cell
+ tags['reciprocal_unit_cell'] = reciprocal_unit_cell
+ inverse_metric_tensor = get_metric_tensor(reciprocal_unit_cell)
+
+ if verbose:
+ print('reciprocal_unit_cell')
+ print(np.round(reciprocal_unit_cell, 3))
+
+ ############################################
+ # Incident wave vector k0 in vacuum and material
+ ############################################
+
+ u0 = 0.0 # in (Ang)
+ # atom form factor of zero reflection angle is the inner potential in 1/A
+ for i in range(len(atoms)):
+ u0 += feq(atoms[i].symbol, 0.0)
+
+ scattering_factor_to_volts = (const.h ** 2) * (1e10 ** 2) / (2 * np.pi * const.m_e * const.e) * volume_unit_cell
+
+ tags['inner_potential_V'] = u0 * scattering_factor_to_volts
+ if verbose:
+ print(f'The inner potential is {u0:.1f} V')
+
+ # Calculating incident wave vector magnitude 'k0' in material
+ wl = tags['wave_length']
+ tags['incident_wave_vector_vacuum'] = 1 / wl
+
+ k0 = tags['incident_wave_vector'] = np.sqrt(1 / wl**2 + u0) # 1/Ang
+
+ tags['convergence_angle_A-1'] = k0 * np.sin(tags['convergence_angle_mrad'] / 1000.)
+ if verbose:
+ print(f"Using an acceleration voltage of {tags['acceleration_voltage_V']/1000:.1f}kV")
+ print(f'Magnitude of incident wave vector in material: {k0:.1f} 1/Ang and in vacuum: {1/wl:.1f} 1/Ang')
+ print(f"Which is an wave length of {1 / k0 * 100.:.3f} pm in the material and {wl * 100.:.3f} pm "
+ f"in the vacuum")
+ print(f"The convergence angle of {tags['convergence_angle_mrad']:.1f}mrad "
+ f"= {tags['convergence_angle_A-1']:.2f} 1/A")
+ print(f"Magnitude of incident wave vector in material: {k0:.1f} 1/A which is a wavelength {100/k0:.3f} pm")
+
+ # ############
+ # Rotate
+ # ############
+
+ # get rotation matrix to rotate zone axis onto z-axis
+ rotation_matrix = get_rotation_matrix(tags)
+
+ if verbose:
+ print(f"Rotation alpha {np.rad2deg(tags['y-axis rotation alpha']):.1f} degree, "
+ f" beta {np.rad2deg(tags['x-axis rotation beta']):.1f} degree")
+ print(f"from zone axis {tags['zone_hkl']}")
+ print(f"Tilting {1} by {np.rad2deg(tags['mistilt_alpha']):.2f} "
+ f" in alpha and {np.rad2deg(tags['mistilt_beta']):.2f} in beta direction results in :")
+ # list(tags['zone_hkl'])
+ #
+ # print(f"zone axis {list(tags['nearest_zone_axis'])} with a mistilt of "
+ # f"{np.rad2deg(tags['mistilt_nearest_zone alpha']):.2f} in alpha "
+ # f"and {np.rad2deg(tags['mistilt_nearest_zone beta']):.2f} in beta direction")
+ nearest = tags['nearest_zone_axes']
+ print('Next nearest zone axes are:')
+ for i in range(1, nearest['amount']):
+ print(f"{nearest[str(i)]['hkl']}: mistilt: {np.rad2deg(nearest[str(i)]['mistilt_alpha']):6.2f}, "
+ f"{np.rad2deg(nearest[str(i)]['mistilt_beta']):6.2f}")
+ k0_unit_vector = np.array([0, 0, 1]) # incident unit wave vector
+ k0_vector = k0_unit_vector * k0 # incident wave vector
+ cent = k0_vector # center of Ewald sphere
+
+ if verbose:
+ print('Center of Ewald sphere ', cent)
+
+ # Find all Miller indices whose reciprocal point lays near the Ewald sphere with radius k0
+ # within a maximum excitation error Sg
+ hkl_max = tags['hkl_max']
+ Sg_max = tags['Sg_max'] # 1/A maximum allowed excitation error
+
+ h = np.linspace(-hkl_max, hkl_max, 2 * hkl_max + 1) # all evaluated single Miller indices
+ hkl = np.array(list(itertools.product(h, h, h))) # all evaluated Miller indices
+ g_non_rot = np.dot(hkl, reciprocal_unit_cell) # all evaluated reciprocal_unit_cell points
+
+ g = np.dot(g_non_rot, rotation_matrix) # rotate these reciprocal_unit_cell points
+ g_norm = vector_norm(g) # length of all vectors
+ not_zero = g_norm > 0
+ g = g[not_zero] # zero reflection will make problems further on, so we exclude it.
+ g_non_rot = g_non_rot[not_zero]
+ g_norm = g_norm[not_zero]
+ hkl = hkl[not_zero]
+
+ # Calculate excitation errors for all reciprocal_unit_cell points
+ # Zuo and Spence, 'Adv TEM', 2017 -- Eq 3:14
+ S = (k0 ** 2 - vector_norm(g - cent) ** 2) / (2 * k0)
+ g_mz = g - k0_vector
+ in_sqrt = g_mz[:, 2]**2 + np.linalg.norm(g_mz, axis=1)**2 - k0**2
+ in_sqrt[in_sqrt < 0] = 0.
+ S2 = -g_mz[:, 2] - np.sqrt(in_sqrt)
+
+ # Determine reciprocal_unit_cell points with excitation error less than the maximum allowed one: Sg_max
+
+ reflections = abs(S) < Sg_max # This is now a boolean array with True for all possible reflections
+ hkl_all = hkl.copy()
+ s_g = S[reflections]
+ g_hkl = g[reflections]
+
+ hkl = hkl[reflections]
+ g_hkl_non_rot = g_non_rot[reflections]
+ g_norm = g_norm[reflections]
+
+ if verbose:
+ print(f"Of the {len(g)} tested reciprocal_unit_cell points, {len(g_hkl)} "
+ f"have an excitation error less than {Sg_max:.2f} 1/Angstrom")
+
+ # Calculate Structure Factors
+ base = atoms.positions
+ structure_factors = []
+ for j in range(len(g_hkl)):
+ F = 0
+ for b in range(len(atoms)):
+ f = feq(atoms[b].symbol, np.linalg.norm(g_hkl[j]))
+ F += f * np.exp(-2 * np.pi * 1j * (g_hkl_non_rot[j] * atoms.positions[b]).sum())
+
+ structure_factors.append(F)
+
+ F = structure_factors = np.array(structure_factors)
+
+ # Sort reflection in allowed and forbidden #
+ allowed = np.absolute(F) > 0.000001 # allowed within numerical error
+
+ if verbose:
+ print(f"Of the {hkl.shape[0]} possible reflection {allowed.sum()} are allowed.")
+
+ # information of allowed reflections
+ s_g_allowed = s_g[allowed]
+ hkl_allowed = hkl[allowed][:]
+ g_allowed = g_hkl[allowed, :]
+ F_allowed = F[allowed]
+ g_norm_allowed = g_norm[allowed]
+
+ atoms.info['diffraction'] = {}
+ dif = atoms.info['diffraction']
+ dif['allowed'] = {}
+ dif['allowed']['Sg'] = s_g_allowed
+ dif['allowed']['hkl'] = hkl_allowed
+ dif['allowed']['g'] = g_allowed
+ dif['allowed']['structure factor'] = F_allowed
+
+ # Calculate Extinction Distance Reimer 7.23
+ # - makes only sense for non-zero F
+
+ xi_g = np.real(np.pi * volume_unit_cell * k0 / F_allowed)
+
+ # Calculate Intensity of beams Reimer 7.25
+ if 'thickness' not in tags:
+ tags['thickness'] = 0.
+ thickness = tags['thickness']
+ if thickness > 0.1:
+ I_g = np.real(np.pi ** 2 / xi_g ** 2 * np.sin(np.pi * thickness * s_g_allowed) ** 2 / (np.pi * s_g_allowed)**2)
+ dif['allowed']['Ig'] = I_g
+
+ dif['allowed']['intensities'] = intensities = np.real(F_allowed) ** 2
+
+ # information of forbidden reflections
+ forbidden = np.logical_not(allowed)
+ s_g_forbidden = s_g[forbidden]
+ hkl_forbidden = hkl[forbidden]
+ g_forbidden = g_hkl[forbidden]
+ F_forbidden = F[forbidden]
+
+ dif['forbidden'] = {}
+ dif['forbidden']['Sg'] = s_g_forbidden
+ dif['forbidden']['hkl'] = hkl_forbidden.copy()
+ dif['forbidden']['g'] = g_forbidden
+
+ # Make pretty labels
+ hkl_label = make_pretty_labels(hkl_allowed)
+ dif['allowed']['label'] = hkl_label
+ hkl_label = make_pretty_labels(hkl_forbidden)
+ dif['forbidden']['label'] = hkl_label
+
+ # Dynamically Allowed Reflection
+ """
+ indices = range(len(hkl_allowed))
+ combinations = [list(x) for x in itertools.permutations(indices, 2)]
+ hkl_forbidden = hkl_forbidden.tolist()
+ dynamically_allowed = np.zeros(len(hkl_forbidden), dtype=bool)
+ for [i, j] in combinations:
+ possible = (hkl_allowed[i] + hkl_allowed[j]).tolist()
+ if possible in hkl_forbidden:
+ dynamically
+ _allowed[hkl_forbidden.index(possible)] = True
+ dif['forbidden']['dynamically_allowed'] = dynamically_allowed
+
+ if verbose:
+ print(f"Of the {g_forbidden.shape[0]} forbidden reflection {dif['dynamically_allowed']['g'].shape[0]} "
+ f"can be dynamically activated.")
+ # print(dif['forbidden']['hkl'][dynamically_allowed])
+ """
+
+ # Center of Laue Circle
+ laue_circle = np.dot(tags['nearest_zone_axis'], tags['reciprocal_unit_cell'])
+ laue_circle = np.dot(laue_circle, rotation_matrix)
+ laue_circle = laue_circle / np.linalg.norm(laue_circle) * k0
+ laue_circle[2] = 0
+
+ dif['Laue_circle'] = laue_circle
+ if verbose:
+ print('Laue_circle', laue_circle)
+
+ # ###########################
+ # Calculate Laue Zones (of allowed reflections)
+ # ###########################
+ # Below is the expression given in most books.
+ # However, that would only work for orthogonal crystal systems
+ # Laue_Zone = abs(np.dot(hkl_allowed,tags['zone_hkl'])) # works only for orthogonal systems
+
+ # This expression works for all crystal systems
+ # Remember we have already tilted, and so the dot product is trivial and gives only the z-component.
+
+ Laue_Zone = abs(np.dot(hkl_allowed, tags['nearest_zone_axis']))
+ dif['allowed']['Laue_Zone'] = Laue_Zone
+
+ ZOLZ = Laue_Zone == 0
+ FOLZ = Laue_Zone == 1
+ SOLZ = Laue_Zone == 2
+ HOLZ = Laue_Zone > 2
+
+ dif['allowed']['ZOLZ'] = ZOLZ
+ dif['allowed']['FOLZ'] = FOLZ
+ dif['allowed']['SOLZ'] = SOLZ
+ dif['allowed']['HOLZ'] = HOLZ
+
+ if verbose:
+ print(' There are {0} allowed reflections in the zero order Laue Zone'.format(ZOLZ.sum()))
+ print(' There are {0} allowed reflections in the first order Laue Zone'.format((Laue_Zone == 1).sum()))
+ print(' There are {0} allowed reflections in the second order Laue Zone'.format((Laue_Zone == 2).sum()))
+ print(' There are {0} allowed reflections in the higher order Laue Zone'.format((Laue_Zone > 2).sum()))
+
+ if verbose:
+ print(' hkl \t Laue zone \t Intensity (*1 and \t log) \t length \n')
+ for i in range(len(hkl_allowed)):
+ print(' {0} \t {1} \t {2:.3f} \t {3:.3f} \t {4:.3f} '.format(hkl_allowed[i],
+ g_allowed[i], intensities[i],
+ np.log(intensities[i] + 1),
+ g_norm_allowed[i]))
+
+ ####################################
+ # Calculate HOLZ and Kikuchi Lines #
+ ####################################
+
+ tags_new_zone = tags.copy()
+ tags_new_zone['mistilt_alpha'] = 0
+ tags_new_zone['mistilt_beta'] = 0
+
+ for i in range(1): # tags['nearest_zone_axes']['amount']):
+
+ zone_tags = tags['nearest_zone_axes'][str(i)]
+
+ if verbose:
+ print('Calculating Kikuchi lines for zone: ', zone_tags['hkl'])
+
+ laue_circle = np.dot(zone_tags['hkl'], tags['reciprocal_unit_cell'])
+ laue_circle = np.dot(laue_circle, rotation_matrix)
+ laue_circle = laue_circle / np.linalg.norm(laue_circle) * k0
+ laue_circle[2] = 0
+
+ zone_tags['Laue_circle'] = laue_circle
+ # Rotate to nearest zone axis
+
+ tags_new_zone['zone_hkl']
+
+ theta = -(zone_tags['mistilt_alpha'])
+ phi = -(zone_tags['mistilt_beta'])
+
+ # first we rotate phi about z-axis
+ c, s = np.cos(phi), np.sin(phi)
+ rot_z = np.array([[1, 0, 0], [0, c, -s], [0, s, c]])
+
+ # second we rotate theta about y-axis
+ c, s = np.cos(theta), np.sin(theta)
+ rot_y = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]])
+
+ # the rotation now makes z-axis coincide with plane normal
+
+ rotation_matrix2 = np.dot(rot_z, rot_y)
+
+ g_kikuchi_all = np.dot(g, rotation_matrix2)
+ ZOLZ = abs(g_kikuchi_all[:, 2]) < 1
+
+ g_kikuchi = g_kikuchi_all[ZOLZ]
+ S = (k0 ** 2 - vector_norm(g_kikuchi - np.array([0, 0, k0])) ** 2) / (2 * k0)
+ reflections = abs(S) < 0.1 # This is now a boolean array with True for all possible reflections
+ g_hkl_kikuchi2 = g_kikuchi[reflections]
+ hkl_kikuchi2 = (hkl_all[ZOLZ])[reflections]
+
+ structure_factors = []
+ for j in range(len(g_hkl_kikuchi2)):
+ F = 0
+ for b in range(len(atoms)):
+ f = feq(atoms[b].symbol, np.linalg.norm(g_hkl_kikuchi2[j]))
+ F += f * np.exp(-2 * np.pi * 1j * (g_hkl_kikuchi2[j] * atoms.positions[b]).sum())
+
+ structure_factors.append(F)
+
+ F = np.array(structure_factors)
+
+ allowed_kikuchi = np.absolute(F) > 0.000001
+
+ g_hkl_kikuchi = g_hkl_kikuchi2[allowed_kikuchi]
+ hkl_kikuchi = hkl_kikuchi2[allowed_kikuchi]
+
+ gd2 = g_hkl_kikuchi / 2.
+ gd2[:, 2] = 0.
+
+ # calculate and save line in Hough space coordinates (distance and theta)
+ slope2 = gd2[:, 0] / (gd2[:, 1] + 1e-20)
+ distance2 = np.sqrt(gd2[:, 0] * gd2[:, 0] + gd2[:, 1] * gd2[:, 1])
+ theta2 = np.arctan(slope2)
+
+ dif['Kikuchi'] = {}
+ dif['Kikuchi']['slope'] = slope2
+ dif['Kikuchi']['distance'] = distance2
+ dif['Kikuchi']['theta'] = theta2
+ dif['Kikuchi']['hkl'] = hkl_kikuchi
+ dif['Kikuchi']['g_hkl'] = g_hkl_kikuchi
+ dif['Kikuchi']['g_deficient'] = gd2
+ dif['Kikuchi']['min dist'] = gd2 + laue_circle
+
+ k_g = k0
+
+ # Dynamic Correction
+ # Does not correct ZOLZ lines !!!!
+ # Equation Spence+Zuo 3.86a
+ if 'dynamic correction' in tags:
+ if tags['dynamic correction']:
+ gamma_1 = - 1. / (2. * k0) * (intensities / (2. * k0 * s_g_allowed)).sum()
+ if verbose:
+ print('Dynamic correction gamma_1: ', gamma_1)
+
+ # Equation Spence+Zuo 3.84
+ k_g = k0 - k0 * gamma_1 / g_allowed[:, 2]
+
+ # k_g = np.dot( [0,0,k0], rotation_matrix)
+ # Calculate angle between k0 and deficient cone vector
+ # For dynamic calculations k0 is replaced by k_g
+ d_theta = np.arcsin(g_norm_allowed / k_g / 2.) - np.arcsin(np.abs(g_allowed[:, 2]) / g_norm_allowed)
+
+ # calculate length of distance of deficient cone to k0 in ZOLZ plane
+ gd_length = 2 * np.sin(d_theta / 2) * k0
+
+ # Calculate nearest point of HOLZ and Kikuchi lines
+ gd = g_allowed.copy()
+ gd[:, 0] = -gd[:, 0] * gd_length / g_norm_allowed
+ gd[:, 1] = -gd[:, 1] * gd_length / g_norm_allowed
+ gd[:, 2] = 0.
+
+ # calculate and save line in Hough space coordinates (distance and theta)
+ slope = gd[:, 0] / (gd[:, 1] + 1e-20)
+ distance = gd_length
+ theta = np.arctan(slope)
+
+ dif['HOLZ'] = {}
+ dif['HOLZ']['slope'] = slope
+ # a line is now given by
+ dif['HOLZ']['distance'] = distance
+ dif['HOLZ']['theta'] = theta
+ dif['HOLZ']['g_deficient'] = gd
+ dif['HOLZ']['g_excess'] = gd + g_allowed
+ dif['HOLZ']['g_allowed'] = g_allowed.copy()
+
+ dif['HOLZ']['ZOLZ'] = ZOLZ
+ dif['HOLZ']['HOLZ'] = np.logical_not(ZOLZ)
+ dif['HOLZ']['FOLZ'] = FOLZ
+ dif['HOLZ']['SOLZ'] = SOLZ
+ dif['HOLZ']['HHOLZ'] = HOLZ # even higher HOLZ
+
+ dif['HOLZ']['hkl'] = dif['allowed']['hkl']
+ dif['HOLZ']['intensities'] = intensities
+
+ print('done')
+
+
+[docs]def make_pretty_labels(hkls, hex_label=False):
+ """Make pretty labels
+
+ Parameters
+ ----------
+ hkls: np.ndarray
+ a numpy array with all the Miller indices to be labeled
+ hex_label: boolean - optional
+ if True this will make for Miller indices.
+
+ Returns
+ -------
+ hkl_label: list
+ list of labels in Latex format
+ """
+ hkl_label = []
+ for i in range(len(hkls)):
+ h, k, l = np.array(hkls)[i]
+
+ if h < 0:
+ h_string = r'[$\bar {' + str(int(-h)) + '},'
+ else:
+ h_string = r'[$\bar {' + str(int(h)) + '},'
+ if k < 0:
+ k_string = r'\bar {' + str(int(-k)) + '},'
+ else:
+ k_string = str(int(k)) + ','
+ if hex_label:
+ ii = -(h + k)
+ if ii < 0:
+ k_string = k_string + r'\bar {' + str(int(-ii)) + '},'
+ else:
+ k_string = k_string + str(int(ii)) + ','
+ if l < 0:
+ l_string = r'\bar {' + str(int(-l)) + '} $]'
+ else:
+ l_string = str(int(l)) + '} $]'
+ label = h_string + k_string + l_string
+ hkl_label.append(label)
+ return hkl_label
+
+
+[docs]def feq(element, q):
+ """Atomic form factor parametrized in 1/Angstrom but converted to 1/Angstrom
+
+ The atomic form factor is from Kirkland: Advanced Computing in Electron Microscopy 2nd edition, Appendix C.
+ From Appendix C of Kirkland, "Advanced Computing in Electron Microscopy", 3Ard ed.
+ Calculation of electron form factor for specific q:
+ Using equation Kirkland C.15
+
+ Parameters
+ ----------
+ element: string
+ element name
+ q: float
+ magnitude of scattering vector in 1/Angstrom -- (=> exp(-i*g.r), physics negative convention)
+
+ Returns
+ -------
+ fL+fG: float
+ atomic scattering vector
+ """
+
+ if not isinstance(element, str):
+ raise TypeError('Element has to be a string')
+ if element not in electronFF:
+ if len(element) > 2:
+ raise TypeError('Please use standard convention for element abbreviation with not more than two letters')
+ else:
+ raise TypeError('Element {element} not known to electron diffraction should')
+ if not isinstance(q, (float, int)):
+ raise TypeError('Magnitude of scattering vector has to be a number of type float')
+
+ # q is in magnitude of scattering vector in 1/A -- (=> exp(-i*g.r), physics negative convention)
+ param = electronFF[element]
+ f_lorentzian = 0
+ f_gauss = 0
+ for i in range(3):
+ f_lorentzian += param['fa'][i]/(q**2 + param['fb'][i])
+ f_gauss += param['fc'][i]*np.exp(-q**2 * param['fd'][i])
+
+ # Conversion factor from scattering factors to volts. h^2/(2pi*m0*e), see e.g. Kirkland eqn. C.5
+ # !NB RVolume is already in A unlike RPlanckConstant
+ # scattering_factor_to_volts=(PlanckConstant**2)*(AngstromConversion**2)/(2*np.pi*ElectronMass*ElectronCharge)
+ return f_lorentzian+f_gauss # * scattering_factor_to_volts
+
+""" default microscope parameters from config file
+
+Read microscope CSV file
+
+for pyTEMLib by Gerd
+
+copyright 2012, Gerd Duscher
+updated 2021
+"""
+# -*- coding: utf-8 -*-
+
+import csv
+import os.path
+
+from pyTEMlib.config_dir import config_path
+microscopes_file = os.path.join(config_path, 'microscopes.csv')
+
+
+
+[docs]class Microscope(object):
+ """Class to read configuration file and provide microscope information"""
+ microscopes = {}
+ name = None
+ E0 = None
+ alpha = None
+ beta = None
+ pppc = None
+ correlation_factor = None
+
+ def __init__(self):
+ self.load_microscopes()
+ default_tem = self.microscopes[list(self.microscopes.keys())[0]]
+ self.set_microscope(default_tem['Microscope'])
+
+ def load_microscopes(self):
+ f = open(microscopes_file, 'r')
+
+ labels = f.readline().strip().split(',')
+ # print labels
+ csv_read = csv.DictReader(f, labels, delimiter=",")
+
+ for line in csv_read:
+ tem = line['Microscope']
+ self.microscopes[tem] = line
+ for i in self.microscopes[tem]:
+ if i != 'Microscope':
+ self.microscopes[tem][i] = float(self.microscopes[tem][i])
+ f.close()
+
+ def get_available_microscope_names(self):
+ tem = []
+ for scope in self.microscopes.keys():
+ tem.append(scope)
+ return tem
+
+ def set_microscope(self, microscope_name):
+ if microscope_name in self.microscopes:
+ self.name = microscope_name
+
+
+microscope = Microscope()
+
+"""
+ EELS Input Dialog for ELNES Analysis
+"""
+from os import error
+Qt_available = True
+try:
+ from PyQt5 import QtCore, QtWidgets
+except:
+ Qt_available = False
+ # print('Qt dialogs are not available')
+
+import numpy as np
+import scipy
+import scipy.optimize
+import scipy.signal
+
+import ipywidgets
+from IPython.display import display
+import matplotlib
+import matplotlib.pylab as plt
+import matplotlib.patches as patches
+
+import sidpy
+import pyTEMlib.file_tools as ft
+from pyTEMlib import eels_tools
+from pyTEMlib import peak_dlg
+from pyTEMlib import eels_dialog_utilities
+
+advanced_present = True
+try:
+ import advanced_eels_tools
+ print('advanced EELS features enabled')
+except ModuleNotFoundError:
+ advanced_present = False
+
+_version = .001
+
+[docs]def get_sidebar():
+ side_bar = ipywidgets.GridspecLayout(16, 3, width='auto', grid_gap="0px")
+ row = 0
+ side_bar[row, :3] = ipywidgets.Button(description='Fit Area',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=7.5,description='Fit Start:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Fit End:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='20px'))
+
+ row += 1
+ side_bar[row, :3] = ipywidgets.Button(description='Peak Finding',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+
+ row += 1
+
+
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=[('0', 0), ('1', 1), ('2', 2), ('3', 3), ('4', 4)],
+ value=0,
+ description='Peaks:',
+ disabled=False,
+ layout=ipywidgets.Layout(width='200px'))
+
+ side_bar[row, 2] = ipywidgets.Button(
+ description='Smooth',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Do Gaussian Mixing',
+ layout=ipywidgets.Layout(width='100px'))
+
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Number:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.Button(
+ description='Find',
+ disabled=False,
+ button_style='', # 'success', 'info', 'warning', 'danger' or ''
+ tooltip='Find first peaks from Gaussian mixture',
+ layout=ipywidgets.Layout(width='100px'))
+
+ row += 1
+
+ side_bar[row, :3] = ipywidgets.Button(description='Peaks',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=[('Peak 1', 0), ('Add Peak', -1)],
+ value=0,
+ description='Peaks:',
+ disabled=False,
+ layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=[ 'Gauss', 'Lorentzian', 'Drude', 'Zero-Loss'],
+ value='Gauss',
+ description='Symmetry:',
+ disabled=False,
+ layout=ipywidgets.Layout(width='200px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Position:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Amplitude:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Width FWHM:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="eV", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.FloatText(value=0.1, description='Asymmetry:', disabled=False, color='black', layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value="a.u.", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+
+ side_bar[row, :3] = ipywidgets.Button(description='White-Line',
+ layout=ipywidgets.Layout(width='auto', grid_area='header'),
+ style=ipywidgets.ButtonStyle(button_color='lightblue'))
+
+ row += 1
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=[('None', 0)],
+ value=0,
+ description='Ratio:',
+ disabled=False,
+ layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value=" ", layout=ipywidgets.Layout(width='100px'))
+ row += 1
+ side_bar[row, :2] = ipywidgets.Dropdown(
+ options=[('None', 0)],
+ value=0,
+ description= 'Sum:',
+ disabled=False,
+ layout=ipywidgets.Layout(width='200px'))
+ side_bar[row, 2] = ipywidgets.widgets.Label(value=" ", layout=ipywidgets.Layout(width='100px'))
+ return side_bar
+
+[docs]class PeakFitWidget(object):
+ def __init__(self, datasets=None):
+ self.datasets = datasets
+ if not isinstance(datasets, dict):
+ raise TypeError('need dictioary of sidpy datasets')
+
+ self.sidebar = get_sidebar()
+ self.key = list(self.datasets)[0]
+ self.dataset = datasets[self.key]
+ if not isinstance(self.dataset, sidpy.Dataset):
+ raise TypeError('dataset or first item inhas to be a sidpy dataset')
+
+ self.model = np.array([])
+ self.y_scale = 1.0
+ self.change_y_scale = 1.0
+ self.spectrum_ll = None
+ self.low_loss_key = None
+
+ self.peaks = {}
+
+ self.show_regions = False
+
+ self.set_dataset()
+
+ self.app_layout = ipywidgets.AppLayout(
+ left_sidebar=self.sidebar,
+ center=self.view.panel,
+ footer=None,#message_bar,
+ pane_heights=[0, 10, 0],
+ pane_widths=[4, 10, 0],
+ )
+ display(self.app_layout)
+ self.set_action()
+
+ def line_select_callback(self, x_min, x_max):
+ self.start_cursor.value = np.round(x_min,3)
+ self.end_cursor.value = np.round(x_max, 3)
+ self.start_channel = np.searchsorted(self.datasets[self.key].energy_loss, self.start_cursor.value)
+ self.end_channel = np.searchsorted(self.datasets[self.key].energy_loss, self.end_cursor.value)
+
+
+ def set_peak_list(self):
+ self.peak_list = []
+ if 'peaks' not in self.peaks:
+ self.peaks['peaks'] = {}
+ key = 0
+ for key in self.peaks['peaks']:
+ if key.isdigit():
+ self.peak_list.append((f'Peak {int(key) + 1}', int(key)))
+ self.peak_list.append(('add peak', -1))
+ #self.sidebar[7, 0].options = self.peak_list
+ #self.sidebar[7, 0].value = 0
+
+
+ def plot(self, scale=True):
+
+ self.view.change_y_scale = self.change_y_scale
+ self.view.y_scale = self.y_scale
+ self.energy_scale = self.dataset.energy_loss.values
+
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ spectrum = self.dataset.view.get_spectrum()
+ else:
+ spectrum = self.dataset
+ if len(self.model) > 1:
+ additional_spectra = {'model': self.model,
+ 'difference': spectrum-self.model}
+ else:
+ additional_spectra = None
+ if 'peaks' in self.peaks:
+ for index, peak in self.peaks['peaks'].items():
+ p = [peak['position'], peak['amplitude'], peak['width']]
+ additional_spectra[f'peak {index}']= eels_tools.gauss(self.energy_scale, p)
+ self.view.plot(scale=True, additional_spectra=additional_spectra )
+ self.change_y_scale = 1.
+
+ self.view.figure.canvas.draw_idle()
+
+
+ def set_dataset(self, index=0):
+ self.spec_dim = ft.get_dimensions_by_type('spectral', self.dataset)
+ if len(self.spec_dim) != 1:
+ raise TypeError('We need exactly one SPECTRAL dimension')
+ self.spec_dim = self.spec_dim[0]
+ self.energy_scale = self.spec_dim[1]
+
+ self.y_scale = 1.0
+ self.change_y_scale = 1.0
+
+ if 'peak_fit' not in self.dataset.metadata:
+ self.dataset.metadata['peak_fit'] = {}
+ if 'edges' in self.dataset.metadata:
+ if 'fit_area' in self.dataset.metadata['edges']:
+ self.dataset.metadata['peak_fit']['fit_start'] = self.dataset.metadata['edges']['fit_area']['fit_start']
+ self.dataset.metadata['peak_fit']['fit_end'] = self.dataset.metadata['edges']['fit_area']['fit_end']
+ self.dataset.metadata['peak_fit']['peaks'] = {'0': {'position': self.energy_scale[1],
+ 'amplitude': 1000.0, 'width': 1.0,
+ 'type': 'Gauss', 'asymmetry': 0}}
+
+ self.peaks = self.dataset.metadata['peak_fit']
+ if 'fit_start' not in self.peaks:
+ self.peaks['fit_start'] = self.energy_scale[1]
+ if 'fit_end' not in self.peaks:
+ self.peaks['fit_end'] = self.energy_scale[-2]
+
+ if 'peak_model' in self.peaks:
+ self.peak_model = self.peaks['peak_model']
+ self.model = self.peak_model
+ if 'edge_model' in self.peaks:
+ self.model = self.model + self.peaks['edge_model']
+ else:
+ self.model = np.array([])
+ self.peak_model = np.array([])
+ if 'peak_out_list' in self.peaks:
+ self.peak_out_list = self.peaks['peak_out_list']
+ self.set_peak_list()
+
+ # check whether a core loss analysis has been done previously
+ if not hasattr(self, 'core_loss') and 'edges' in self.dataset.metadata:
+ self.core_loss = True
+ else:
+ self.core_loss = False
+
+ self.update()
+ if self.dataset.data_type.name =='SPECTRAL_IMAGE':
+ self.view = eels_dialog_utilities.SIPlot(self.dataset)
+ else:
+ self.view = eels_dialog_utilities.SpectrumPlot(self.dataset)
+ self.y_scale = 1.0
+ self.change_y_scale = 1.0
+
+ def set_fit_area(self, value):
+
+ self.peaks['fit_start'] = self.sidebar[1, 0].value
+ self.peaks['fit_end'] = self.sidebar[2, 0].value
+
+ self.plot()
+
+ def set_y_scale(self, value):
+ self.change_y_scale = 1/self.y_scale
+ if self.sidebar[12, 0].value:
+ dispersion = self.energy_scale[1] - self.energy_scale[0]
+ self.y_scale = 1/self.dataset.metadata['experiment']['flux_ppm'] * dispersion
+ else:
+ self.y_scale = 1.0
+
+ self.change_y_scale *= self.y_scale
+ self.update()
+ self.plot()
+
+ def update(self, index=0):
+
+ # self.setWindowTitle('update')
+ self.sidebar[1, 0].value = self.peaks['fit_start']
+ self.sidebar[2, 0].value = self.peaks['fit_end']
+
+ peak_index = self.sidebar[7, 0].value
+ self.peak_index = self.sidebar[7, 0].value
+ if str(peak_index) not in self.peaks['peaks']:
+ self.peaks['peaks'][str(peak_index)] = {'position': self.energy_scale[1], 'amplitude': 1000.0,
+ 'width': 1.0, 'type': 'Gauss', 'asymmetry': 0}
+ self.sidebar[8, 0].value = self.peaks['peaks'][str(peak_index)]['type']
+ if 'associated_edge' in self.peaks['peaks'][str(peak_index)]:
+ self.sidebar[7, 2].value = (self.peaks['peaks'][str(peak_index)]['associated_edge'])
+ else:
+ self.sidebar[7, 2].value = ''
+ self.sidebar[9, 0].value = self.peaks['peaks'][str(peak_index)]['position']
+ self.sidebar[10, 0].value = self.peaks['peaks'][str(peak_index)]['amplitude']
+ self.sidebar[11, 0].value = self.peaks['peaks'][str(peak_index)]['width']
+ if 'asymmetry' not in self.peaks['peaks'][str(peak_index)]:
+ self.peaks['peaks'][str(peak_index)]['asymmetry'] = 0.
+ self.sidebar[12, 0].value = self.peaks['peaks'][str(peak_index)]['asymmetry']
+
+
+[docs] def fit_peaks(self, value = 0):
+ """Fit spectrum with peaks given in peaks dictionary"""
+ # print('Fitting peaks...')
+ p_in = []
+ for key, peak in self.peaks['peaks'].items():
+ if key.isdigit():
+ p_in.append(peak['position'])
+ p_in.append(peak['amplitude'])
+ p_in.append(peak['width'])
+
+ spectrum = np.array(self.dataset)
+
+ # set the energy scale and fit start and end points
+ energy_scale = np.array(self.energy_scale)
+ start_channel = np.searchsorted(energy_scale, self.peaks['fit_start'])
+ end_channel = np.searchsorted(energy_scale, self.peaks['fit_end'])
+
+ energy_scale = self.energy_scale[start_channel:end_channel]
+ # select the core loss model if it exists. Otherwise, we will fit to the full spectrum.
+ if 'model' in self.dataset.metadata:
+ model = self.dataset.metadata['model'][start_channel:end_channel]
+ elif self.core_loss:
+ # print('Core loss model found. Fitting on top of the model.')
+ model = self.dataset.metadata['edges']['model']['spectrum'][start_channel:end_channel]
+ else:
+ # print('No core loss model found. Fitting to the full spectrum.')
+ model = np.zeros(end_channel - start_channel)
+
+ # if we have a core loss model we will only fit the difference between the model and the data.
+ difference = np.array(spectrum[start_channel:end_channel] - model)
+
+ # find the optimum fitting parameters
+ [self.p_out, _] = scipy.optimize.leastsq(eels_tools.residuals_smooth, np.array(p_in), ftol=1e-3,
+ args=(energy_scale, difference, False))
+
+ # construct the fit data from the optimized parameters
+ self.peak_model = np.zeros(len(self.energy_scale))
+ self.model = np.zeros(len(self.energy_scale))
+ self.model[start_channel:end_channel] = model
+ fit = eels_tools.model_smooth(energy_scale, self.p_out, False)
+ self.peak_model[start_channel:end_channel] = fit
+ self.dataset.metadata['peak_fit']['edge_model'] = self.model
+ self.model = self.model + self.peak_model
+ self.dataset.metadata['peak_fit']['peak_model'] = self.peak_model
+
+ for key, peak in self.peaks['peaks'].items():
+ if key.isdigit():
+ p_index = int(key)*3
+ self.peaks['peaks'][key] = {'position': self.p_out[p_index],
+ 'amplitude': self.p_out[p_index+1],
+ 'width': self.p_out[p_index+2],
+ 'type': 'Gauss',
+ 'associated_edge': ''}
+
+ eels_tools.find_associated_edges(self.dataset)
+ self.find_white_lines()
+ self.update()
+ self.plot()
+
+
+
+ def find_white_lines(self):
+ eels_tools.find_white_lines(self.dataset)
+
+ self.wl_list = []
+ self.wls_list = []
+ if len(self.dataset.metadata['peak_fit']['white_line_ratios']) > 0:
+ for key in self.dataset.metadata['peak_fit']['white_line_ratios']:
+ self.wl_list.append(key)
+ for key in self.dataset.metadata['peak_fit']['white_line_sums']:
+ self.wls_list.append(key)
+
+ self.sidebar[14, 0].options = self.wl_list
+ self.sidebar[14, 0].value = self.wl_list[0]
+ self.sidebar[14, 2].value = f"{self.dataset.metadata['peak_fit']['white_line_ratios'][self.wl_list[0]]:.2f}"
+
+ self.sidebar[15, 0].options = self.wls_list
+ self.sidebar[15, 0].value = self.wls_list[0]
+ self.sidebar[15, 2].value = f"{self.dataset.metadata['peak_fit']['white_line_sums'][self.wls_list[0]]*1e6:.4f} ppm"
+
+ else:
+ self.wl_list.append('Ratio')
+ self.wls_list.append('Sum')
+
+ self.sidebar[14, 0].options = ['None']
+ self.sidebar[14, 0].value = 'None'
+ self.sidebar[14, 2].value = ' '
+
+ self.sidebar[15, 0].options = ['None']
+ self.sidebar[15, 0].value = 'None'
+ self.sidebar[15, 2].value = ' '
+
+ def find_peaks(self, value=0):
+ number_of_peaks = int(self.sidebar[5, 0].value)
+
+ self.peak_list = []
+ self.peaks['peaks'] = {}
+ for i in range(number_of_peaks):
+ self.peak_list.append((f'Peak {i+1}', i))
+ p = self.peak_out_list[i]
+ self.peaks['peaks'][str(i)] = {'position': p[0], 'amplitude': p[1], 'width': p[2], 'type': 'Gauss',
+ 'asymmetry': 0}
+
+ self.peak_list.append((f'add peak', -1))
+
+ self.sidebar[7, 0].options = self.peak_list
+ self.sidebar[7, 0].value = 0
+ eels_tools.find_associated_edges(self.dataset)
+ self.find_white_lines()
+
+ self.update()
+ self.plot()
+
+[docs] def smooth(self, value=0):
+ """Fit lots of Gaussian to spectrum and let the program sort it out
+
+ We sort the peaks by area under the Gaussians, assuming that small areas mean noise.
+
+ """
+ iterations = self.sidebar[4, 0].value
+ self.sidebar[5, 0].value = 0
+ advanced_present=False
+
+ self.peak_model, self.peak_out_list, number_of_peaks = smooth(self.dataset, iterations, advanced_present)
+
+ spec_dim = ft.get_dimensions_by_type('SPECTRAL', self.dataset)[0]
+ if spec_dim[1][0] > 0:
+ self.model = self.dataset.metadata['edges']['model']['spectrum']
+ elif 'model' in self.dataset.metadata:
+ self.model = self.dataset.metadata['model']
+ else:
+ self.model = np.zeros(len(spec_dim[1]))
+
+ self.dataset.metadata['peak_fit']['edge_model'] = self.model
+ self.model = self.model + self.peak_model
+ self.dataset.metadata['peak_fit']['peak_model'] = self.peak_model
+ self.dataset.metadata['peak_fit']['peak_out_list'] = self.peak_out_list
+
+ self.sidebar[5, 0].value = number_of_peaks
+ self.update()
+ self.plot()
+
+ def make_model(self):
+ p_peaks = []
+ for key, peak in self.peaks['peaks'].items():
+ if key.isdigit():
+ p_peaks.append(peak['position'])
+ p_peaks.append(peak['amplitude'])
+ p_peaks.append(peak['width'])
+
+
+ # set the energy scale and fit start and end points
+ energy_scale = np.array(self.energy_scale)
+ start_channel = np.searchsorted(energy_scale, self.peaks['fit_start'])
+ end_channel = np.searchsorted(energy_scale, self.peaks['fit_end'])
+ energy_scale = self.energy_scale[start_channel:end_channel]
+ # select the core loss model if it exists. Otherwise, we will fit to the full spectrum.
+
+ fit = eels_tools.model_smooth(energy_scale, p_peaks, False)
+ self.peak_model[start_channel:end_channel] = fit
+ if 'edge_model' in self.dataset.metadata['peak_fit']:
+ self.model = self.dataset.metadata['peak_fit']['edge_model'] + self.peak_model
+ else:
+ self.model = np.zeros(self.dataset.shape)
+
+ def modify_peak_position(self, value=-1):
+ peak_index = self.sidebar[7, 0].value
+ self.peaks['peaks'][str(peak_index)]['position'] = self.sidebar[9,0].value
+ self.make_model()
+ self.plot()
+
+ def modify_peak_amplitude(self, value=-1):
+ peak_index = self.sidebar[7, 0].value
+ self.peaks['peaks'][str(peak_index)]['amplitude'] = self.sidebar[10,0].value
+ self.make_model()
+ self.plot()
+
+ def modify_peak_width(self, value=-1):
+ peak_index = self.sidebar[7, 0].value
+ self.peaks['peaks'][str(peak_index)]['width'] = self.sidebar[11,0].value
+ self.make_model()
+ self.plot()
+
+ def set_action(self):
+ self.sidebar[1, 0].observe(self.set_fit_area, names='value')
+ self.sidebar[2, 0].observe(self.set_fit_area, names='value')
+
+ self.sidebar[4, 2].on_click(self.smooth)
+ self.sidebar[7,0].observe(self.update)
+ self.sidebar[5,2].on_click(self.find_peaks)
+
+ self.sidebar[6, 0].on_click(self.fit_peaks)
+ self.sidebar[9, 0].observe(self.modify_peak_position, names='value')
+ self.sidebar[10, 0].observe(self.modify_peak_amplitude, names='value')
+ self.sidebar[11, 0].observe(self.modify_peak_width, names='value')
+
+
+
+
+if Qt_available:
+ class PeakFitDialog(QtWidgets.QDialog):
+ """
+ EELS Input Dialog for ELNES Analysis
+ """
+
+ def __init__(self, datasets=None):
+ super().__init__(None, QtCore.Qt.WindowStaysOnTopHint)
+
+ if datasets is None:
+ # make a dummy dataset
+ datasets = ft.make_dummy_dataset('spectrum')
+ if not isinstance(datasets, dict):
+ datasets= {'Channel_000': datasets}
+
+ self.dataset = datasets[list(datasets.keys())[0]]
+ self.datasets = datasets
+ # Create an instance of the GUI
+ if 'low_loss' in self.dataset.metadata:
+ mode = 'low_loss'
+ else:
+ mode = 'core_loss'
+
+ self.ui = peak_dlg.UiDialog(self, mode=mode)
+
+ self.set_action()
+
+ self.energy_scale = np.array([])
+ self.peak_out_list = []
+ self.p_out = []
+ self.axis = None
+ self.show_regions = False
+ self.show()
+
+
+
+ if not isinstance(self.dataset, sidpy.Dataset):
+ raise TypeError('dataset has to be a sidpy dataset')
+ self.spec_dim = ft.get_dimensions_by_type('spectral', self.dataset)
+ if len(self.spec_dim) != 1:
+ raise TypeError('We need exactly one SPECTRAL dimension')
+ self.spec_dim = self.spec_dim[0]
+ self.energy_scale = self.spec_dim[1].values.copy()
+
+ if 'peak_fit' not in self.dataset.metadata:
+ self.dataset.metadata['peak_fit'] = {}
+ if 'edges' in self.dataset.metadata:
+ if 'fit_area' in self.dataset.metadata['edges']:
+ self.dataset.metadata['peak_fit']['fit_start'] = \
+ self.dataset.metadata['edges']['fit_area']['fit_start']
+ self.dataset.metadata['peak_fit']['fit_end'] = self.dataset.metadata['edges']['fit_area']['fit_end']
+ self.dataset.metadata['peak_fit']['peaks'] = {'0': {'position': self.energy_scale[1],
+ 'amplitude': 1000.0, 'width': 1.0,
+ 'type': 'Gauss', 'asymmetry': 0}}
+
+
+ self.peaks = self.dataset.metadata['peak_fit']
+ if 'fit_start' not in self.peaks:
+ self.peaks['fit_start'] = self.energy_scale[1]
+ self.peaks['fit_end'] = self.energy_scale[-2]
+
+ if 'peak_model' in self.peaks:
+ self.peak_model = self.peaks['peak_model']
+ self.model = self.peak_model
+ if 'edge_model' in self.peaks:
+ self.model = self.model + self.peaks['edge_model']
+ else:
+ self.model = np.array([])
+ self.peak_model = np.array([])
+ if 'peak_out_list' in self.peaks:
+ self.peak_out_list = self.peaks['peak_out_list']
+ self.set_peak_list()
+
+ # check whether a core loss analysis has been done previously
+ if not hasattr(self, 'core_loss') and 'edges' in self.dataset.metadata:
+ self.core_loss = True
+ else:
+ self.core_loss = False
+
+ self.update()
+ self.dataset.plot()
+
+ if self.dataset.data_type.name == 'SPECTRAL_IMAGE':
+ if 'SI_bin_x' not in self.dataset.metadata['experiment']:
+ self.dataset.metadata['experiment']['SI_bin_x'] = 1
+ self.dataset.metadata['experiment']['SI_bin_y'] = 1
+ bin_x = self.dataset.metadata['experiment']['SI_bin_x']
+ bin_y = self.dataset.metadata['experiment']['SI_bin_y']
+
+ self.dataset.view.set_bin([bin_x, bin_y])
+
+ if hasattr(self.dataset.view, 'axes'):
+ self.axis = self.dataset.view.axes[-1]
+ elif hasattr(self.dataset.view, 'axis'):
+ self.axis = self.dataset.view.axis
+ self.figure = self.axis.figure
+
+ if not advanced_present:
+ self.ui.iteration_list = ['0']
+ self.ui.smooth_list.clear()
+ self.ui.smooth_list.addItems(self.ui.iteration_list)
+ self.ui.smooth_list.setCurrentIndex(0)
+
+ if 'low_loss' in self.dataset.metadata:
+ self.ui.iteration_list = ['0']
+
+
+ self.figure.canvas.mpl_connect('button_press_event', self.plot)
+
+
+ self.plot()
+
+ def update(self):
+ # self.setWindowTitle('update')
+ self.ui.edit1.setText(f"{self.peaks['fit_start']:.2f}")
+ self.ui.edit2.setText(f"{self.peaks['fit_end']:.2f}")
+
+ peak_index = self.ui.list3.currentIndex()
+ if str(peak_index) not in self.peaks['peaks']:
+ self.peaks['peaks'][str(peak_index)] = {'position': self.energy_scale[1], 'amplitude': 1000.0,
+ 'width': 1.0, 'type': 'Gauss', 'asymmetry': 0}
+ self.ui.list4.setCurrentText(self.peaks['peaks'][str(peak_index)]['type'])
+ if 'associated_edge' in self.peaks['peaks'][str(peak_index)]:
+ self.ui.unit3.setText(self.peaks['peaks'][str(peak_index)]['associated_edge'])
+ else:
+ self.ui.unit3.setText('')
+ self.ui.edit5.setText(f"{self.peaks['peaks'][str(peak_index)]['position']:.2f}")
+ self.ui.edit6.setText(f"{self.peaks['peaks'][str(peak_index)]['amplitude']:.2f}")
+ self.ui.edit7.setText(f"{self.peaks['peaks'][str(peak_index)]['width']:.2f}")
+ if 'asymmetry' not in self.peaks['peaks'][str(peak_index)]:
+ self.peaks['peaks'][str(peak_index)]['asymmetry'] = 0.
+ self.ui.edit8.setText(f"{self.peaks['peaks'][str(peak_index)]['asymmetry']:.2f}")
+
+ def plot(self):
+
+ spec_dim = ft.get_dimensions_by_type(sidpy.DimensionType.SPECTRAL, self.dataset)
+ spec_dim = spec_dim[0]
+ self.energy_scale = spec_dim[1].values
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ spectrum = self.dataset.view.get_spectrum()
+ self.axis = self.dataset.view.axes[1]
+ name = 's'
+ if 'zero_loss' in self.dataset.metadata:
+ x = self.dataset.view.x
+ y = self.dataset.view.y
+ self.energy_scale -= self.dataset.metadata['zero_loss']['shifts'][x, y]
+ name = f"shift { self.dataset.metadata['zero_loss']['shifts'][x, y]:.3f}"
+ self.setWindowTitle(f'plot {x}')
+ else:
+ spectrum = np.array(self.dataset)
+ self.axis = self.dataset.view.axis
+
+ x_limit = self.axis.get_xlim()
+ y_limit = self.axis.get_ylim()
+ self.axis.clear()
+
+ self.axis.plot(self.energy_scale, spectrum, label='spectrum')
+ if 'zero_loss' in self.dataset.metadata:
+ self.axis.plot(self.energy_scale, spectrum, label=name)
+
+ if len(self.model) > 1:
+ self.axis.plot(self.energy_scale, self.model, label='model')
+ self.axis.plot(self.energy_scale, spectrum - self.model, label='difference')
+ #self.axis.plot(self.energy_scale, (spectrum - self.model) / np.sqrt(spectrum), label='Poisson')
+ self.axis.legend()
+ self.axis.set_xlim(x_limit)
+ self.axis.set_ylim(y_limit)
+ self.axis.figure.canvas.draw_idle()
+
+ for index, peak in self.peaks['peaks'].items():
+ p = [peak['position'], peak['amplitude'], peak['width']]
+ self.axis.plot(self.energy_scale, eels_tools.gauss(self.energy_scale, p))
+
+ def fit_peaks(self):
+ """Fit spectrum with peaks given in peaks dictionary"""
+ print('Fitting peaks...')
+ p_in = []
+ for key, peak in self.peaks['peaks'].items():
+ if key.isdigit():
+ p_in.append(peak['position'])
+ p_in.append(peak['amplitude'])
+ p_in.append(peak['width'])
+
+ # check whether we have a spectral image or just a single spectrum
+ if self.dataset.data_type == sidpy.DataType.SPECTRAL_IMAGE:
+ spectrum = self.dataset.view.get_spectrum()
+ else:
+ spectrum = np.array(self.dataset)
+
+ # set the energy scale and fit start and end points
+ energy_scale = np.array(self.energy_scale)
+ start_channel = np.searchsorted(energy_scale, self.peaks['fit_start'])
+ end_channel = np.searchsorted(energy_scale, self.peaks['fit_end'])
+
+ energy_scale = self.energy_scale[start_channel:end_channel]
+ # select the core loss model if it exists. Otherwise, we will fit to the full spectrum.
+ if 'model' in self.dataset.metadata:
+ model = self.dataset.metadata['model'][start_channel:end_channel]
+ elif self.core_loss:
+ print('Core loss model found. Fitting on top of the model.')
+ model = self.dataset.metadata['edges']['model']['spectrum'][start_channel:end_channel]
+ else:
+ print('No core loss model found. Fitting to the full spectrum.')
+ model = np.zeros(end_channel - start_channel)
+
+ # if we have a core loss model we will only fit the difference between the model and the data.
+ difference = np.array(spectrum[start_channel:end_channel] - model)
+
+ # find the optimum fitting parameters
+ [self.p_out, _] = scipy.optimize.leastsq(eels_tools.residuals_smooth, np.array(p_in), ftol=1e-3,
+ args=(energy_scale, difference, False))
+
+ # construct the fit data from the optimized parameters
+ self.peak_model = np.zeros(len(self.energy_scale))
+ self.model = np.zeros(len(self.energy_scale))
+ self.model[start_channel:end_channel] = model
+ fit = eels_tools.model_smooth(energy_scale, self.p_out, False)
+ self.peak_model[start_channel:end_channel] = fit
+ self.dataset.metadata['peak_fit']['edge_model'] = self.model
+ self.model = self.model + self.peak_model
+ self.dataset.metadata['peak_fit']['peak_model'] = self.peak_model
+
+ for key, peak in self.peaks['peaks'].items():
+ if key.isdigit():
+ p_index = int(key)*3
+ self.peaks['peaks'][key] = {'position': self.p_out[p_index],
+ 'amplitude': self.p_out[p_index+1],
+ 'width': self.p_out[p_index+2],
+ 'associated_edge': ''}
+
+ self.find_associated_edges()
+ self.find_white_lines()
+ self.update()
+ self.plot()
+
+ def smooth(self):
+ """Fit lots of Gaussian to spectrum and let the program sort it out
+
+ We sort the peaks by area under the Gaussians, assuming that small areas mean noise.
+
+ """
+ iterations = int(self.ui.smooth_list.currentIndex())
+
+ self.peak_model, self.peak_out_list, number_of_peaks = smooth(self.dataset, iterations, advanced_present)
+
+ spec_dim = ft.get_dimensions_by_type('SPECTRAL', self.dataset)[0]
+ if spec_dim[1][0] > 0:
+ self.model = self.dataset.metadata['edges']['model']['spectrum']
+ elif 'model' in self.dataset.metadata:
+ self.model = self.dataset.metadata['model']
+ else:
+ self.model = np.zeros(len(spec_dim[1]))
+
+ self.ui.find_edit.setText(str(number_of_peaks))
+
+ self.dataset.metadata['peak_fit']['edge_model'] = self.model
+ self.model = self.model + self.peak_model
+ self.dataset.metadata['peak_fit']['peak_model'] = self.peak_model
+ self.dataset.metadata['peak_fit']['peak_out_list'] = self.peak_out_list
+
+ self.update()
+ self.plot()
+
+ def find_associated_edges(self):
+ onsets = []
+ edges = []
+ if 'edges' in self.dataset.metadata:
+ for key, edge in self.dataset.metadata['edges'].items():
+ if key.isdigit():
+ element = edge['element']
+ for sym in edge['all_edges']: # TODO: Could be replaced with exclude
+ onsets.append(edge['all_edges'][sym]['onset'] + edge['chemical_shift'])
+ # if 'sym' == edge['symmetry']:
+ edges.append([key, f"{element}-{sym}", onsets[-1]])
+ for key, peak in self.peaks['peaks'].items():
+ if key.isdigit():
+ distance = self.energy_scale[-1]
+ index = -1
+ for ii, onset in enumerate(onsets):
+ if onset < peak['position'] < onset+50:
+ if distance > np.abs(peak['position'] - onset):
+ distance = np.abs(peak['position'] - onset) # TODO: check whether absolute is good
+ distance_onset = peak['position'] - onset
+ index = ii
+ if index >= 0:
+ peak['associated_edge'] = edges[index][1] # check if more info is necessary
+ peak['distance_to_onset'] = distance_onset
+
+ def find_white_lines(self):
+ eels_tools.find_white_lines(self.dataset)
+
+ self.ui.wl_list = []
+ self.ui.wls_list = []
+ if len(self.peaks['white_line_ratios']) > 0:
+ for key in self.peaks['white_line_ratios']:
+ self.ui.wl_list.append(key)
+ for key in self.peaks['white_line_sums']:
+ self.ui.wls_list.append(key)
+
+ self.ui.listwl.clear()
+ self.ui.listwl.addItems(self.ui.wl_list)
+ self.ui.listwl.setCurrentIndex(0)
+ self.ui.unitswl.setText(f"{self.peaks['white_line_ratios'][self.ui.wl_list[0]]:.2f}")
+
+ self.ui.listwls.clear()
+ self.ui.listwls.addItems(self.ui.wls_list)
+ self.ui.listwls.setCurrentIndex(0)
+ self.ui.unitswls.setText(f"{self.peaks['white_line_sums'][self.ui.wls_list[0]]*1e6:.4f} ppm")
+ else:
+ self.ui.wl_list.append('Ratio')
+ self.ui.wls_list.append('Sum')
+
+ self.ui.listwl.clear()
+ self.ui.listwl.addItems(self.ui.wl_list)
+ self.ui.listwl.setCurrentIndex(0)
+ self.ui.unitswl.setText('')
+
+ self.ui.listwls.clear()
+ self.ui.listwls.addItems(self.ui.wls_list)
+ self.ui.listwls.setCurrentIndex(0)
+ self.ui.unitswls.setText('')
+
+ def find_peaks(self):
+ number_of_peaks = int(str(self.ui.find_edit.displayText()).strip())
+
+ # is now sorted in smooth function
+ # flat_list = [item for sublist in self.peak_out_list for item in sublist]
+ # new_list = np.reshape(flat_list, [len(flat_list) // 3, 3])
+ # arg_list = np.argsort(np.abs(new_list[:, 1]))
+
+ self.ui.peak_list = []
+ self.peaks['peaks'] = {}
+ for i in range(number_of_peaks):
+ self.ui.peak_list.append(f'Peak {i+1}')
+ p = self.peak_out_list[i]
+ self.peaks['peaks'][str(i)] = {'position': p[0], 'amplitude': p[1], 'width': p[2], 'type': 'Gauss',
+ 'asymmetry': 0}
+
+ self.ui.peak_list.append(f'add peak')
+ self.ui.list3.clear()
+ self.ui.list3.addItems(self.ui.peak_list)
+ self.ui.list3.setCurrentIndex(0)
+ self.find_associated_edges()
+ self.find_white_lines()
+
+ self.update()
+ self.plot()
+
+ def set_peak_list(self):
+ self.ui.peak_list = []
+ if 'peaks' not in self.peaks:
+ self.peaks['peaks'] = {}
+ key = 0
+ for key in self.peaks['peaks']:
+ if key.isdigit():
+ self.ui.peak_list.append(f'Peak {int(key) + 1}')
+ self.ui.find_edit.setText(str(int(key) + 1))
+ self.ui.peak_list.append(f'add peak')
+ self.ui.list3.clear()
+ self.ui.list3.addItems(self.ui.peak_list)
+ self.ui.list3.setCurrentIndex(0)
+
+ def on_enter(self):
+ if self.sender() == self.ui.edit1:
+ value = float(str(self.ui.edit1.displayText()).strip())
+ if value < self.energy_scale[0]:
+ value = self.energy_scale[0]
+ if value > self.energy_scale[-5]:
+ value = self.energy_scale[-5]
+ self.peaks['fit_start'] = value
+ self.ui.edit1.setText(str(self.peaks['fit_start']))
+ elif self.sender() == self.ui.edit2:
+ value = float(str(self.ui.edit2.displayText()).strip())
+ if value < self.energy_scale[5]:
+ value = self.energy_scale[5]
+ if value > self.energy_scale[-1]:
+ value = self.energy_scale[-1]
+ self.peaks['fit_end'] = value
+ self.ui.edit2.setText(str(self.peaks['fit_end']))
+ elif self.sender() == self.ui.edit5:
+ value = float(str(self.ui.edit5.displayText()).strip())
+ peak_index = self.ui.list3.currentIndex()
+ self.peaks['peaks'][str(peak_index)]['position'] = value
+ elif self.sender() == self.ui.edit6:
+ value = float(str(self.ui.edit6.displayText()).strip())
+ peak_index = self.ui.list3.currentIndex()
+ self.peaks['peaks'][str(peak_index)]['amplitude'] = value
+ elif self.sender() == self.ui.edit7:
+ value = float(str(self.ui.edit7.displayText()).strip())
+ peak_index = self.ui.list3.currentIndex()
+ self.peaks['peaks'][str(peak_index)]['width'] = value
+
+ def on_list_enter(self):
+ self.setWindowTitle(f'list {self.sender}, {self.ui.list_model}')
+ if self.sender() == self.ui.list3:
+ if self.ui.list3.currentText().lower() == 'add peak':
+ peak_index = self.ui.list3.currentIndex()
+ self.ui.list3.insertItem(peak_index, f'Peak {peak_index+1}')
+ self.peaks['peaks'][str(peak_index+1)] = {'position': self.energy_scale[1],
+ 'amplitude': 1000.0, 'width': 1.0,
+ 'type': 'Gauss', 'asymmetry': 0}
+ self.ui.list3.setCurrentIndex(peak_index)
+ self.update()
+
+ elif self.sender() == self.ui.listwls or self.sender() == self.ui.listwl:
+ wl_index = self.sender().currentIndex()
+
+ self.ui.listwl.setCurrentIndex(wl_index)
+ self.ui.unitswl.setText(f"{self.peaks['white_line_ratios'][self.ui.wl_list[wl_index]]:.2f}")
+ self.ui.listwls.setCurrentIndex(wl_index)
+ self.ui.unitswls.setText(f"{self.peaks['white_line_sums'][self.ui.wls_list[wl_index]] * 1e6:.4f} ppm")
+ elif self.sender() == self.ui.list_model:
+ self.setWindowTitle('list 1')
+ if self.sender().currentIndex() == 1:
+ if 'resolution_function' in self.datasets:
+ self.setWindowTitle('list 2')
+ self.dataset.metadata['model'] = np.array(self.datasets['resolution_function'])
+ else:
+ self.ui.list_model.setCurrentIndex(0)
+ else:
+ self.ui.list_model.setCurrentIndex(0)
+ def set_action(self):
+ pass
+ self.ui.edit1.editingFinished.connect(self.on_enter)
+ self.ui.edit2.editingFinished.connect(self.on_enter)
+ self.ui.edit5.editingFinished.connect(self.on_enter)
+ self.ui.edit6.editingFinished.connect(self.on_enter)
+ self.ui.edit7.editingFinished.connect(self.on_enter)
+ self.ui.edit8.editingFinished.connect(self.on_enter)
+ self.ui.list3.activated[str].connect(self.on_list_enter)
+ self.ui.find_button.clicked.connect(self.find_peaks)
+ self.ui.smooth_button.clicked.connect(self.smooth)
+ self.ui.fit_button.clicked.connect(self.fit_peaks)
+ if hasattr(self.ui, 'listwls'):
+ self.ui.listwls.activated[str].connect(self.on_list_enter)
+ self.ui.listwl.activated[str].connect(self.on_list_enter)
+ else:
+ self.ui.zl_button.clicked.connect(self.fit_zero_loss)
+ self.ui.drude_button.clicked.connect(self.smooth)
+ self.ui.list_model.activated[str].connect(self.on_list_enter)
+
+ def fit_zero_loss(self):
+ """get shift of spectrum form zero-loss peak position"""
+ zero_loss_fit_width=0.3
+
+ energy_scale = self.dataset.energy_loss
+ zl_dataset = self.dataset.copy()
+ zl_dataset.title = 'resolution_function'
+ shifts = np.zeros(self.dataset.shape[0:2])
+ zero_p = np.zeros([self.dataset.shape[0],self.dataset.shape[1],6])
+ fwhm_p = np.zeros(self.dataset.shape[0:2])
+ bin_x = bin_y = 1
+ total_spec = int(self.dataset.shape[0]/bin_x)*int(self.dataset.shape[1]/bin_y)
+ self.ui.progress.setMaximum(total_spec)
+ self.ui.progress.setValue(0)
+ zero_loss_fit_width=0.3
+ ind = 0
+ for x in range(self.dataset.shape[0]):
+ for y in range(self.dataset.shape[1]):
+ ind += 1
+ self.ui.progress.setValue(ind)
+ spectrum = self.dataset[x, y, :]
+ fwhm, delta_e = eels_tools.fix_energy_scale(spectrum, energy_scale)
+ z_loss, p_zl = eels_tools.resolution_function(energy_scale - delta_e, spectrum, zero_loss_fit_width)
+ fwhm2, delta_e2 = eels_tools.fix_energy_scale(z_loss, energy_scale - delta_e)
+ shifts[x, y] = delta_e + delta_e2
+ zero_p[x,y,:] = p_zl
+ zl_dataset[x,y] = z_loss
+ fwhm_p[x,y] = fwhm2
+
+ zl_dataset.metadata['zero_loss'] = {'parameter': zero_p,
+ 'shifts': shifts,
+ 'fwhm': fwhm_p}
+ self.dataset.metadata['zero_loss'] = {'parameter': zero_p,
+ 'shifts': shifts,
+ 'fwhm': fwhm_p}
+
+ self.datasets['resolution_function'] = zl_dataset
+ self.update()
+ self.plot()
+
+
+
+[docs]def smooth(dataset, iterations, advanced_present):
+ """Gaussian mixture model (non-Bayesian)
+
+ Fit lots of Gaussian to spectrum and let the program sort it out
+ We sort the peaks by area under the Gaussians, assuming that small areas mean noise.
+
+ """
+
+ # TODO: add sensitivity to dialog and the two functions below
+ peaks = dataset.metadata['peak_fit']
+
+ if advanced_present and iterations > 1:
+ peak_model, peak_out_list = advanced_eels_tools.smooth(dataset, peaks['fit_start'],
+ peaks['fit_end'], iterations=iterations)
+ else:
+ peak_model, peak_out_list = eels_tools.find_peaks(dataset, peaks['fit_start'], peaks['fit_end'])
+ peak_out_list = [peak_out_list]
+
+ flat_list = [item for sublist in peak_out_list for item in sublist]
+ new_list = np.reshape(flat_list, [len(flat_list) // 3, 3])
+ area = np.sqrt(2 * np.pi) * np.abs(new_list[:, 1]) * np.abs(new_list[:, 2] / np.sqrt(2 * np.log(2)))
+ arg_list = np.argsort(area)[::-1]
+ area = area[arg_list]
+ peak_out_list = new_list[arg_list]
+
+ number_of_peaks = np.searchsorted(area * -1, -np.average(area))
+
+ return peak_model, peak_out_list, number_of_peaks
+
+"""Functions to calculate electron probe"""
+import numpy as np
+import pyTEMlib.image_tools
+import scipy.ndimage as ndimage
+
+
+[docs]def make_gauss(size_x, size_y, width=1.0, x0=0.0, y0=0.0, intensity=1.0):
+ """Make a Gaussian shaped probe """
+ size_x = size_x / 2
+ size_y = size_y / 2
+ x, y = np.mgrid[-size_x:size_x, -size_y:size_y]
+ g = np.exp(-((x - x0) ** 2 + (y - y0) ** 2) / 2.0 / width ** 2)
+ probe = g / g.sum() * intensity
+
+ return probe
+
+
+[docs]def make_lorentz(size_x, size_y, gamma=1.0, x0=0., y0=0., intensity=1.):
+ """Make a Lorentzian shaped probe """
+
+ size_x = np.floor(size_x / 2)
+ size_y = np.floor(size_y / 2)
+ x, y = np.mgrid[-size_x:size_x, -size_y:size_y]
+ g = gamma / (2 * np.pi) / np.power(((x - x0) ** 2 + (y - y0) ** 2 + gamma ** 2), 1.5)
+ probe = g / g.sum() * intensity
+ return probe
+
+
+[docs]def zero_loss_peak_weight():
+ # US100 zero_loss peak for Cc of aberrations
+ x = np.linspace(-0.5, 0.9, 29)
+ y = [0.0143, 0.0193, 0.0281, 0.0440, 0.0768, 0.1447, 0.2785, 0.4955, 0.7442, 0.9380, 1.0000, 0.9483, 0.8596,
+ 0.7620, 0.6539, 0.5515, 0.4478, 0.3500, 0.2683, 0.1979, 0.1410, 0.1021, 0.0752, 0.0545, 0.0401, 0.0300,
+ 0.0229, 0.0176, 0.0139]
+ return x, y
+
+
+[docs]def make_chi(phi, theta, aberrations):
+ maximum_aberration_order = 5
+ chi = np.zeros(theta.shape)
+ for n in range(maximum_aberration_order + 1): # First Sum up to fifth order
+ term_first_sum = np.power(theta, n + 1) / (n + 1) # term in first sum
+
+ second_sum = np.zeros(theta.shape) # second Sum initialized with zeros
+ for m in range((n + 1) % 2, n + 2, 2):
+ if m > 0:
+ if f'C{n}{m}a' not in aberrations: # Set non existent aberrations coefficient to zero
+ aberrations[f'C{n}{m}a'] = 0.
+ if f'C{n}{m}b' not in aberrations:
+ aberrations[f'C{n}{m}b'] = 0.
+
+ # term in second sum
+ second_sum = second_sum + aberrations[f'C{n}{m}a'] * np.cos(m * phi) + aberrations[
+ f'C{n}{m}b'] * np.sin(m * phi)
+ else:
+ if f'C{n}{m}' not in aberrations: # Set non existent aberrations coefficient to zero
+ aberrations[f'C{n}{m}'] = 0.
+
+ # term in second sum
+ second_sum = second_sum + aberrations[f'C{n}{m}']
+ chi = chi + term_first_sum * second_sum * 2 * np.pi / aberrations['wavelength']
+
+ return chi
+
+
+[docs]def get_chi(ab, size_x, size_y, verbose=False):
+ """ Get aberration function chi without defocus spread
+
+ # Internally reciprocal lattice vectors in 1/nm or rad.
+ # All calculations of chi in angles.
+ # All aberration coefficients in nm
+ """
+ aperture_angle = ab['convergence_angle'] / 1000.0 # in rad
+
+ wavelength = pyTEMlib.image_tools.get_wavelength(ab['acceleration_voltage'])
+ if verbose:
+ print(f"Acceleration voltage {ab['acceleration_voltage'] / 1000:}kV => wavelength {wavelength * 1000.:.2f}pm")
+
+ ab['wavelength'] = wavelength
+
+ # Reciprocal plane in 1/nm
+ dk = 1 / ab['FOV']
+ k_x = np.array(dk * (-size_x / 2. + np.arange(size_x)))
+ k_y = np.array(dk * (-size_y / 2. + np.arange(size_y)))
+ t_x_v, t_y_v = np.meshgrid(k_x, k_y)
+
+ # define reciprocal plane in angles
+ phi = np.arctan2(t_x_v, t_y_v)
+ theta = np.arctan2(np.sqrt(t_x_v ** 2 + t_y_v ** 2), 1 / wavelength)
+
+ # calculate chi
+ chi = make_chi(phi, theta, ab)
+
+ # Aperture function
+ mask = theta >= aperture_angle
+
+ aperture = np.ones((size_x, size_y), dtype=float)
+ aperture[mask] = 0.
+
+ return chi, aperture
+
+
+[docs]def print_aberrations(ab):
+ from IPython.display import HTML, display
+ output = '<html><body>'
+ output += f"Aberrations [nm] for acceleration voltage: {ab['acceleration_voltage'] / 1e3:.0f} kV"
+ output += '<table>'
+ output += f"<tr><td> C10 </td><td> {ab['C10']:.1f} </tr>"
+ output += f"<tr><td> C12a </td><td> {ab['C12a']:20.1f} <td> C12b </td><td> {ab['C12b']:20.1f} </tr>"
+ output += f"<tr><td> C21a </td><td> {ab['C21a']:.1f} <td> C21b </td><td> {ab['C21b']:.1f} "
+ output += f" <td> C23a </td><td> {ab['C23a']:.1f} <td> C23b </td><td> {ab['C23b']:.1f} </tr>"
+ output += f"<tr><td> C30 </td><td> {ab['C30']:.1f} </tr>"
+ output += f"<tr><td> C32a </td><td> {ab['C32a']:20.1f} <td> C32b </td><td> {ab['C32b']:20.1f} "
+ output += f"<td> C34a </td><td> {ab['C34a']:20.1f} <td> C34b </td><td> {ab['C34b']:20.1f} </tr>"
+ output += f"<tr><td> C41a </td><td> {ab['C41a']:.3g} <td> C41b </td><td> {ab['C41b']:.3g} "
+ output += f" <td> C43a </td><td> {ab['C43a']:.3g} <td> C43b </td><td> {ab['C41b']:.3g} "
+ output += f" <td> C45a </td><td> {ab['C45a']:.3g} <td> C45b </td><td> {ab['C45b']:.3g} </tr>"
+ output += f"<tr><td> C50 </td><td> {ab['C50']:.3g} </tr>"
+ output += f"<tr><td> C52a </td><td> {ab['C52a']:20.1f} <td> C52b </td><td> {ab['C52b']:20.1f} "
+ output += f"<td> C54a </td><td> {ab['C54a']:20.1f} <td> C54b </td><td> {ab['C54b']:20.1f} "
+ output += f"<td> C56a </td><td> {ab['C56a']:20.1f} <td> C56b </td><td> {ab['C56b']:20.1f} </tr>"
+ output += f"<tr><td> Cc </td><td> {ab['Cc']:.3g} </tr>"
+
+ output += '</table></body></html>'
+
+ display(HTML(output))
+
+
+[docs]def get_ronchigram(size, ab, scale='mrad'):
+ """ Get Ronchigram
+
+ """
+ size_x = size_y = size
+ chi, A_k = get_chi(ab, size_x, size_y)
+
+ v_noise = np.random.rand(size_x, size_y)
+ smoothing = 5
+ phi_r = ndimage.gaussian_filter(v_noise, sigma=(smoothing, smoothing), order=0)
+
+ sigma = 6 # 6 for carbon and thin
+
+ q_r = np.exp(-1j * sigma * phi_r)
+ # q_r = 1-phi_r * sigma
+
+ T_k = A_k * (np.exp(-1j * chi))
+ t_r = (np.fft.ifft2(np.fft.fftshift(T_k)))
+
+ psi_k = np.fft.fftshift(np.fft.fft2(q_r * t_r))
+
+ ronchigram = np.absolute(psi_k * np.conjugate(psi_k))
+
+ fov_reciprocal = 1 / ab['FOV'] * size_x / 2
+ if scale == '1/nm':
+ extent = [-fov_reciprocal, fov_reciprocal, -fov_reciprocal, fov_reciprocal]
+ ylabel = 'reciprocal distance [1/nm]'
+ else:
+ fov_mrad = fov_reciprocal * ab['wavelength'] * 1000
+ extent = [-fov_mrad, fov_mrad, -fov_mrad, fov_mrad]
+ ylabel = 'reciprocal distance [mrad]'
+
+ ab['ronchi_extent'] = extent
+ ab['ronchi_label'] = ylabel
+ return ronchigram
+
+
+[docs]def get_chi_2(ab, u, v):
+ chi1 = ab['C10'] * (u ** 2 + v ** 2) / 2 \
+ + ab['C12a'] * (u ** 2 - v ** 2) / 2 \
+ - ab['C12b'] * u * v
+
+ chi2 = ab['C21a'] * (u ** 3 + u * v ** 2) / 3 \
+ - ab['C21b'] * (u ** 2 * v + v ** 3) / 3 \
+ + ab['C23a'] * (u ** 3 - 3 * u * v ** 2) / 3 \
+ - ab['C23b'] * (3 * u ** 2 * v - v ** 3) / 3
+
+ chi3 = ab['C30'] * (u ** 4 + 2 * u ** 2 * v ** 2 + v ** 4) / 4 \
+ + ab['C32a'] * (u ** 4 - v ** 4) / 4 \
+ - ab['C32b'] * (u ** 3 * v + u * v ** 3) / 2 \
+ + ab['C34a'] * (u ** 4 - 6 * u ** 2 * v ** 2 + v ** 4) / 4 \
+ - ab['C34b'] * (4 * u ** 3 * v - 4 * u * v ** 3) / 4
+
+ chi4 = ab['C41a'] * (u ** 5 + 2 * u ** 3 * v ** 2 + u * v ** 4) / 5 \
+ - ab['C41b'] * (u ** 4 * v + 2 * u ** 2 * v ** 3 + v ** 5) / 5 \
+ + ab['C43a'] * (u ** 5 - 2 * u ** 3 * v ** 2 - 3 * u * v ** 4) / 5 \
+ - ab['C43b'] * (3 * u ** 4 * v + 2 * u ** 2 * v ** 3 - v ** 5) / 5 \
+ + ab['C45a'] * (u ** 5 - 10 * u ** 3 * v ** 2 + 5 * u * v ** 4) / 5 \
+ - ab['C45b'] * (5 * u ** 4 * v - 10 * u ** 2 * v ** 3 + v ** 5) / 5
+
+ chi5 = ab['C50'] * (u ** 6 + 3 * u ** 4 * v ** 2 + 3 * u ** 2 * v ** 4 + v ** 6) / 6 \
+ + ab['C52a'] * (u ** 6 + u ** 4 * v ** 2 - u ** 2 * v ** 4 - v ** 6) / 6 \
+ - ab['C52b'] * (2 * u ** 5 * v + 4 * u ** 3 * v ** 3 + 2 * u * v ** 5) / 6 \
+ + ab['C54a'] * (u ** 6 - 5 * u ** 4 * v ** 2 - 5 * u ** 2 * v ** 4 + v ** 6) / 6 \
+ - ab['C54b'] * (4 * u ** 5 * v - 4 * u * v ** 5) / 6 \
+ + ab['C56a'] * (u ** 6 - 15 * u ** 4 * v ** 2 + 15 * u ** 2 * v ** 4 - v ** 6) / 6 \
+ - ab['C56b'] * (6 * u ** 5 * v - 20 * u ** 3 * v ** 3 + 6 * u * v ** 5) / 6
+
+ chi = chi1 + chi2 + chi3 + chi4 + chi5
+ return chi * 2 * np.pi / ab['wavelength']
+
+
+[docs]def get_d2chidu2(ab, u, v):
+ d2chi1du2 = ab['C10'] + ab['C12a']
+
+ d2chi2du2 = ab['C21a'] * 2 * u \
+ - ab['C21b'] * 2 / 3 * v \
+ + ab['C23a'] * 2 * u \
+ - ab['C23b'] * 2 * v
+
+ d2chi3du2 = ab['C30'] * (3 * u ** 2 + v ** 2) \
+ + ab['C32a'] * 3 * u ** 2 \
+ - ab['C32b'] * 3 * u * v \
+ + ab['C34a'] * (3 * u ** 2 - 3 * v ** 2) \
+ - ab['C34b'] * 6 * u * v
+
+ d2chi4du2 = ab['C41a'] * 4 / 5 * (5 * u ** 3 + 3 * u * v ** 2) \
+ - ab['C41b'] * 4 / 5 * (3 * u ** 2 * v + v ** 3) \
+ + ab['C43a'] * 4 / 5 * (5 * u ** 3 - 3 * u * v ** 2) \
+ - ab['C43b'] * 4 / 5 * (9 * u ** 2 * v + v ** 3) \
+ + ab['C45a'] * 4 * (u ** 3 - 3 * u * v ** 2) \
+ - ab['C45b'] * 4 * (3 * u ** 2 * v - v ** 3)
+
+ d2chi5du2 = ab['C50'] * (5 * u ** 4 + 6 * u ** 2 * v ** 2 + v ** 4) \
+ + ab['C52a'] * (15 * u ** 4 + 6 * u ** 2 * v ** 2 - v ** 4) / 3 \
+ - ab['C52b'] * (20 * u ** 3 * v + 12 * u * v ** 3) / 3 \
+ + ab['C54a'] * 5 / 3 * (3 * u ** 4 - 6 * u ** 2 * v ** 2 - v ** 4) \
+ - ab['C54b'] * 5 / 3 * (8 * u ** 3 * v) \
+ + ab['C56a'] * 5 * (u ** 4 - 6 * u ** 2 * v ** 2 + v ** 4) \
+ - ab['C56b'] * 20 * (u ** 3 * v - u * v ** 3)
+
+ d2chidu2 = d2chi1du2 + d2chi2du2 + d2chi3du2 + d2chi4du2 + d2chi5du2
+ return d2chidu2
+
+
+[docs]def get_d2chidudv(ab, u, v):
+ d2chi1dudv = -ab['C12b']
+
+ d2chi2dudv = ab['C21a'] * 2 / 3 * v \
+ - ab['C21b'] * 2 / 3 * u \
+ - ab['C23a'] * 2 * v \
+ - ab['C23b'] * 2 * u
+
+ d2chi3dudv = ab['C30'] * 2 * u * v \
+ + ab['C32a'] * 0 \
+ - ab['C32b'] * 3 / 2 * (u ** 2 + v ** 2) \
+ - ab['C34a'] * 6 * u * v \
+ - ab['C34b'] * 3 * (u ** 2 - v ** 2)
+
+ d2chi4dudv = ab['C41a'] * 4 / 5 * (3 * u ** 2 * v + v ** 3) \
+ - ab['C41b'] * 4 / 5 * (u ** 3 + 3 * u * v ** 2) \
+ - ab['C43a'] * 12 / 5 * (u ** 2 * v + v ** 3) \
+ - ab['C43b'] * 12 / 5 * (u ** 3 + u * v ** 2) \
+ - ab['C45a'] * 4 * (3 * u ** 2 * v - v ** 3) \
+ - ab['C45b'] * 4 * (u ** 3 - 3 * u * v ** 2)
+
+ d2chi5dudv = ab['C50'] * 4 * u * v * (u ** 2 + v ** 2) \
+ + ab['C52a'] * 4 / 3 * (u ** 3 * v - u * v ** 3) \
+ - ab['C52b'] * (5 * u ** 4 + 18 * u ** 2 * v ** 2 + 5 * v ** 4) / 3 \
+ - ab['C54a'] * 20 / 3 * (u ** 3 * v + u * v ** 3) \
+ - ab['C54b'] * 10 / 3 * (u ** 4 - v ** 4) \
+ - ab['C56a'] * 20 * (u ** 3 * v - u * v ** 3) \
+ - ab['C56b'] * 5 * (u ** 4 - 6 * u ** 2 * v ** 2 + v ** 4)
+
+ d2chidudv = d2chi1dudv + d2chi2dudv + d2chi3dudv + d2chi4dudv + d2chi5dudv
+ return d2chidudv
+
+
+[docs]def get_d2chidv2(ab, u, v):
+ d2chi1dv2 = ab['C10'] - ab['C12a']
+
+ d2chi2dv2 = ab['C21a'] * 2 / 3 * u \
+ - ab['C21b'] * 2 * v \
+ - ab['C23a'] * 2 * u \
+ + ab['C23b'] * 2 * v
+
+ d2chi3dv2 = ab['C30'] * (u ** 2 + 3 * v ** 2) \
+ - ab['C32a'] * 3 * v ** 2 \
+ - ab['C32b'] * 3 * v * u \
+ - ab['C34a'] * 3 * (u ** 2 - v ** 2) \
+ + ab['C34b'] * 6 * u * v
+
+ d2chi4dv2 = ab['C41a'] * 4 / 5 * (u ** 3 + 3 * u * v ** 2) \
+ - ab['C41b'] * 4 / 5 * (3 * u ** 2 * v + 5 * v ** 3) \
+ - ab['C43a'] * 4 / 5 * (u ** 3 + 9 * u * v ** 2) \
+ - ab['C43b'] * 4 / 5 * (3 * u ** 2 * v - 5 * v ** 3) \
+ - ab['C45a'] * 4 * (u ** 3 - 3 * u * v ** 2) \
+ + ab['C45b'] * 4 * (3 * u ** 2 * v - v ** 3)
+
+ d2chi5dv2 = ab['C50'] * (u ** 4 + 6 * u ** 2 * v ** 2 + 5 * v ** 4) \
+ + ab['C52a'] * (u ** 4 - 6 * u ** 2 * v ** 2 - 15 * v ** 4) / 3 \
+ - ab['C52b'] * (12 * u ** 3 * v + 20 * u * v ** 3) / 3 \
+ - ab['C54a'] * 5 / 3 * (u ** 4 + 6 * u ** 2 * v ** 2 - 3 * v ** 4) \
+ + ab['C54b'] * 40 / 3 * u * v ** 3 \
+ - ab['C56a'] * 5 * (u ** 4 - 6 * u ** 2 * v ** 2 + v ** 4) \
+ + ab['C56b'] * 20 * (u ** 3 * v - u * v ** 3)
+
+ d2chidv2 = d2chi1dv2 + d2chi2dv2 + d2chi3dv2 + d2chi4dv2 + d2chi5dv2
+ return d2chidv2
+
+
+[docs]def get_source_energy_spread():
+ x = np.linspace(-0.5, .9, 29)
+ y = [0.0143, 0.0193, 0.0281, 0.0440, 0.0768, 0.1447, 0.2785, 0.4955, 0.7442, 0.9380, 1.0000, 0.9483, 0.8596, 0.7620,
+ 0.6539, 0.5515, 0.4478, 0.3500, 0.2683, 0.1979, 0.1410, 0.1021, 0.0752, 0.0545, 0.0401, 0.0300, 0.0229, 0.0176,
+ 0.0139]
+
+ return x, y
+
+
+[docs]def get_target_aberrations(TEM_name, acceleration_voltage):
+ ab = {}
+ if TEM_name == 'NionUS200':
+ if int(acceleration_voltage) == 200000:
+ print(f' **** Using Target Values at {acceleration_voltage / 1000}kV for Aberrations of {TEM_name}****')
+ ab = {'C10': 0, 'C12a': 0, 'C12b': 0, 'C21a': -335., 'C21b': 283., 'C23a': -34., 'C23b': 220.,
+ 'C30': -8080.,
+ 'C32a': 18800., 'C32b': -2260., 'C34a': 949., 'C34b': 949., 'C41a': 54883., 'C41b': -464102.,
+ 'C43a': 77240.5,
+ 'C43b': -540842., 'C45a': -79844.4, 'C45b': -76980.8, 'C50': 9546970., 'C52a': -2494290.,
+ 'C52b': 2999910.,
+ 'C54a': -2020140., 'C54b': -2019630., 'C56a': -535079., 'C56b': 1851850.}
+ ab['source_size'] = 0.051
+ ab['acceleration_voltage'] = acceleration_voltage
+ ab['convergence_angle'] = 30
+
+ ab['Cc'] = 1.3e6 # // Cc in nm
+
+ if int(acceleration_voltage) == 100000:
+ print(f' **** Using Target Values at {acceleration_voltage / 1000}kV for Aberrations of {TEM_name}****')
+
+ ab = {'C10': 0, 'C12a': 0, 'C12b': 0, 'C21a': 157., 'C21b': 169, 'C23a': -173., 'C23b': 48.7, 'C30': 201.,
+ 'C32a': 1090., 'C32b': 6840., 'C34a': 1010., 'C34b': 79.9, 'C41a': -210696., 'C41b': -262313.,
+ 'C43a': 348450., 'C43b': -9.7888e4, 'C45a': 6.80247e4, 'C45b': -3.14637e1, 'C50': -193896.,
+ 'C52a': -1178950, 'C52b': -7414340, 'C54a': -1753890, 'C54b': -1753890, 'C56a': -631786,
+ 'C56b': -165705}
+ ab['source_size'] = 0.051
+ ab['acceleration_voltage'] = acceleration_voltage
+ ab['convergence_angle'] = 30
+ ab['Cc'] = 1.3e6
+
+ if int(acceleration_voltage) == 60000:
+ print(f' **** Using Target Values at {acceleration_voltage / 1000}kV for Aberrations of {TEM_name}****')
+
+ ab = {'C10': 0, 'C12a': 0, 'C12b': 0, 'C21a': 11.5, 'C21b': 113, 'C23a': -136., 'C23b': 18.2, 'C30': 134.,
+ 'C32a': 1080., 'C32b': 773., 'C34a': 1190., 'C34b': -593., 'C41a': -179174., 'C41b': -350378.,
+ 'C43a': 528598, 'C43b': -257349., 'C45a': 63853.4, 'C45b': 1367.98, 'C50': 239021., 'C52a': 1569280.,
+ 'C52b': -6229310., 'C54a': -3167620., 'C54b': -449198., 'C56a': -907315., 'C56b': -16281.9}
+ ab['source_size'] = 0.081
+ ab['acceleration_voltage'] = acceleration_voltage
+ ab['convergence_angle'] = 30
+ ab['Cc'] = 1.3e6 # // Cc in nm
+
+ ab['origin'] = 'target aberrations'
+ ab['TEM_name'] = TEM_name
+ ab['wavelength'] = pyTEMlib.image_tools.get_wavelength(ab['acceleration_voltage'])
+
+ if TEM_name == 'NionUS100':
+ if int(acceleration_voltage) == 100000:
+ print(f' **** Using Target Values at {acceleration_voltage / 1000}kV for Aberrations of {TEM_name}****')
+
+ ab = {'C10': 0, 'C12a': 0, 'C12b': 0, 'C21a': 157., 'C21b': 169, 'C23a': -173., 'C23b': 48.7, 'C30': 201.,
+ 'C32a': 1090., 'C32b': 6840., 'C34a': 1010., 'C34b': 79.9, 'C41a': -210696., 'C41b': -262313.,
+ 'C43a': 348450., 'C43b': -9.7888e4, 'C45a': 6.80247e4, 'C45b': -3.14637e1, 'C50': -193896.,
+ 'C52a': -1178950, 'C52b': -7414340, 'C54a': -1753890, 'C54b': -1753890, 'C56a': -631786,
+ 'C56b': -165705}
+ ab['source_size'] = 0.051
+ ab['acceleration_voltage'] = acceleration_voltage
+ ab['convergence_angle'] = 30
+ ab['Cc'] = 1.3e6 # // Cc in nm
+
+ if int(acceleration_voltage) == 60000:
+ print(f' **** Using Target Values at {acceleration_voltage / 1000}kV for Aberrations of {TEM_name}****')
+
+ ab = {'C10': 0, 'C12a': 0, 'C12b': 0, 'C21a': 11.5, 'C21b': 113, 'C23a': -136., 'C23b': 18.2, 'C30': 134.,
+ 'C32a': 1080., 'C32b': 773., 'C34a': 1190., 'C34b': -593., 'C41a': -179174., 'C41b': -350378.,
+ 'C43a': 528598, 'C43b': -257349., 'C45a': 63853.4, 'C45b': 1367.98, 'C50': 239021., 'C52a': 1569280.,
+ 'C52b': -6229310., 'C54a': -3167620., 'C54b': -449198., 'C56a': -907315., 'C56b': -16281.9}
+ ab['source_size'] = 0.081
+ ab['acceleration_voltage'] = acceleration_voltage
+ ab['convergence_angle'] = 30
+ ab['Cc'] = 1.3e6 # // Cc in nm
+
+ ab['origin'] = 'target aberrations'
+ ab['TEM_name'] = TEM_name
+ ab['wavelength'] = pyTEMlib.image_tools.get_wavelength(ab['acceleration_voltage'])
+
+ if TEM_name == 'ZeissMC200':
+ ab = {'C10': 0, 'C12a': 0, 'C12b': 0, 'C21a': 0, 'C21b': 0, 'C23a': 0, 'C23b': 0, 'C30': 0.,
+ 'C32a': 0., 'C32b': -0., 'C34a': 0., 'C34b': 0., 'C41a': 0., 'C41b': -0., 'C43a': 0.,
+ 'C43b': -0., 'C45a': -0., 'C45b': -0., 'C50': 0., 'C52a': -0., 'C52b': 0.,
+ 'C54a': -0., 'C54b': -0., 'C56a': -0., 'C56b': 0.}
+ ab['C30'] = 2.2 * 1e6
+
+ ab['Cc'] = 2.0 * 1e6
+
+ ab['source_size'] = 0.2
+ ab['acceleration_voltage'] = acceleration_voltage
+ ab['convergence_angle'] = 10
+
+ ab['origin'] = 'target aberrations'
+ ab['TEM_name'] = TEM_name
+
+ ab['wavelength'] = pyTEMlib.image_tools.get_wavelength(ab['acceleration_voltage'])
+ return ab
+
+
+[docs]def get_ronchigram_2(size, ab, scale='mrad', threshold=3):
+ aperture_angle = ab['convergence_angle'] / 1000.0 # in rad
+
+ wavelength = pyTEMlib.image_tools.get_wavelength(ab['acceleration_voltage'])
+ # if verbose:
+ # print(f"Acceleration voltage {ab['acceleration_voltage']/1000:}kV => wavelength {wavelength*1000.:.2f}pm")
+
+ ab['wavelength'] = wavelength
+
+ size_x = size_y = size
+
+ # Reciprocal plane in 1/nm
+ dk = ab['reciprocal_FOV'] / size
+ k_x = np.array(dk * (-size_x / 2. + np.arange(size_x)))
+ k_y = np.array(dk * (-size_y / 2. + np.arange(size_y)))
+ t_x_v, t_y_v = np.meshgrid(k_x, k_y)
+
+ chi = get_chi_2(ab, t_x_v, t_y_v) # , verbose= True)
+ # define reciprocal plane in angles
+ phi = np.arctan2(t_x_v, t_y_v)
+ theta = np.arctan2(np.sqrt(t_x_v ** 2 + t_y_v ** 2), 1 / wavelength)
+
+ # Aperture function
+ mask = theta >= aperture_angle
+
+ aperture = np.ones((size_x, size_y), dtype=float)
+ aperture[mask] = 0.
+
+ v_noise = np.random.rand(size_x, size_y)
+ smoothing = 5
+ phi_r = ndimage.gaussian_filter(v_noise, sigma=(smoothing, smoothing), order=0)
+
+ sigma = 6 # 6 for carbon and thin
+
+ q_r = np.exp(-1j * sigma * phi_r)
+ # q_r = 1-phi_r * sigma
+
+ T_k = aperture * (np.exp(-1j * chi))
+ t_r = np.fft.ifft2(np.fft.fftshift(T_k))
+
+ Psi_k = np.fft.fftshift(np.fft.fft2(q_r * t_r))
+
+ ronchigram = I_k = np.absolute(Psi_k * np.conjugate(Psi_k))
+
+ fov_reciprocal = ab['reciprocal_FOV']
+ if scale == '1/nm':
+ extent = [-fov_reciprocal, fov_reciprocal, -fov_reciprocal, fov_reciprocal]
+ ylabel = 'reciprocal distance [1/nm]'
+ else:
+ fov_mrad = fov_reciprocal * ab['wavelength'] * 1000
+ extent = [-fov_mrad, fov_mrad, -fov_mrad, fov_mrad]
+ ylabel = 'reciprocal distance [mrad]'
+
+ ab['ronchi_extent'] = extent
+ ab['ronchi_label'] = ylabel
+
+ h = np.zeros([chi.shape[0], chi.shape[1], 2, 2])
+ h[:, :, 0, 0] = get_d2chidu2(ab, t_x_v, t_y_v)
+ h[:, :, 0, 1] = get_d2chidudv(ab, t_x_v, t_y_v)
+ h[:, :, 1, 0] = get_d2chidudv(ab, t_x_v, t_y_v)
+ h[:, :, 1, 1] = get_d2chidv2(ab, t_x_v, t_y_v)
+
+ # get Eigenvalues
+ _, s, _ = np.linalg.svd(h)
+
+ # get smallest Eigenvalue per pixel
+ infinite_magnification = np.min(s, axis=2)
+
+ # set all values below a threshold value to one, otherwise 0
+ infinite_magnification[infinite_magnification <= threshold] = 1
+ infinite_magnification[infinite_magnification > threshold] = 0
+
+ return ronchigram, infinite_magnification
+
+
+# ## Aberration Function for Probe calculations
+[docs]def make_chi1(phi, theta, wavelength, ab, c1_include):
+ """
+ # ##
+ # Aberration function chi without defocus
+ # ##
+ """
+ t0 = np.power(theta, 1) / 1 * (float(ab['C01a']) * np.cos(1 * phi) + float(ab['C01b']) * np.sin(1 * phi))
+
+ if c1_include == 1: # First and second terms
+ t1 = np.power(theta, 2) / 2 * (ab['C10'] + ab['C12a'] * np.cos(2 * phi) + ab['C12b'] * np.sin(2 * phi))
+ elif c1_include == 2: # Second terms only
+ t1 = np.power(theta, 2) / 2 * (ab['C12a'] * np.cos(2 * phi) + ab['C12b'] * np.sin(2 * phi))
+ else: # none for zero
+ t1 = t0 * 0.
+
+ t2 = np.power(theta, 3) / 3 * (ab['C21a'] * np.cos(1 * phi) + ab['C21b'] * np.sin(1 * phi)
+ + ab['C23a'] * np.cos(3 * phi) + ab['C23b'] * np.sin(3 * phi))
+
+ t3 = np.power(theta, 4) / 4 * (ab['C30']
+ + ab['C32a'] * np.cos(2 * phi)
+ + ab['C32b'] * np.sin(2 * phi)
+ + ab['C34a'] * np.cos(4 * phi)
+ + ab['C34b'] * np.sin(4 * phi))
+
+ t4 = np.power(theta, 5) / 5 * (ab['C41a'] * np.cos(1 * phi)
+ + ab['C41b'] * np.sin(1 * phi)
+ + ab['C43a'] * np.cos(3 * phi)
+ + ab['C43b'] * np.sin(3 * phi)
+ + ab['C45a'] * np.cos(5 * phi)
+ + ab['C45b'] * np.sin(5 * phi))
+
+ t5 = np.power(theta, 6) / 6 * (ab['C50']
+ + ab['C52a'] * np.cos(2 * phi)
+ + ab['C52b'] * np.sin(2 * phi)
+ + ab['C54a'] * np.cos(4 * phi)
+ + ab['C54b'] * np.sin(4 * phi)
+ + ab['C56a'] * np.cos(6 * phi)
+ + ab['C56b'] * np.sin(6 * phi))
+
+ chi = t0 + t1 + t2 + t3 + t4 + t5
+ if 'C70' in ab:
+ chi += np.power(theta, 8) / 8 * (ab['C70'])
+
+ return chi * 2 * np.pi / wavelength # np.power(theta,6)/6*( ab['C50'] )
+
+
+[docs]def probe2(ab, size_x, size_y, tags, verbose=False):
+ """
+
+ * This function creates an incident STEM probe
+ * at position (0,0)
+ * with parameters given in ab dictionary
+ *
+ * The following Aberration functions are being used:
+ * 1) ddf = Cc*de/E but not + Cc2*(de/E)^2,
+ * Cc, Cc2 = chrom. Aber. (1st, 2nd order) [1]
+ * 2) chi(qx,qy) = (2*pi/lambda)*{0.5*C1*(qx^2+qy^2)+
+ * 0.5*C12a*(qx^2-qy^2)+
+ * C12b*qx*qy+
+ * C21a/3*qx*(qx^2+qy^2)+
+ * ...
+ * +0.5*C3*(qx^2+qy^2)^2
+ * +0.125*C5*(qx^2+qy^2)^3
+ * ... (need to finish)
+ *
+ *
+ * qx = acos(k_x/K), qy = acos(k_y/K)
+ *
+ * References:
+ * [1] J. Zach, M. Haider,
+ * "Correction of spherical and Chromatic Aberration
+ * in a low Voltage SEM", Optik 98 (3), 112-118 (1995)
+ * [2] O.L. Krivanek, N. Delby, A.R. Lupini,
+ * "Towards sub-Angstrom Electron Beams",
+ * Ultramicroscopy 78, 1-11 (1999)
+ *
+
+
+ # Internally reciprocal lattice vectors in 1/nm or rad.
+ # All calculations of chi in angles.
+ # All aberration coefficients in nm
+ """
+
+ if 'fov' not in ab:
+ if 'fov' not in tags:
+ print(' need field of view in tags ')
+ else:
+ ab['fov'] = tags['fov']
+
+ if 'convAngle' not in ab:
+ ab['convAngle'] = 30 # in mrad
+
+ ap_angle = ab['convAngle'] / 1000.0 # in rad
+
+ e0 = ab['EHT'] = float(ab['EHT']) # acceleration voltage in ev
+
+ # defocus = ab['C10']
+
+ if 'C01a' not in ab:
+ ab['C01a'] = 0.
+ if 'C01b' not in ab:
+ ab['C01b'] = 0.
+
+ if 'C50' not in ab:
+ ab['C50'] = 0.
+ if 'C70' not in ab:
+ ab['C70'] = 0.
+
+ if 'Cc' not in ab:
+ ab['Cc'] = 1.3e6 # Cc in nm
+
+ def get_wl():
+ h = 6.626 * 10 ** -34
+ m0 = 9.109 * 10 ** -31
+ ev = 1.602 * 10 ** -19 * e0
+ c = 2.998 * 10 ** 8
+ return h / np.sqrt(2 * m0 * ev * (1 + ev / (2 * m0 * c ** 2))) * 10 ** 9
+
+ wavelength = get_wl()
+ if verbose:
+ print('Acceleration voltage {0:}kV => wavelength {1:.2f}pm'.format(int(e0 / 1000), wavelength * 1000))
+ ab['wavelength'] = wavelength
+
+ # Reciprocal plane in 1/nm
+ dk = 1 / ab['fov']
+ k_x = np.array(dk * (-size_x / 2. + np.arange(size_x)))
+ k_y = np.array(dk * (-size_y / 2. + np.arange(size_y)))
+ t_xv, t_yv = np.meshgrid(k_x, k_y)
+
+ # define reciprocal plane in angles
+ phi = np.arctan2(t_xv, t_yv)
+ theta = np.arctan2(np.sqrt(t_xv ** 2 + t_yv ** 2), 1 / wavelength)
+
+ # calculate chi but omit defocus
+ chi = np.fft.ifftshift(make_chi1(phi, theta, wavelength, ab, 2))
+ probe = np.zeros((size_x, size_y))
+
+ # Aperture function
+ mask = theta >= ap_angle
+
+ # Calculate probe with Cc
+
+ for i in range(len(ab['zeroLoss'])):
+ df = ab['C10'] + ab['Cc'] * ab['zeroEnergy'][i] / e0
+ if verbose:
+ print('defocus due to Cc: {0:.2f} nm with weight {1:.2f}'.format(df, ab['zeroLoss'][i]))
+ # Add defocus
+ chi2 = chi + np.power(theta, 2) / 2 * df
+ # Calculate exponent of - i * chi
+ chi_t = np.fft.ifftshift(np.vectorize(complex)(np.cos(chi2), -np.sin(chi2)))
+ # Apply aperture function
+ chi_t[mask] = 0.
+ # inverse fft of aberration function
+ i2 = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(chi_t)))
+ # add intensities
+ probe = probe + np.real(i2 * np.conjugate(i2)).T * ab['zeroLoss'][i]
+
+ ab0 = {}
+ for key in ab:
+ ab0[key] = 0.
+ # chiIA = np.fft.fftshift(make_chi1(phi, theta, wavelength, ab0, 0)) # np.ones(chi2.shape)*2*np.pi/wavelength
+ chi_i = np.ones((size_y, size_x))
+ chi_i[mask] = 0.
+ i2 = np.fft.fftshift(np.fft.ifft2(np.fft.ifftshift(chi_i)))
+ ideal = np.real(i2 * np.conjugate(i2))
+
+ probe_f = np.fft.fft2(probe, probe.shape) + 1e-12
+ ideal_f = np.fft.fft2(ideal, probe.shape)
+ fourier_space_division = ideal_f / probe_f
+ probe_r = (np.fft.ifft2(fourier_space_division, probe.shape))
+
+ return probe / sum(ab['zeroLoss']), np.real(probe_r)
+
+"""utility functions for sidpy; will move to sidpy"""
+import numpy as np
+import sidpy
+import h5py
+import pyNSID
+import os
+import ipywidgets as widgets
+from IPython.display import display
+import json
+
+
+[docs]class ChooseDataset(object):
+ """Widget to select dataset object """
+
+ def __init__(self, input_object, show_dialog=True):
+ if isinstance(input_object, sidpy.Dataset):
+ if isinstance(input_object.h5_dataset, h5py.Dataset):
+ self.current_channel = input_object.h5_dataset.parent
+ elif isinstance(input_object, h5py.Group):
+ self.current_channel = input_object
+ elif isinstance(input_object, h5py.Dataset):
+ self.current_channel = input_object.parent
+ else:
+ raise ValueError('Need hdf5 group or sidpy Dataset to determine image choices')
+ self.dataset_names = []
+ self.dataset_list = []
+ self.dataset_type = None
+ self.dataset = None
+ self.reader = pyNSID.NSIDReader(self.current_channel.file.filename)
+
+ self.get_dataset_list()
+ self.select_image = widgets.Dropdown(options=self.dataset_names,
+ value=self.dataset_names[0],
+ description='select dataset:',
+ disabled=False,
+ button_style='')
+ if show_dialog:
+ display(self.select_image)
+
+ self.select_image.observe(self.set_dataset, names='value')
+ self.set_dataset(0)
+ self.select_image.index = (len(self.dataset_names) - 1)
+
+[docs] def get_dataset_list(self):
+ """ Get by Log number sorted list of datasets"""
+ datasets = self.reader.read()
+ order = []
+ for dset in datasets:
+ if self.dataset_type is None or dset.data_type == self.data_type:
+ if 'Log' in dset.title:
+ position = dset.title.find('Log_') + 4
+ order.append(int(dset.title[position:position + 3])+1)
+ else:
+ order.append(0)
+ for index in np.argsort(order):
+ dset = datasets[index]
+ self.dataset_names.append('/'.join(dset.title.replace('-', '_').split('/')[-1:]))
+ self.dataset_list.append(dset)
+
+ def set_dataset(self, b):
+ index = self.select_image.index
+ self.dataset = self.dataset_list[index]
+ # Find
+ self.dataset.title = self.dataset.title.split('/')[-1]
+
+
+[docs]def get_dimensions_by_order(dims_in, dataset):
+ """get dimension
+
+ Parameters
+ ----------
+ dims_in: int or list of int
+ the dimensions by numerical order
+ dataset: sidpy.Dataset
+
+ Returns
+ -------
+ dims_out: list of dimensions
+ """
+
+ if isinstance(dims_in, int):
+ dims_in = [dims_in]
+ dims_out = []
+ for item in dims_in:
+ if isinstance(item, int):
+ if item in dataset._axes:
+ dims_out.append([item, dataset._axes[item]])
+ return dims_out
+
+
+[docs]def get_dimensions_by_type(dims_in, dataset):
+ """ get dimension by dimension_type name
+
+ Parameters
+ ----------
+ dims_in: dimension_type or list of dimension_types
+ the dimensions by numerical order
+ dataset: sidpy.Dataset
+
+ Returns
+ -------
+ dims_out: list of dimensions
+ """
+
+ if isinstance(dims_in, (str, sidpy.DimensionType)):
+ dims_in = [dims_in]
+ for i in range(len(dims_in)):
+ if isinstance(dims_in[i], str):
+ dims_in[i] = sidpy.DimensionType[dims_in[i].upper()]
+ dims_out = []
+ for dim, axis in dataset._axes.items():
+ if axis.dimension_type in dims_in:
+ dims_out.append([dim, dataset._axes[dim]])
+ return dims_out
+
+
+[docs]def make_dummy_dataset(value_type):
+ """Make a dummy sidpy.Dataset """
+
+ assert isinstance(value_type, sidpy.DataType)
+ if type == sidpy.DataType.SPECTRUM:
+ dataset = sidpy.Dataset.from_array(np.arange(100))
+ dataset.data_type = 'spectrum'
+ dataset.units = 'counts'
+ dataset.quantity = 'intensity'
+
+ dataset.set_dimension(0, sidpy.Dimension(np.arange(dataset.shape[0]) + 70, name='energy_scale'))
+ dataset.dim_0.dimension_type = 'spectral'
+ dataset.dim_0.units = 'eV'
+ dataset.dim_0.quantity = 'energy loss'
+ else:
+ raise NotImplementedError('not implemented')
+ return dataset
+
+
+
+
+
+[docs]def get_image_dims(dataset):
+ """Get all spatial dimensions"""
+
+ image_dims = []
+ for dim, axis in dataset._axes.items():
+ if axis.dimension_type == sidpy.DimensionType.SPATIAL:
+ image_dims.append(dim)
+ return image_dims
+
+
+[docs]def get_extent(dataset):
+ """get extent to plot with matplotlib"""
+ image_dims = get_image_dims(dataset)
+ return dataset.get_extent(image_dims)
+
+""" dft simulations tools
+
+Part of pyTEMlib
+by Gerd Duscher
+created 10/29/2020
+
+Supports the conversion of DFT data to simulated EELS spectra
+
+- exciting_get_spectra: importing dielectric function from the exciting program
+- final_state_broadening: apply final state broadening to loss-spectra
+"""
+
+import numpy as np
+from lxml import etree
+
+
+[docs]def exciting_get_spectra(file):
+ """get EELS spectra from exciting calculation"""
+
+ tags = {'data': {}}
+
+ tree = etree.ElementTree(file=file)
+ root = tree.getroot()
+
+ data = tags['data']
+
+ if root.tag in ['loss', 'dielectric']:
+ print(' reading ', root.tag, ' function from file ', file)
+ # print(root[0].tag, root[0].text)
+ map_def = root[0]
+ i = 0
+ v = {}
+ for child_of_root in map_def:
+ data[child_of_root.tag] = child_of_root.attrib
+ v[child_of_root.tag] = []
+ i += 1
+
+ for elem in tree.iter(tag='map'):
+ m_dict = elem.attrib
+ for key in m_dict:
+ v[key].append(float(m_dict[key]))
+
+ for key in data:
+ data[key]['data'] = np.array(v[key])
+ data['type'] = root.tag+' function'
+ return tags
+
+
+[docs]def final_state_broadening(x, y, start, instrument):
+ """Final state smearing of ELNES edges
+
+ Parameters
+ ----------
+ x: numpy array
+ x or energy loss axis of density of states
+ y: numpy array
+ y or intensity axis of density of states
+ start: float
+ start energy of edge
+ instrument: float
+ instrument broadening
+
+ Return
+ ------
+ out_data: numpy array
+ smeared intensity according to final state and instrument broadening
+ """
+
+ # Getting the smearing
+ a_i = 107.25*5
+ b_i = 0.04688*2.
+ x = np.array(x)-start
+ zero = int(-x[0]/(x[1]-x[0]))+1
+ smear_i = x*0.0
+ smear_i[zero:-1] = (a_i/x[zero:-1]**2)+b_i*np.sqrt(x[zero:-1])
+ h_bar = 6.58e-16 # h/2pi
+ pre = 1.0
+ m = 6.58e-31
+ smear = x*0.0
+ smear[zero:-1] = pre*(h_bar/(smear_i[zero:-1]*0.000000001))*np.sqrt((2*x[zero:-1]*1.6E-19)/m)
+
+ def lorentzian(xx, pp):
+ yy = ((0.5 * pp[1]/3.14)/((xx-pp[0])**2 + ((pp[1]/2)**2)))
+ return yy/sum(yy)
+
+ p = [0, instrument]
+ in_data = y.copy()
+ out_data = np.array(y)*0.0
+ for i in range(zero+5, len(x)):
+ p[0] = x[i]
+ p[1] = smear[i]/1.0
+ lor = lorentzian(x+1e-9, p)
+ out_data[i] = sum(in_data*lor)
+ if np.isnan(out_data[i]):
+ out_data[i] = 0.0
+
+ p[1] = instrument
+ in_data = out_data.copy()
+ for i in range(zero-5, len(x)):
+ p[0] = x[i]
+ lor = lorentzian(x+1e-9, p)
+ out_data[i] = sum(in_data*lor)
+ # print(out_data[i],in_data[i], lor[i],in_data[i-1], lor[i-1], )
+ return out_data
+
+"""plotting of sidpy Datasets with bokeh for google colab"""
+
+import numpy as np
+import sidpy
+from sidpy.hdf.dtype_utils import is_complex_dtype
+
+import plotly.graph_objects as go
+from plotly.subplots import make_subplots
+from ipywidgets import widgets
+
+
+import pyTEMlib.eels_tools as eels
+import pyTEMlib.file_tools as ft
+
+
+"""from bokeh.layouts import column
+from bokeh.plotting import figure # , show, output_notebook
+from bokeh.models import CustomJS, Slider, Span
+from bokeh.models import LinearColorMapper, ColorBar, ColumnDataSource, BoxSelectTool
+from bokeh.palettes import Spectral11
+"""
+from pyTEMlib.sidpy_tools import *
+import sys
+import matplotlib.pyplot as plt
+# from matplotlib.widgets import Slider, Button
+import matplotlib.patches as patches
+# import matplotlib.animation as animation
+
+if sys.version_info.major == 3:
+ unicode = str
+
+default_cmap = plt.cm.viridis
+
+
+[docs]def plot(dataset, palette='Viridis256'):
+ """plot according to data_type"""
+ if dataset.data_type.name == 'IMAGE_STACK':
+ p = plot_stack(dataset, palette=palette)
+ elif dataset.data_type.name == 'IMAGE':
+ p = plot_image(dataset, palette=palette)
+ elif dataset.data_type.name == 'SPECTRUM':
+ p = plot_spectrum(dataset, palette=palette)
+ else:
+ p = None
+ return p
+
+
+[docs]def plot_stack(dataset, palette="Viridis256"):
+ """Plotting a stack of images
+
+ Plotting a stack of images contained in a sidpy.Dataset.
+ The images can be scrolled through with a slider widget.
+
+ Parameters
+ ----------
+ dataset: sidpy.Dataset
+ sidpy dataset with data_type 'IMAGE_STACK'
+ palette: bokeh palette
+ palette is optional
+
+ Returns
+ -------
+ p: bokeh plot
+
+ Example
+ -------
+ >> import pyTEMlib
+ >> from bokeh.plotting import figure, show, output_notebook
+ >> output_notebook()
+ >> p = pyTEMlib.viz(dataset)
+ >> p.show(p)
+ """
+
+ if not isinstance(dataset, sidpy.Dataset):
+ raise TypeError('Need a sidpy dataset for plotting')
+ if dataset.data_type.name != 'IMAGE_STACK':
+ raise TypeError('Need an IMAGE_STACK for plotting a stack')
+
+ stack = np.array(dataset-dataset.min())
+ stack = stack/stack.max()*256
+ stack = np.array(stack, dtype=int)
+
+ color_mapper = LinearColorMapper(palette=palette, low=0, high=256)
+
+ p = figure(match_aspect=True, plot_width=600, plot_height=600)
+ im_plot = p.image(image=[stack[0]], x=[0], y=[0], dw=[dataset.x[-1]], dh=[dataset.y[-1]], color_mapper=color_mapper)
+ p.x_range.range_padding = 0
+ p.y_range.range_padding = 0
+ p.xaxis.axis_label = 'distance (nm)'
+ p.yaxis.axis_label = 'distance (nm)'
+
+ slider = Slider(start=0, end=stack.shape[0]-1, value=0, step=1, title="frame")
+
+ update_curve = CustomJS(args=dict(source=im_plot, slider=slider, stack=stack),
+ code="""var f = slider.value;
+ source.data_source.data['image'] = [stack[f]];
+ // necessary because we mutated source.data in-place
+ source.data_source.change.emit(); """)
+ slider.js_on_change('value', update_curve)
+
+ return column(slider, p)
+
+
+[docs]def plot_image(dataset, palette="Viridis256"):
+ """Plotting an image
+
+ Plotting an image contained in a sidpy.Dataset.
+
+ Parameters
+ ----------
+ dataset: sidpy.Dataset
+ sidpy dataset with data_type 'IMAGE_STACK'
+ palette: bokeh palette
+ palette is optional
+
+ Returns
+ -------
+ p: bokeh plot
+
+ Example
+ -------
+ >> import pyTEMlib
+ >> from bokeh.plotting import figure, show, output_notebook
+ >> output_notebook()
+ >> p = pyTEMlib.viz(dataset)
+ >> p.show(p)
+
+
+ """
+ if not isinstance(dataset, sidpy.Dataset):
+ raise TypeError('Need a sidpy dataset for plotting')
+
+ if dataset.data_type.name not in ['IMAGE', 'IMAGE_STACK']:
+ raise TypeError('Need an IMAGE or IMAGE_STACK for plotting an image')
+
+ if dataset.data_type.name == 'IMAGE_STACK':
+ image = dataset.sum(axis=0)
+ image = sidpy.Dataset.from_array(image)
+ image.data_type = 'image'
+ image.title = dataset.title
+ image.set_dimension(0, dataset.dim_1)
+ image.set_dimension(1, dataset.dim_2)
+ else:
+ image = dataset
+
+ p = figure(tooltips=[("x", "$x"), ("y", "$y"), ("value", "@image")], match_aspect=True,
+ plot_width=675, plot_height=600, )
+ color_mapper = LinearColorMapper(palette=palette, low=float(image.min()), high=float(image.max()))
+
+ # must give a vector of image data for image parameter
+ p.image(image=[np.array(image)], x=0, y=0, dw=image.x[-1], dh=image.y[-1], color_mapper=color_mapper,
+ level="image")
+ p.x_range.range_padding = 0
+ p.y_range.range_padding = 0
+
+ p.grid.grid_line_width = 0.
+ p.xaxis.axis_label = 'distance (nm)'
+ p.yaxis.axis_label = 'distance (nm)'
+
+ color_bar = ColorBar(color_mapper=color_mapper, major_label_text_font_size="7pt",
+ label_standoff=6, border_line_color=None, location=(0, 0))
+ p.add_layout(color_bar, 'right')
+ return p
+
+
+[docs]def plot_spectrum(dataset, selected_range, palette=None):
+ """Plot spectrum"""
+ if not isinstance(dataset, sidpy.Dataset):
+ raise TypeError('Need a sidpy dataset for plotting')
+
+ if dataset.data_type.name not in ['SPECTRUM']:
+ raise TypeError('Need an sidpy.Dataset of data_type SPECTRUM for plotting a spectrum ')
+
+ p = figure(x_axis_type="linear", plot_width=800, plot_height=400,
+ tooltips=[("index", "$index"), ("(x,y)", "($x, $y)")],
+ tools="pan,wheel_zoom,box_zoom,reset, hover, lasso_select")
+ p.add_tools(BoxSelectTool(dimensions="width"))
+
+ # first line is dataset
+ spectrum = ColumnDataSource(data=dict(x=dataset.dim_0, y=np.array(dataset)))
+ p.scatter('x', 'y', color='blue', size=1, alpha=0., source=spectrum,
+ selection_color="firebrick", selection_alpha=0.)
+ p.line(x='x', y='y', source=spectrum, legend_label=dataset.title, color=palette[0], line_width=2)
+ # add other lines if available
+ if 'add2plot' in dataset.metadata:
+ data = dataset.metadata['add2plot']
+ for key, line in data.items():
+ p.line(dataset.dim_0.values, line['data'], legend_label=line['legend'], color=palette[key], line_width=2)
+ p.legend.click_policy = "hide"
+ p.xaxis.axis_label = dataset.labels[0]
+ p.yaxis.axis_label = dataset.data_descriptor
+ p.title.text = dataset.title
+
+ my_span = Span(location=0, dimension='width', line_color='gray', line_width=1)
+ p.add_layout(my_span)
+
+ callback = CustomJS(args=dict(s1=spectrum), code="""
+ var inds = s1.selected.indices;
+ if (inds.length == 0)
+ return;
+ var kernel = IPython.notebook.kernel;
+ kernel.execute("selected_range = " + [inds[0], inds[inds.length-1]]);""")
+
+ spectrum.selected.js_on_change('indices', callback)
+ return p
+
+
+[docs]class CurveVisualizer(object):
+ """Plots a sidpy.Dataset with spectral dimension
+
+ """
+ def __init__(self, dset, spectrum_number=None, axis=None, leg=None, **kwargs):
+ if not isinstance(dset, sidpy.Dataset):
+ raise TypeError('dset should be a sidpy.Dataset object')
+ if axis is None:
+ self.fig = plt.figure()
+ self.axis = self.fig.add_subplot(1, 1, 1)
+ else:
+ self.axis = axis
+ self.fig = axis.figure
+
+ self.dset = dset
+ self.selection = []
+ [self.spec_dim, self.energy_scale] = get_dimensions_by_type('spectral', self.dset)[0]
+
+ self.lined = dict()
+ self.plot(**kwargs)
+
+ def plot(self, **kwargs):
+ line1, = self.axis.plot(self.energy_scale.values, self.dset, label='spectrum', **kwargs)
+ lines = [line1]
+ if 'add2plot' in self.dset.metadata:
+ data = self.dset.metadata['add2plot']
+ for key, line in data.items():
+ line_add, = self.axis.plot(self.energy_scale.values, line['data'], label=line['legend'])
+ lines.append(line_add)
+
+ legend = self.axis.legend(loc='upper right', fancybox=True, shadow=True)
+ legend.get_frame().set_alpha(0.4)
+
+ for legline, origline in zip(legend.get_lines(), lines):
+ legline.set_picker(True)
+ legline.set_pickradius(5) # 5 pts tolerance
+ self.lined[legline] = origline
+ self.fig.canvas.mpl_connect('pick_event', self.onpick)
+
+ self.axis.axhline(0, color='gray', alpha=0.6)
+ self.axis.set_xlabel(self.dset.labels[0])
+ self.axis.set_ylabel(self.dset.data_descriptor)
+ self.axis.ticklabel_format(style='sci', scilimits=(-2, 3))
+ self.fig.canvas.draw_idle()
+
+ def update(self, **kwargs):
+ x_limit = self.axis.get_xlim()
+ y_limit = self.axis.get_ylim()
+ self.axis.clear()
+ self.plot(**kwargs)
+ self.axis.set_xlim(x_limit)
+ self.axis.set_ylim(y_limit)
+
+ def onpick(self, event):
+ # on the pick event, find the orig line corresponding to the
+ # legend proxy line, and toggle the visibility
+ legline = event.artist
+ origline = self.lined[legline]
+ vis = not origline.get_visible()
+ origline.set_visible(vis)
+ # Change the alpha on the line in the legend so we can see what lines
+ # have been toggled
+ if vis:
+ legline.set_alpha(1.0)
+ else:
+ legline.set_alpha(0.2)
+ self.fig.canvas.draw()
+
+
+[docs]def verify_spectrum_dataset(datasets):
+ if isinstance(datasets, sidpy.Dataset):
+ datasets = {'Channel_000': datasets}
+
+ first_dataset = datasets[list(datasets)[0]]
+ has_complex_dataset = False
+ for dat in datasets.values():
+ if is_complex_dtype(dat.dtype):
+ has_complex_dataset = True
+
+
+ if first_dataset.data_type.name != 'SPECTRUM':
+ raise TypeError('We need a spectrum dataset here')
+ if first_dataset.ndim >1:
+ if first_dataset.shape[1] >1:
+ raise TypeError('Wrong dimensions for spectrum datasset')
+
+ energy_dim = first_dataset.get_spectrum_dims()
+ energy_dim = first_dataset.get_dimension_by_number(energy_dim[0])[0]
+ energy_dim.label = f'{energy_dim.quantity} ({energy_dim.units})'
+
+ default_plot_dictionary = {'title': '',
+ 'theme': "plotly_white",
+ 'y_scale': 1.0,
+ 'y_axis_label': first_dataset.data_descriptor,
+ 'x_axis_label': energy_dim.label,
+ 'show_legend': True,
+ 'height': 500,
+ 'figure_size': None,
+ 'scale_bar': False,
+ 'colorbar': True,
+ 'set_title': True,
+ 'has_complex_dataset': has_complex_dataset}
+
+
+ default_plot_dictionary.update(first_dataset.metadata['plot_parameter'])
+ first_dataset.metadata['plot_parameter'] = default_plot_dictionary
+
+ return datasets
+
+[docs]def spectrum_view_plotly(datasets, figure=None, show=False):
+
+ datasets = verify_spectrum_dataset(datasets)
+ first_dataset = datasets[list(datasets)[0]]
+ plot_dic = first_dataset.metadata['plot_parameter']
+
+ if figure is None:
+ if plot_dic['has_complex_dataset']:
+ fig = make_subplots(rows=1, cols=2, subplot_titles=("Magnitude", "Phase"))
+ else:
+ fig = go.Figure()
+
+ else:
+ fig = figure
+
+ for key, dat in datasets.items():
+ if dat.data_type == first_dataset.data_type:
+ energy_dim = dat.get_spectrum_dims()
+ energy_dim = dat.get_dimension_by_number(energy_dim[0])[0]
+ if is_complex_dtype(dat.dtype):
+ fig.add_trace(go.Scatter(x=energy_dim.values, y=np.abs(dat).squeeze()*plot_dic['y_scale'], name=f'{dat.title}-Magnitude', mode="lines+markers", marker=dict(size=2)), row=1, col=1)
+ fig.add_trace(go.Scatter(x=energy_dim.values, y=np.angle(dat).squeeze()*plot_dic['y_scale'], name=f'{dat.title}-Phase', mode="lines+markers", marker=dict(size=2)), row=1, col=2)
+ else:
+ fig.add_trace(go.Scatter(x=energy_dim.values, y=np.array(dat).squeeze()*plot_dic['y_scale'], name=dat.title, mode="lines+markers", marker=dict(size=2)))
+
+
+ fig.update_layout(
+ selectdirection='h',
+ showlegend = plot_dic['show_legend'],
+ dragmode='select',
+ title_text=plot_dic['title'],
+ yaxis_title_text=plot_dic['y_axis_label'],
+ xaxis_title_text=plot_dic['x_axis_label'],
+ height=plot_dic['height'],
+ template=plot_dic['theme']
+ )
+ fig.update_layout(hovermode='x unified')
+
+ if plot_dic['has_complex_dataset']:
+ fig.update_yaxes(title_text='angle (rad)', row = 1, col = 2)
+ fig.update_xaxes(title_text=plot_dic['x_axis_label'], row = 1, col = 2)
+
+ config = {'displayModeBar': True}
+ if show:
+ fig.show(config=config)
+ return fig
+
+
+[docs]class SpectrumView(object):
+ def __init__(self, datasets, figure=None, **kwargs):
+ first_dataset = datasets[list(datasets)[0]]
+ if first_dataset.data_type.name != 'SPECTRUM':
+ raise TypeError('We need a spectrum dataset here')
+ if first_dataset.ndim >1:
+ if first_dataset.shape[1] >1:
+ raise TypeError('Wrong dimensions for spectrum datasset')
+
+ energy_dim = first_dataset.get_spectrum_dims()
+ energy_dim = first_dataset.get_dimension_by_number(energy_dim[0])[0]
+
+ if 'plot_parameter' not in first_dataset.metadata:
+ first_dataset.metadata['plot_parameter'] = {}
+ plot_dic = first_dataset.metadata['plot_parameter']
+ energy_dim.label = f'{energy_dim.quantity} ({energy_dim.units})'
+
+ plot_dic['title'] = kwargs.pop('title', '')
+ plot_dic['theme'] = kwargs.pop('theme', "plotly_white")
+ plot_dic['y_scale'] = kwargs.pop('y_scale', 1.0)
+ plot_dic['y_axis_label'] = kwargs.pop('y_axis_label', first_dataset.data_descriptor)
+ plot_dic['x_axis_label'] = kwargs.pop('x_axis_label', energy_dim.label)
+ plot_dic['height'] = kwargs.pop('height', 500)
+
+
+ if 'incident_beam_current_counts' in first_dataset.metadata['experiment']:
+ plot_dic['y_scale'] = 1e6/first_dataset.metadata['experiment']['incident_beam_current_counts']
+ plot_dic['y_axis_label'] = ' probability (ppm)'
+ # plot_dic['y_scale'] = 1e6/first_dataset.sum()
+
+ def selection_fn(trace,points,selector):
+ self.energy_selection = [points.point_inds[0], points.point_inds[-1]]
+
+ self.fig = spectrum_view_plotly(datasets)
+
+ self.spectrum_widget = go.FigureWidget(self.fig)
+
+ self.spectrum_widget.data[0].on_selection(selection_fn)
+ self.spectrum_widget.data[0].on_click(self.identify_edges)
+
+ self.edge_annotation = 0
+ self.edge_line = 0
+ self.regions = {}
+ self.initialize_edge()
+
+ self.plot = display(self.spectrum_widget)
+
+[docs] def initialize_edge(self):
+ """ Intitalizes edge cursor
+ Should be run first so that edge cursor is first
+ """
+ self.edge_annotation = len(self.spectrum_widget.layout.annotations)
+ self.edge_line = len(self.spectrum_widget.layout.shapes)
+ self.spectrum_widget.add_vline(x=200, line_dash="dot", line_color='blue',
+ annotation_text= " ",
+ annotation_position="top right",
+ visible = False)
+
+ def identify_edges(self, trace, points, selector):
+ energy = points.xs[0]
+ edge_names = find_edge_names(points.xs[0])
+ self.spectrum_widget.layout['annotations'][self.edge_annotation].x=energy
+
+ self.spectrum_widget.layout['annotations'][self.edge_annotation].text = f"{edge_names}"
+ self.spectrum_widget.layout['annotations'][self.edge_annotation].visible = True
+ self.spectrum_widget.layout['shapes'][self.edge_line].x0 = energy
+ self.spectrum_widget.layout['shapes'][self.edge_line].x1 = energy
+ self.spectrum_widget.layout['shapes'][self.edge_line].visible = True
+ self.spectrum_widget.layout.update()
+
+ def add_region(self, text, start, end, color='blue'):
+ if text not in self.regions:
+ self.regions[text] = {'annotation': len(self.spectrum_widget.layout.annotations),
+ 'shape': len(self.spectrum_widget.layout.shapes),
+ 'start': start,
+ 'end': end,
+ 'color': color}
+ self.spectrum_widget.add_vrect(x0=start, x1=end,
+ annotation_text=text, annotation_position="top left",
+ fillcolor=color, opacity=0.15, line_width=0)
+ self.spectrum_widget.layout.update()
+ else:
+ self.update_region(text, start, end)
+
+
+ def update_region(self, text, start, end):
+ if text in self.regions:
+ region = self.regions[text]
+ self.spectrum_widget.layout.annotations[region['annotation']].x =start
+ self.spectrum_widget.layout['shapes'][region['shape']].x0 = start
+ self.spectrum_widget.layout['shapes'][region['shape']].x1 = end
+ self.spectrum_widget.layout.update()
+
+ def regions_visibility(self, visibility=True):
+
+ for region in self.regions.values():
+ self.spectrum_widget.layout.annotations[region['annotation']].visible = visibility
+ self.spectrum_widget.layout.shapes[region['shape']].visible = visibility
+
+
+[docs]def find_edge_names(energy_value):
+
+ selected_edges = []
+ for shift in [1,2,5,10,20]:
+ selected_edge = ''
+ edges = eels.find_major_edges(energy_value, shift)
+ edges = edges.split('\n')
+ for edge in edges[1:]:
+ edge = edge[:-3].split(':')
+ name = edge[0].strip()
+ energy = float(edge[1].strip())
+ selected_edge = name
+
+ if selected_edge != '':
+ selected_edges.append(selected_edge)
+ if len(selected_edges)>0:
+ return selected_edges
+
' + + '' + + _("Hide Search Matches") + + "
" + ) + ); + }, + + /** + * helper function to hide the search marks again + */ + hideSearchWords: () => { + document + .querySelectorAll("#searchbox .highlight-link") + .forEach((el) => el.remove()); + document + .querySelectorAll("span.highlighted") + .forEach((el) => el.classList.remove("highlighted")); + localStorage.removeItem("sphinx_highlight_terms") + }, + + initEscapeListener: () => { + // only install a listener if it is really needed + if (!DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS) return; + + document.addEventListener("keydown", (event) => { + // bail for input elements + if (BLACKLISTED_KEY_CONTROL_ELEMENTS.has(document.activeElement.tagName)) return; + // bail with special keys + if (event.shiftKey || event.altKey || event.ctrlKey || event.metaKey) return; + if (DOCUMENTATION_OPTIONS.ENABLE_SEARCH_SHORTCUTS && (event.key === "Escape")) { + SphinxHighlight.hideSearchWords(); + event.preventDefault(); + } + }); + }, +}; + +_ready(SphinxHighlight.highlightSearchWords); +_ready(SphinxHighlight.initEscapeListener); diff --git a/about.html b/about.html new file mode 100644 index 00000000..4186ee8f --- /dev/null +++ b/about.html @@ -0,0 +1,207 @@ + + + + + + +Python framework for model based analysis of TEM/STEM data
+The pyTEMlib:
+is a pycroscopy package based on python
enables quantitative analysis through model based approach
provides routines for the analysis of diffraction, image and spectroscopic datasets
handles one, two, three, and four dimensional datasets
works in jupyter notebooks and in python programs.
provides dialog windows for metadata and analysis input in jupyter notebooks and in python programs.
pyNSID is a python package that currently provides three areas of analysis:
+Diffraction: Single and poly crystalline diffraction data and analysis in parallel and convergent illumination
Imaging: Image analysis, atom detection and image stack registration.
EELS: It provides a framework for quantification of EELS spectra and spectrum images.
Just as scipy uses numpy underneath, scientific packages like pyTEMlib use sidpy format for dataset representation
and pyNSID for all file-handling.
Dialogs are based on pyQt5 and some features require Ipython widgets
The packages sidpy and pyNSID use popular packages such as numpy, h5py, dask, matplotlib, etc. for most of
the storage, computation, and visualization.
Note
+We are running weekly hackathons for pycroscopy development every Friday from 3-5 PM - USA Eastern time. +The requirements for participation are: knowledge of python, numpy, h5py, git. +Please email vasudevanrk at ornl.gov to be added to the hackathons
+pyTEMlib originates in the need for teaching and the development of new techniques for TEM/STEM data analysis. +Please, see my lecture note(-books) for information on the background of analysis.
+Cannot use desktop computers for analysis
Need: High performance computing, storage resources and compatible, scalable file structures
Sophisticated imaging and spectroscopy modes resulting in 5,6,7… dimensional data
Need: Robust software and generalized data formatting
Different formats from each instrument. Proprietary in most cases
Incompatible for correlation
Need: Open, instrument-independent data format
Software supplied with instruments often insufficient / incapable of custom analysis routines
Commercial software (Eg: Matlab, Origin..) are often prohibitively expensive.
Need: Free, powerful, open source, user-friendly software
Analysis software and data not shared
No guarantees of reproducibility or traceability
Need: open source data structures, file formats, centralized code and data repositories
We envision pyTEMlib to be a convenient package that facilitates all scientists to analyse data and develop new methods of anlysis, without being burdened with basic code functionality.
This project is being led by staff members at Oak Ridge National Laboratory (ORNL), and professors at University of Tennessee, Knoxville
We invite anyone interested to join our team to build better, free software for the scientific community
Please visit our credits and acknowledgements page for more information.
If you are interested in integrating our in your existing package, please get in touch with us.
Here are a few options for you to get in touch with the developers and the user community +to ask questions, report bugs, request features etc:
+If you have a GitHub account (free), please raise an issue.
If you have a GMail account (free), please start a new thread in our google group
When reporting a bug / asking certain questions, we will be able to respond faster if you can provide:
+a (simplified) script / snippet that reproduces the error(s) you are facing
the full description of the error(s)
details regarding your operating system (Mac / Windows / Linux)
Software versions - python, pyNSID and core dependency packages (numpy, h5py, dask, matplotlib, etc.)
+ | + |
+ | + |
+ |
+ | + |
+ | + |
+ |
+ | + |
+ |
+ | + |
+ |
+ | + |
Model Based Analysis of TEM/STEM Data
+Last Update 03/25/2022
+Jump to our GitHub project page
++ | Created on Sat Jan 19 10:07:35 2019 |
+
pyTEMlib requires many commonly used scientific and numeric python packages such as numpy, h5py etc. +To simplify the installation process, we recommend the installation of +Anaconda which contains most of the prerequisite packages, +conda - a package / environment manager, +as well as an interactive development environment - Spyder.
+Do you already have Anaconda installed?
+No?
+Download and install Anaconda for Python 3.6
Yes?
+Is your Anaconda based on python 3.5+?
+No?
+Uninstall existing Python / Anaconda distribution(s).
Restart computer
Yes?
+Proceed to install pyTEMlib
pyTEMlib is compatible with python 3.5 onwards. Please raise an issue if you find a bug.
We do not support 32 bit architectures
We only support text that is UTF-8 compliant due to restrictions posed by HDF5
Installing, uninstalling, or updating pyTEMlib (or any other python package for that matter) can be performed using the Terminal
application.
+You will need to open the Terminal to type any command shown on this page.
+Here is how you can access the Terminal on your computer:
Windows - Open Command Prompt
by clicking on the Start button on the bottom left and typing cmd
in the search box.
+You can either click on the Command Prompt
that appears in the search result or just hit the Enter button on your keyboard.
Note - be sure to install in a location where you have write access. Do not install as administrator unless you are required to do so.
MacOS - Click on the Launchpad
. You will be presented a screen with a list of all your applications with a search box at the top.
+Alternatively, simultaneously hold down the Command
and Space
keys on the keyboard to launch the Spotlight search
.
+Type terminal
in the search box and click on the Terminal
application.
Linux (e.g - Ubuntu) - Open the Dash by clicking the Ubuntu (or equivalent) icon in the upper-left, type “terminal”. +Select the Terminal application from the results that appear.
Ensure that a compatible Anaconda distribution has been successfully installed
Open a terminal window.
You can now install pyTEMlib via pip
as shown below.
+Type the following command into the terminal / command prompt and hit the Return / Enter key:
pip:
+pip install pyTEMlib
+
Note that we do not recommend installing pyTEMlib this way since branches other than the master branch may contain bugs.
+Note
+Windows users will need to install git
before proceeding. Please type the following command in the Command Prompt:
conda install git
+
Install a specific branch of pyTEMlib (dev
in this case):
pip install -U git+https://github.com/pycroscopy/pyTEMlib@dev
+
We recommend periodically updating your conda / anaconda distribution. Please see these instructions to update anaconda.
+If you already have pyTEMlib installed and want to update to the latest version, use the following command in a terminal window:
+pip install -U --no-deps pyTEMlib
+
If it does not work try reinstalling the package:
+pip uninstall pyTEMlib
+pip install pyTEMlib
+
We recommend HDF View for exploring HDF5 files generated by and used in pyTEMlib.
+part of
+pyTEMlib, a pycroscopy library
+by
+Gerd Duscher
+Materials Science & Engineering +Joint Institute of Advanced Materials +The University of Tennessee, Knoxville
+Usage of the EELS Tools of pyTEMlib
+ +part of
+pyTEMlib, a pycroscopy library
+by
+Gerd Duscher
+Materials Science & Engineering +Joint Institute of Advanced Materials +The University of Tennessee, Knoxville
+Usage of the Image Tools of pyTEMlib
+ +Starting with revision from version 0.2021.2.22
+Moved read_poscar function from kinematic_scattering to file_tools.py and added read_cif function.
Revised kinematic scattering library and renamed to kinematic_scattering.py. This function handles tilted samples
better. +- added image_dialog to interactive_image with interactive histogram to change contrast in images +- added multislice tools in dynamic_scattering.py (supercell potential is rather basic)
+