pyNanoMatBuilder 0.10.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyNanoMatBuilder/.ipynb_checkpoints/TEM_creator-checkpoint.py +1361 -0
- pyNanoMatBuilder/.ipynb_checkpoints/__init__-checkpoint.py +32 -0
- pyNanoMatBuilder/.ipynb_checkpoints/archimedeanNPs-checkpoint.py +1301 -0
- pyNanoMatBuilder/.ipynb_checkpoints/catalanNPs-checkpoint.py +776 -0
- pyNanoMatBuilder/.ipynb_checkpoints/crystalNPs-checkpoint.py +1263 -0
- pyNanoMatBuilder/.ipynb_checkpoints/data-checkpoint.py +204 -0
- pyNanoMatBuilder/.ipynb_checkpoints/johnsonNPs-checkpoint.py +876 -0
- pyNanoMatBuilder/.ipynb_checkpoints/make_files_remastered-checkpoint.py +2204 -0
- pyNanoMatBuilder/.ipynb_checkpoints/otherNPs-checkpoint.py +258 -0
- pyNanoMatBuilder/.ipynb_checkpoints/platonicNPs-checkpoint.py +2250 -0
- pyNanoMatBuilder/.ipynb_checkpoints/pyNMBcore-checkpoint.py +93 -0
- pyNanoMatBuilder/.ipynb_checkpoints/utils-checkpoint.py +3573 -0
- pyNanoMatBuilder/.ipynb_checkpoints/visualID-checkpoint.py +196 -0
- pyNanoMatBuilder/README +1 -0
- pyNanoMatBuilder/__init__.py +32 -0
- pyNanoMatBuilder/archimedeanNPs.py +1225 -0
- pyNanoMatBuilder/catalanNPs.py +733 -0
- pyNanoMatBuilder/crystalNPs.py +1216 -0
- pyNanoMatBuilder/data.py +205 -0
- pyNanoMatBuilder/johnsonNPs.py +835 -0
- pyNanoMatBuilder/otherNPs.py +257 -0
- pyNanoMatBuilder/platonicNPs.py +2255 -0
- pyNanoMatBuilder/pyNMBcore.py +98 -0
- pyNanoMatBuilder/resources/.ipynb_checkpoints/tools4pyPC-checkpoint.py +44 -0
- pyNanoMatBuilder/resources/.ipynb_checkpoints/visualID-checkpoint.py +101 -0
- pyNanoMatBuilder/resources/.ipynb_checkpoints/visualID_Eng-checkpoint.py +140 -0
- pyNanoMatBuilder/resources/__init__.py +0 -0
- pyNanoMatBuilder/resources/cif_database/.ipynb_checkpoints/cod1000041-NaCl-checkpoint.cif +259 -0
- pyNanoMatBuilder/resources/cif_database/CsPbBr3_cubic_231023.cif +112 -0
- pyNanoMatBuilder/resources/cif_database/CsPbBr3_ortho_14608.cif +93 -0
- pyNanoMatBuilder/resources/cif_database/__init__.py +0 -0
- pyNanoMatBuilder/resources/cif_database/amorphousC/__init__.py +0 -0
- pyNanoMatBuilder/resources/cif_database/amorphousC/aC_relax_10x10.xyz.gz +0 -0
- pyNanoMatBuilder/resources/cif_database/amorphousC/aC_relax_5x5.xyz.gz +0 -0
- pyNanoMatBuilder/resources/cif_database/cod1000041-NaCl.cif +259 -0
- pyNanoMatBuilder/resources/cif_database/cod1539039-Fe_beta.cif +79 -0
- pyNanoMatBuilder/resources/cif_database/cod1539039-Mn_beta.cif +79 -0
- pyNanoMatBuilder/resources/cif_database/cod5000217-Fe_bcc.cif +168 -0
- pyNanoMatBuilder/resources/cif_database/cod9008459-Ag_fcc.cif +252 -0
- pyNanoMatBuilder/resources/cif_database/cod9008463-Au_fcc.cif +259 -0
- pyNanoMatBuilder/resources/cif_database/cod9008466-Co_fcc.cif +254 -0
- pyNanoMatBuilder/resources/cif_database/cod9008492-Co_hcp.cif +86 -0
- pyNanoMatBuilder/resources/cif_database/cod9008513-Ru_hcp.cif +84 -0
- pyNanoMatBuilder/resources/cif_database/cod9011068-Mn_alpha.cif +118 -0
- pyNanoMatBuilder/resources/cif_database/cod9012884-Co_epsilon.cif +82 -0
- pyNanoMatBuilder/resources/cif_database/cod9012957-Pt_fcc.cif +263 -0
- pyNanoMatBuilder/resources/cif_database/cod9015662-TiO2-rutile.cif +65 -0
- pyNanoMatBuilder/resources/cif_database/cod9015929-TiO2-anatase.cif +90 -0
- pyNanoMatBuilder/resources/css/.directory +4 -0
- pyNanoMatBuilder/resources/css/BrainHalfHalf-120x139.base64 +1 -0
- pyNanoMatBuilder/resources/css/BrainHalfHalf-120x139.png +0 -0
- pyNanoMatBuilder/resources/css/BrainHalfHalf.base64 +8231 -0
- pyNanoMatBuilder/resources/css/BrainHalfHalf.png +0 -0
- pyNanoMatBuilder/resources/css/BrainHalfHalf.svg +289 -0
- pyNanoMatBuilder/resources/css/visualID.css +274 -0
- pyNanoMatBuilder/resources/figs/.directory +6 -0
- pyNanoMatBuilder/resources/figs/.ipynb_checkpoints/bccrdd-C-checkpoint.png +0 -0
- pyNanoMatBuilder/resources/figs/InoD-C.png +0 -0
- pyNanoMatBuilder/resources/figs/InoD.base64 +1 -0
- pyNanoMatBuilder/resources/figs/InoD.png +0 -0
- pyNanoMatBuilder/resources/figs/InoD.xyz +310 -0
- pyNanoMatBuilder/resources/figs/MarksD-C.png +0 -0
- pyNanoMatBuilder/resources/figs/MarksD.base64 +1 -0
- pyNanoMatBuilder/resources/figs/MarksD.png +0 -0
- pyNanoMatBuilder/resources/figs/MarksD.xyz +51 -0
- pyNanoMatBuilder/resources/figs/OhWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/OhWS.png +0 -0
- pyNanoMatBuilder/resources/figs/OhWS.script +1 -0
- pyNanoMatBuilder/resources/figs/OhWS.xyz +87 -0
- pyNanoMatBuilder/resources/figs/WS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/WS.png +0 -0
- pyNanoMatBuilder/resources/figs/WS.script +1 -0
- pyNanoMatBuilder/resources/figs/WS.xyz +607 -0
- pyNanoMatBuilder/resources/figs/__init__.py +0 -0
- pyNanoMatBuilder/resources/figs/at.xyz +3 -0
- pyNanoMatBuilder/resources/figs/bccrDDWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/bccrDDWS.png +0 -0
- pyNanoMatBuilder/resources/figs/bccrDDWS.script +1 -0
- pyNanoMatBuilder/resources/figs/bccrDDWS.xyz +67 -0
- pyNanoMatBuilder/resources/figs/bccrdd-C.png +0 -0
- pyNanoMatBuilder/resources/figs/bccrdd.base64 +1 -0
- pyNanoMatBuilder/resources/figs/bccrdd.png +0 -0
- pyNanoMatBuilder/resources/figs/bccrdd.xyz +16 -0
- pyNanoMatBuilder/resources/figs/bccrddWS.png +0 -0
- pyNanoMatBuilder/resources/figs/bccrddWS.script +1 -0
- pyNanoMatBuilder/resources/figs/bccrddWS.xyz +67 -0
- pyNanoMatBuilder/resources/figs/cube-C.png +0 -0
- pyNanoMatBuilder/resources/figs/cube.base64 +1 -0
- pyNanoMatBuilder/resources/figs/cube.png +0 -0
- pyNanoMatBuilder/resources/figs/cube.xyz +11 -0
- pyNanoMatBuilder/resources/figs/cubeWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/cubeWS.png +0 -0
- pyNanoMatBuilder/resources/figs/cubeWS.script +1 -0
- pyNanoMatBuilder/resources/figs/cubeWS.xyz +367 -0
- pyNanoMatBuilder/resources/figs/cubo-C.png +0 -0
- pyNanoMatBuilder/resources/figs/cubo.base64 +1 -0
- pyNanoMatBuilder/resources/figs/cubo.png +0 -0
- pyNanoMatBuilder/resources/figs/cubo.xyz +14 -0
- pyNanoMatBuilder/resources/figs/cuboWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/cuboWS.png +0 -0
- pyNanoMatBuilder/resources/figs/cuboWS.script +1 -0
- pyNanoMatBuilder/resources/figs/cuboWS.xyz +311 -0
- pyNanoMatBuilder/resources/figs/dicoTdWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/dicoTdWS.png +0 -0
- pyNanoMatBuilder/resources/figs/dicoTdWS.script +1 -0
- pyNanoMatBuilder/resources/figs/dicoTdWS.xyz +4749 -0
- pyNanoMatBuilder/resources/figs/ellipsoid-C.png +0 -0
- pyNanoMatBuilder/resources/figs/ellipsoid.base64 +1 -0
- pyNanoMatBuilder/resources/figs/ellipsoid.png +0 -0
- pyNanoMatBuilder/resources/figs/fccOh-C.png +0 -0
- pyNanoMatBuilder/resources/figs/fccOh.base64 +1 -0
- pyNanoMatBuilder/resources/figs/fccOh.png +0 -0
- pyNanoMatBuilder/resources/figs/fccOh.xyz +8 -0
- pyNanoMatBuilder/resources/figs/fccTd-C.png +0 -0
- pyNanoMatBuilder/resources/figs/fccTd.base64 +1 -0
- pyNanoMatBuilder/resources/figs/fccTd.png +0 -0
- pyNanoMatBuilder/resources/figs/fccTd.xyz +6 -0
- pyNanoMatBuilder/resources/figs/fccdrdd.png +0 -0
- pyNanoMatBuilder/resources/figs/fccdrdd.xyz +16 -0
- pyNanoMatBuilder/resources/figs/fccrdd-C.png +0 -0
- pyNanoMatBuilder/resources/figs/fccrdd.base64 +1 -0
- pyNanoMatBuilder/resources/figs/fccrdd.png +0 -0
- pyNanoMatBuilder/resources/figs/fccrdd.xyz +16 -0
- pyNanoMatBuilder/resources/figs/hcpsph1WS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/hcpsph1WS.png +0 -0
- pyNanoMatBuilder/resources/figs/hcpsph1WS.script +1 -0
- pyNanoMatBuilder/resources/figs/hcpsph1WS.xyz +410 -0
- pyNanoMatBuilder/resources/figs/hcpsph2WS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/hcpsph2WS.png +0 -0
- pyNanoMatBuilder/resources/figs/hcpsph2WS.script +1 -0
- pyNanoMatBuilder/resources/figs/hcpsph2WS.xyz +344 -0
- pyNanoMatBuilder/resources/figs/ico-C.png +0 -0
- pyNanoMatBuilder/resources/figs/ico.base64 +1 -0
- pyNanoMatBuilder/resources/figs/ico.png +0 -0
- pyNanoMatBuilder/resources/figs/ico.xyz +14 -0
- pyNanoMatBuilder/resources/figs/pbpy-C.png +0 -0
- pyNanoMatBuilder/resources/figs/pbpy.base64 +1 -0
- pyNanoMatBuilder/resources/figs/pbpy.png +0 -0
- pyNanoMatBuilder/resources/figs/pbpy.xyz +9 -0
- pyNanoMatBuilder/resources/figs/pnmbAvailableStructures.png +0 -0
- pyNanoMatBuilder/resources/figs/pnmbAvailableStructures.svg +1037 -0
- pyNanoMatBuilder/resources/figs/rDD-C.png +0 -0
- pyNanoMatBuilder/resources/figs/rDD.base64 +1 -0
- pyNanoMatBuilder/resources/figs/rDD.png +0 -0
- pyNanoMatBuilder/resources/figs/rDD.xyz +22 -0
- pyNanoMatBuilder/resources/figs/rhcuboWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/rhcuboWS.png +0 -0
- pyNanoMatBuilder/resources/figs/rhcuboWS.script +1 -0
- pyNanoMatBuilder/resources/figs/rhcuboWS.xyz +333 -0
- pyNanoMatBuilder/resources/figs/script-facettes-345PtLight.spt +77 -0
- pyNanoMatBuilder/resources/figs/sphere-C.png +0 -0
- pyNanoMatBuilder/resources/figs/sphere.base64 +1 -0
- pyNanoMatBuilder/resources/figs/sphere.png +0 -0
- pyNanoMatBuilder/resources/figs/tbp-C.png +0 -0
- pyNanoMatBuilder/resources/figs/tbp.base64 +1 -0
- pyNanoMatBuilder/resources/figs/tbp.png +0 -0
- pyNanoMatBuilder/resources/figs/tbp.xyz +22 -0
- pyNanoMatBuilder/resources/figs/tpt-C.png +0 -0
- pyNanoMatBuilder/resources/figs/tpt.base64 +1 -0
- pyNanoMatBuilder/resources/figs/tpt.png +0 -0
- pyNanoMatBuilder/resources/figs/tpt.xyz +150 -0
- pyNanoMatBuilder/resources/figs/trOh-C.png +0 -0
- pyNanoMatBuilder/resources/figs/trOh.base64 +1 -0
- pyNanoMatBuilder/resources/figs/trOh.png +0 -0
- pyNanoMatBuilder/resources/figs/trOh.xyz +40 -0
- pyNanoMatBuilder/resources/figs/trOhWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/trOhWS.png +0 -0
- pyNanoMatBuilder/resources/figs/trOhWS.script +1 -0
- pyNanoMatBuilder/resources/figs/trOhWS.xyz +57 -0
- pyNanoMatBuilder/resources/figs/trTd-C.png +0 -0
- pyNanoMatBuilder/resources/figs/trTd.base64 +1 -0
- pyNanoMatBuilder/resources/figs/trTd.png +0 -0
- pyNanoMatBuilder/resources/figs/trTd.xyz +14 -0
- pyNanoMatBuilder/resources/figs/trbccrDDWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/trbccrDDWS.png +0 -0
- pyNanoMatBuilder/resources/figs/trbccrDDWS.script +1 -0
- pyNanoMatBuilder/resources/figs/trbccrDDWS.xyz +371 -0
- pyNanoMatBuilder/resources/figs/trbccrddWS.png +0 -0
- pyNanoMatBuilder/resources/figs/trbccrddWS.script +1 -0
- pyNanoMatBuilder/resources/figs/trbccrddWS.xyz +371 -0
- pyNanoMatBuilder/resources/figs/trcubeWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/trcubeWS.png +0 -0
- pyNanoMatBuilder/resources/figs/trcubeWS.script +1 -0
- pyNanoMatBuilder/resources/figs/trcubeWS.xyz +359 -0
- pyNanoMatBuilder/resources/figs/ttrbccrDDWS-C.png +0 -0
- pyNanoMatBuilder/resources/figs/ttrbccrDDWS.png +0 -0
- pyNanoMatBuilder/resources/figs/ttrbccrDDWS.script +1 -0
- pyNanoMatBuilder/resources/figs/ttrbccrDDWS.xyz +61 -0
- pyNanoMatBuilder/resources/figs/ttrbccrddWS.png +0 -0
- pyNanoMatBuilder/resources/figs/ttrbccrddWS.script +1 -0
- pyNanoMatBuilder/resources/figs/ttrbccrddWS.xyz +61 -0
- pyNanoMatBuilder/resources/figs/underConstruction-C.png +0 -0
- pyNanoMatBuilder/resources/figs/underConstruction.png +0 -0
- pyNanoMatBuilder/resources/figs/underConstruction.svg +259 -0
- pyNanoMatBuilder/resources/svg/Logo-Universite-Toulouse-n-2023.png +0 -0
- pyNanoMatBuilder/resources/svg/Logo-institutionnel-couleur-Uonly.svg +64 -0
- pyNanoMatBuilder/resources/svg/Python_logo_and_wordmark.svg.png +0 -0
- pyNanoMatBuilder/resources/svg/__init__.py +0 -0
- pyNanoMatBuilder/resources/svg/bccrdd.spt +288 -0
- pyNanoMatBuilder/resources/svg/bccrdd.xyz +17 -0
- pyNanoMatBuilder/resources/svg/logo-C.png +0 -0
- pyNanoMatBuilder/resources/svg/logo.png +0 -0
- pyNanoMatBuilder/resources/svg/logo2-C.png +0 -0
- pyNanoMatBuilder/resources/svg/logo2.png +0 -0
- pyNanoMatBuilder/resources/svg/logo3-C.png +0 -0
- pyNanoMatBuilder/resources/svg/logo3.png +0 -0
- pyNanoMatBuilder/resources/svg/logoEnd.svg +94 -0
- pyNanoMatBuilder/resources/svg/logo_lpcno_300_dpi_notexttransparent.png +0 -0
- pyNanoMatBuilder/resources/svg/pyNanoMatBuilder_banner.png +0 -0
- pyNanoMatBuilder/resources/svg/pyNanoMatBuilder_banner.svg +162 -0
- pyNanoMatBuilder/resources/svg/pyNanoMatBuilder_logo.png +0 -0
- pyNanoMatBuilder/resources/svg/pyNanoMatBuilder_logo.svg +123 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/__init__-checkpoint.py +12 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/core-checkpoint.py +750 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/crystals-checkpoint.py +375 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/energy-checkpoint.py +215 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/external_pgm-checkpoint.py +156 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/geometry-checkpoint.py +1340 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/io-checkpoint.py +587 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/polydispersity-checkpoint.py +837 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/prop-checkpoint.py +864 -0
- pyNanoMatBuilder/utils/.ipynb_checkpoints/symmetry-checkpoint.py +135 -0
- pyNanoMatBuilder/utils/__init__.py +12 -0
- pyNanoMatBuilder/utils/core.py +751 -0
- pyNanoMatBuilder/utils/crystals.py +375 -0
- pyNanoMatBuilder/utils/energy.py +215 -0
- pyNanoMatBuilder/utils/external_pgm.py +156 -0
- pyNanoMatBuilder/utils/geometry.py +1340 -0
- pyNanoMatBuilder/utils/io.py +587 -0
- pyNanoMatBuilder/utils/polydispersity.py +837 -0
- pyNanoMatBuilder/utils/prop.py +864 -0
- pyNanoMatBuilder/utils/symmetry.py +135 -0
- pyNanoMatBuilder/utils/utils.py.org +3573 -0
- pyNanoMatBuilder/utils/utils.py.tmp +75 -0
- pyNanoMatBuilder/visualID.py +123 -0
- pynanomatbuilder-0.10.3.dist-info/METADATA +118 -0
- pynanomatbuilder-0.10.3.dist-info/RECORD +240 -0
- pynanomatbuilder-0.10.3.dist-info/WHEEL +5 -0
- pynanomatbuilder-0.10.3.dist-info/licenses/LICENSE +674 -0
- pynanomatbuilder-0.10.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1361 @@
|
|
|
1
|
+
# External dependencies
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import os
|
|
5
|
+
import gzip
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import importlib
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pandas as pd
|
|
12
|
+
|
|
13
|
+
import abtem
|
|
14
|
+
|
|
15
|
+
from ase import io
|
|
16
|
+
from ase.atoms import Atoms
|
|
17
|
+
from ase.geometry import cellpar_to_cell
|
|
18
|
+
from ase.spacegroup import get_spacegroup
|
|
19
|
+
from ase.io import read
|
|
20
|
+
from ase.io import write
|
|
21
|
+
from ase.visualize import view
|
|
22
|
+
|
|
23
|
+
import matplotlib.pyplot as plt
|
|
24
|
+
from sklearn.decomposition import PCA
|
|
25
|
+
import math
|
|
26
|
+
|
|
27
|
+
# Internal Relative Imports
|
|
28
|
+
from .visualID import fg, hl, bg
|
|
29
|
+
from . import visualID as vID
|
|
30
|
+
|
|
31
|
+
from . import crystalNPs as cyNP
|
|
32
|
+
from . import platonicNPs as pNP
|
|
33
|
+
from . import archimedeanNPs as aNP
|
|
34
|
+
from . import catalanNPs as cNP
|
|
35
|
+
from . import johnsonNPs as jNP
|
|
36
|
+
from . import otherNPs as oNP
|
|
37
|
+
from . import utils as pyNMBu
|
|
38
|
+
from . import data
|
|
39
|
+
|
|
40
|
+
class CreateHRTEMStructure:
|
|
41
|
+
"""
|
|
42
|
+
A class for generating HRTEM images from CIF compounds. First the class creates XYZ files, viewable on JMOL,
|
|
43
|
+
containing the coordinates of a nanoparticule laying on a transmission electronic miscroscope (TEM) grid of carbon.
|
|
44
|
+
This class allows to create images for different NPs (various compounds, shapes, sizes) laying on their surfaces or edges
|
|
45
|
+
with different angles rotations, meaning it is possible to explore different orientation of the NP on the grid.
|
|
46
|
+
Then the class uses AbTEM to generate their HRTEM images. Many parameters are explored, see the HRTEM_parameter.ipynb notebook
|
|
47
|
+
for explanations.
|
|
48
|
+
|
|
49
|
+
Additional Notes:
|
|
50
|
+
- The supported nanoparticle shapes include: Wulff constructions: cube, octahedron, cuboctahedron, dodecahedron,
|
|
51
|
+
spheroids, and their truncated versions
|
|
52
|
+
- The NPs are laying on flat areas of the carbon substrate.
|
|
53
|
+
- The name of the output XYZ files is : {element}_{structure}_{form}_{counter}.xyz
|
|
54
|
+
- The name of the output PNG files is : {element}_{structure}_{form}_{xyz_counter}_{microscope_counter}.png
|
|
55
|
+
- Orientation (surface/edge), angles and size metadata are stored in CSV files.
|
|
56
|
+
- The NPs are created using the crystalNPs class.
|
|
57
|
+
- Files are created for each NP surface plane and each carbon grid flat surface.
|
|
58
|
+
- A distance tolerance between the NP and the grid can be choosen by the user.
|
|
59
|
+
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
def __init__(self, path, xyz_gz_file, cif_data, wulff_shapes,
|
|
63
|
+
nRot, sizes, min_size : float=0, max_size: float=50, tolerance: int=3, noOutput:bool = True):
|
|
64
|
+
|
|
65
|
+
"""
|
|
66
|
+
Initialize the class with CIF data, Wulff shapes information and size,
|
|
67
|
+
and the tolerance distance between the NP and the carbon grid.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
path (str): Path that will contain the output XYZ files.
|
|
71
|
+
xyz_gz_file (str): Path to the gzipped (.gz) XYZ file containing
|
|
72
|
+
atomic coordinates of the carbon substrate.
|
|
73
|
+
cif_data (pd.DataFrame): The CIF data of the compounds.
|
|
74
|
+
wulff_shapes (pd.DataFrame): The Wulff shapes and their informations.
|
|
75
|
+
nRot (int): Number of rotations of the NP laying on its surface
|
|
76
|
+
along z (angle = 360/nRot).
|
|
77
|
+
sizes (array_like): Array of the sizes of the nanoparticles.
|
|
78
|
+
min_size (float, optional): Minimal size for the NPs, equals to the
|
|
79
|
+
diameter of the circumscribed sphere. Defaults to 0 nm.
|
|
80
|
+
max_size (float, optional): Maximal size for the NPs, equals to the
|
|
81
|
+
diameter of the circumscribed sphere. Defaults to 50 nm.
|
|
82
|
+
tolerance (float, optional): Tolerance distance between the NP and
|
|
83
|
+
the carbon grid. Be careful: if too small, chemical bonds
|
|
84
|
+
appear in the interface. Defaults to 2.0.
|
|
85
|
+
noOutput (bool): If False, prints details of the generated files.
|
|
86
|
+
|
|
87
|
+
Note:
|
|
88
|
+
This class utilizes the internal method
|
|
89
|
+
:meth:`self.create_NP_TEMimages` using the provided parameters
|
|
90
|
+
(tolerance, noOutput, min_size, max_size, nRot, path, xyz_gz_file)
|
|
91
|
+
to process the nanoparticle images.
|
|
92
|
+
"""
|
|
93
|
+
self.path = path
|
|
94
|
+
self.xyz_gz_file = Path(xyz_gz_file)
|
|
95
|
+
self.cif_data = cif_data # DataFrame containing CIF data
|
|
96
|
+
self.wulff_shapes = wulff_shapes # DataFrame of Wulff forms
|
|
97
|
+
self.loaded_cifs = {} # stock loaded cif files
|
|
98
|
+
self.nRot = nRot
|
|
99
|
+
self.sizes= [[k] for k in sizes] #nested list of the sizes
|
|
100
|
+
self.tolerance= tolerance
|
|
101
|
+
self._xyz_counter = 0
|
|
102
|
+
self._xyz_metadata = []
|
|
103
|
+
|
|
104
|
+
substrate_name = str(xyz_gz_file)
|
|
105
|
+
if substrate_name.endswith('aC_relax_10x10.xyz.gz'):
|
|
106
|
+
self.substrate_size = [10, 10, 10]
|
|
107
|
+
elif substrate_name.endswith('aC_relax_5x5.xyz.gz'):
|
|
108
|
+
self.substrate_size = [5, 5, 5]
|
|
109
|
+
|
|
110
|
+
self.create_NP_TEMimages(tolerance, noOutput,min_size, max_size, nRot,path, xyz_gz_file)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def find_surface_atoms(self,xyz_gz_file, grid_size=2, z_tolerance=6):
|
|
114
|
+
"""
|
|
115
|
+
Identify surface atoms from a compressed XYZ file using a grid-based method.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
xyz_gz_file (str): Path to the gzipped (.gz) XYZ file containing
|
|
119
|
+
atomic coordinates of the carbon substrate.
|
|
120
|
+
grid_size (float, optional): Size of the xy-grid cells in Angstroms,
|
|
121
|
+
used to discretize the xy-plane. Smaller grid size results in
|
|
122
|
+
finer surface resolution, larger grid size is coarser.
|
|
123
|
+
Defaults to 2.
|
|
124
|
+
z_tolerance (float, optional): Maximum allowed vertical distance from
|
|
125
|
+
global maximum z to consider an atom as a surface atom.
|
|
126
|
+
Defaults to 6.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
None: Generates two files in a subdirectory named 'output_xyz':
|
|
130
|
+
* **<original_filename>_surface.xyz**: XYZ file copy where surface
|
|
131
|
+
carbon atoms are replaced by oxygen atoms ('O') for
|
|
132
|
+
visualization purposes.
|
|
133
|
+
* **<original_filename>_surface_atoms.txt**: Text file listing
|
|
134
|
+
indices (1-based) of atoms identified as surface atoms.
|
|
135
|
+
|
|
136
|
+
Note:
|
|
137
|
+
If some xy-grid cells do not contain any atoms near the surface
|
|
138
|
+
(for instance due to vertical gaps or columns), the algorithm might
|
|
139
|
+
select atoms located deep within the material. The `z_tolerance`
|
|
140
|
+
parameter ensures that only atoms close enough to the global maximum
|
|
141
|
+
z-value are considered, preventing deep internal atoms from being
|
|
142
|
+
mistakenly identified as surface atoms.
|
|
143
|
+
|
|
144
|
+
Procedure:
|
|
145
|
+
1. Reads atomic coordinates from the gzipped XYZ file.
|
|
146
|
+
2. Defines a regular xy-grid over the atomic coordinates.
|
|
147
|
+
3. Identifies the topmost atom (maximum z-coordinate) in each grid cell,
|
|
148
|
+
only if close enough to the global maximum z.
|
|
149
|
+
4. Marks these atoms as surface atoms and replaces their type with 'O'.
|
|
150
|
+
5. Saves the modified XYZ file and a text file listing surface
|
|
151
|
+
atom indices.
|
|
152
|
+
|
|
153
|
+
Example:
|
|
154
|
+
>>> find_surface_atoms('file.xyz.gz', grid_size=2.0, z_tolerance=5.0)
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
with gzip.open(xyz_gz_file, 'rt', encoding='utf-8' ) as f:
|
|
158
|
+
lines = f.readlines()
|
|
159
|
+
|
|
160
|
+
num_atoms = int(lines[0].strip())
|
|
161
|
+
header = lines[:2]
|
|
162
|
+
atom_lines = lines[2:2 + num_atoms]
|
|
163
|
+
coords = []
|
|
164
|
+
for line in atom_lines:
|
|
165
|
+
parts = line.split()
|
|
166
|
+
atom_type = parts[0]
|
|
167
|
+
x, y, z = map(float, parts[1:4])
|
|
168
|
+
coords.append((atom_type, x, y, z))
|
|
169
|
+
|
|
170
|
+
coords_array = np.array(coords, dtype=object)
|
|
171
|
+
xy_values = coords_array[:, 1:3].astype(float)
|
|
172
|
+
z_values = coords_array[:, 3].astype(float)
|
|
173
|
+
z_global_max = z_values.max()
|
|
174
|
+
x_min, y_min = xy_values.min(axis=0)
|
|
175
|
+
x_max, y_max = xy_values.max(axis=0)
|
|
176
|
+
x_bins = np.arange(x_min, x_max + grid_size, grid_size)
|
|
177
|
+
y_bins = np.arange(y_min, y_max + grid_size, grid_size)
|
|
178
|
+
|
|
179
|
+
surface_indices = []
|
|
180
|
+
for i in range(len(x_bins)-1):
|
|
181
|
+
for j in range(len(y_bins)-1):
|
|
182
|
+
in_cell = np.where((xy_values[:, 0] >= x_bins[i]) & (xy_values[:, 0] < x_bins[i+1]) &
|
|
183
|
+
(xy_values[:, 1] >= y_bins[j]) & (xy_values[:, 1] < y_bins[j+1]))[0]
|
|
184
|
+
if len(in_cell) > 0:
|
|
185
|
+
z_local_max_idx = in_cell[np.argmax(z_values[in_cell])]
|
|
186
|
+
if z_values[z_local_max_idx] >= z_global_max - z_tolerance:
|
|
187
|
+
surface_indices.append(z_local_max_idx)
|
|
188
|
+
|
|
189
|
+
surface_indices = np.unique(surface_indices)
|
|
190
|
+
modified_coords = coords.copy()
|
|
191
|
+
for idx in surface_indices:
|
|
192
|
+
atom_type, x, y, z = modified_coords[idx]
|
|
193
|
+
modified_coords[idx] = ('O', x, y, z)
|
|
194
|
+
|
|
195
|
+
return surface_indices
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def flat_areas_atoms(self,xyz_gz_file, surface_indices, diameter_nm=2, min_cluster_span=20, max_z_variation=5, anisotropy_threshold= 1.1, overlap_tolerance=0.2):
|
|
199
|
+
"""
|
|
200
|
+
Identify candidate grafting sites for a nanoparticle of given diameter
|
|
201
|
+
using a sliding grid over the xy-plane.
|
|
202
|
+
|
|
203
|
+
Each candidate cluster must satisfy the following criteria:
|
|
204
|
+
* Contain at least two atoms.
|
|
205
|
+
* Cover a minimum lateral spatial extent (min_cluster_span).
|
|
206
|
+
* Remain sufficiently flat (within max_z_variation in the z direction).
|
|
207
|
+
* Be reasonably isotropic in xy (to avoid long narrow stripes).
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
xyz_gz_file (str): Path to the gzipped (.gz) XYZ file containing
|
|
211
|
+
atomic coordinates of the carbon substrate.
|
|
212
|
+
surface_indices (list of int): List of surface atom indices (0-based)
|
|
213
|
+
to consider for clustering.
|
|
214
|
+
diameter_nm (float): Diameter of the nanoparticle (in nanometers),
|
|
215
|
+
used to define the size of sliding xy grid cells (1 nm = 10 Å).
|
|
216
|
+
min_cluster_span (float): Minimum lateral spatial extent (in Å)
|
|
217
|
+
required between atoms in a cluster (in xy-plane). Prevents
|
|
218
|
+
selecting groups that are too small to accommodate the NP laterally.
|
|
219
|
+
max_z_variation (float): Maximum allowed difference in z-coordinates
|
|
220
|
+
(in Å) within a cluster. Ensures that the candidate site is
|
|
221
|
+
sufficiently flat.
|
|
222
|
+
anisotropy_threshold (float): Maximum allowed ratio between std(x)
|
|
223
|
+
and std(y) (or vice versa) in the cluster. Rejects elongated,
|
|
224
|
+
anisotropic clusters.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
list: A list containing the coordinates (np.ndarray) of the
|
|
228
|
+
accepted substrate flat areas.
|
|
229
|
+
"""
|
|
230
|
+
import numpy as np
|
|
231
|
+
from pathlib import Path
|
|
232
|
+
import gzip
|
|
233
|
+
import random
|
|
234
|
+
from scipy.spatial.distance import pdist
|
|
235
|
+
from matplotlib import colormaps
|
|
236
|
+
|
|
237
|
+
log_lines = []
|
|
238
|
+
with gzip.open(xyz_gz_file, 'rt', encoding='utf-8') as f:
|
|
239
|
+
lines = f.readlines()
|
|
240
|
+
|
|
241
|
+
num_atoms = int(lines[0].strip())
|
|
242
|
+
atom_lines = lines[2:2 + num_atoms]
|
|
243
|
+
|
|
244
|
+
coords = []
|
|
245
|
+
for line in atom_lines:
|
|
246
|
+
parts = line.split()
|
|
247
|
+
x, y, z = map(float, parts[1:4])
|
|
248
|
+
coords.append((x, y, z))
|
|
249
|
+
|
|
250
|
+
xyz = np.array(coords)
|
|
251
|
+
surface_xyz = xyz[surface_indices]
|
|
252
|
+
|
|
253
|
+
# Define sliding grid parameters
|
|
254
|
+
diameter_angstrom = diameter_nm * 10
|
|
255
|
+
|
|
256
|
+
x_min, y_min = surface_xyz[:, :2].min(axis=0)
|
|
257
|
+
x_max, y_max = surface_xyz[:, :2].max(axis=0)
|
|
258
|
+
|
|
259
|
+
accepted_clusters = []
|
|
260
|
+
cluster_dict = {}
|
|
261
|
+
cluster_id = 0
|
|
262
|
+
cluster_rejected_dict = {}
|
|
263
|
+
cluster_rejected_id = 0
|
|
264
|
+
|
|
265
|
+
x_starts = np.arange(x_min, x_max, 5.0)
|
|
266
|
+
y_starts = np.arange(y_min, y_max, 10.0)
|
|
267
|
+
accepted_cluster_coords=[]
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
for i, x0 in enumerate(x_starts):
|
|
271
|
+
for j, y0 in enumerate(y_starts):
|
|
272
|
+
local_indices = [idx for idx, (x, y, _) in enumerate(surface_xyz)
|
|
273
|
+
if x0 <= x < x0 + diameter_angstrom and y0 <= y < y0 + diameter_angstrom]
|
|
274
|
+
msg = f"🔵🔵🔵 Grid cell ({i},{j}) at ({x0:.1f}, {y0:.1f}) → {len(local_indices)} atoms"
|
|
275
|
+
# print(msg)
|
|
276
|
+
log_lines.append(msg)
|
|
277
|
+
|
|
278
|
+
if len(local_indices) >= 2:
|
|
279
|
+
coords_patch = surface_xyz[local_indices]
|
|
280
|
+
xy_patch = coords_patch[:, :2]
|
|
281
|
+
max_xy_dist = np.max(pdist(xy_patch)) if len(xy_patch) >= 2 else 0
|
|
282
|
+
std_x, std_y = np.std(xy_patch[:, 0]), np.std(xy_patch[:, 1])
|
|
283
|
+
anisotropy = max(std_x, std_y) / max(1e-6, min(std_x, std_y))
|
|
284
|
+
x_values= coords_patch[:, 0]
|
|
285
|
+
y_values= coords_patch[:, 1]
|
|
286
|
+
z_values = coords_patch[:, 2]
|
|
287
|
+
z_range = z_values.max() - z_values.min()
|
|
288
|
+
|
|
289
|
+
reasons = []
|
|
290
|
+
ok = []
|
|
291
|
+
if max_xy_dist < min_cluster_span:
|
|
292
|
+
reasons.append(f"too compact (xy span = {max_xy_dist:.2f} Å < {min_cluster_span} Å)")
|
|
293
|
+
else:
|
|
294
|
+
ok.append(f"compact (xy span = {max_xy_dist:.2f} Å > {min_cluster_span} Å)")
|
|
295
|
+
if z_range > max_z_variation:
|
|
296
|
+
reasons.append(f"not flat (z_range = {z_range:.2f} Å > {max_z_variation} Å)")
|
|
297
|
+
else:
|
|
298
|
+
ok.append(f"flat (z_range = {z_range:.2f} Å < {max_z_variation} Å)")
|
|
299
|
+
if anisotropy > anisotropy_threshold:
|
|
300
|
+
reasons.append(f"too elongated (anisotropy = {anisotropy:.2f} > {anisotropy_threshold})")
|
|
301
|
+
else:
|
|
302
|
+
ok.append(f"regular shape (anisotropy = {anisotropy:.2f} < {anisotropy_threshold})")
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
# NEW
|
|
306
|
+
|
|
307
|
+
# Check overlap
|
|
308
|
+
atom_ids = [surface_indices[i] + 1 for i in local_indices]
|
|
309
|
+
overlap_found = False
|
|
310
|
+
for existing in accepted_clusters:
|
|
311
|
+
common = set(atom_ids) & set(existing)
|
|
312
|
+
if len(common) / max(len(atom_ids), len(existing)) > overlap_tolerance:
|
|
313
|
+
reasons.append(f"overlap > {int(overlap_tolerance*100)}% with existing cluster")
|
|
314
|
+
overlap_found = True
|
|
315
|
+
break
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
if not reasons:
|
|
320
|
+
cluster_dict[cluster_id] = [surface_indices[i] + 1 for i in local_indices]
|
|
321
|
+
atom_ids = cluster_dict[cluster_id]
|
|
322
|
+
accepted_clusters.append(atom_ids)
|
|
323
|
+
select_line = f"select all; color atoms grey; select {', '.join(f'@{idx}' for idx in atom_ids)}; color atoms green"
|
|
324
|
+
z_mean = z_values.mean()
|
|
325
|
+
msg = f" ✅ Cluster {cluster_id:2d} ✓ n = {len(local_indices):2d}, z mean = {z_mean:.2f}, min = {z_values.min():.2f}, max = {z_values.max():.2f}, max_xy_dist={max_xy_dist:.2f} [{min_cluster_span} Å], z_range={z_range:.2f} [{max_z_variation} Å], anisotropy={anisotropy:.2f}"
|
|
326
|
+
# print(msg)
|
|
327
|
+
log_lines.append(msg)
|
|
328
|
+
log_lines.append(select_line)
|
|
329
|
+
log_lines.append("") # blank line between clusters
|
|
330
|
+
cluster_id += 1
|
|
331
|
+
accepted_cluster_coords.append(coords_patch) # NEW
|
|
332
|
+
else:
|
|
333
|
+
cluster_rejected_dict[cluster_rejected_id] = [surface_indices[i] + 1 for i in local_indices]
|
|
334
|
+
atom_ids = cluster_rejected_dict[cluster_rejected_id]
|
|
335
|
+
select_line = f"select all; color atoms grey; select {', '.join(f'@{idx}' for idx in atom_ids)}; color atoms red"
|
|
336
|
+
out_str = "; ".join(reasons)
|
|
337
|
+
ok_str = "; ".join(ok)
|
|
338
|
+
out_str = f"{out_str}; ✅ FULFILLED CRITERIA = {ok_str}"
|
|
339
|
+
msg = f" ❌ Rejected: {out_str}"
|
|
340
|
+
# print(msg)
|
|
341
|
+
log_lines.append(msg)
|
|
342
|
+
log_lines.append(select_line)
|
|
343
|
+
log_lines.append("") # blank line between clusters
|
|
344
|
+
cluster_rejected_id += 1
|
|
345
|
+
return accepted_cluster_coords
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
def place_NPsurface_on_grid(self, path, xyz_gz_file, surface_indices,tolerance,instanceWulff,surfaces_indices, element, structure, form, nRot, number, noOutput, circumsphere_diameter):
|
|
349
|
+
|
|
350
|
+
"""
|
|
351
|
+
Create XYZ files of NPs laying on one of their surfaces on a carbon grid.
|
|
352
|
+
|
|
353
|
+
This function generates the interface between the nanoparticle and the carbon
|
|
354
|
+
substrate, positioning the nanoparticle so it lays flat on its surface facet.
|
|
355
|
+
|
|
356
|
+
Args:
|
|
357
|
+
xyz_gz_file (str): Path to the gzipped (.gz) XYZ file containing
|
|
358
|
+
atomic coordinates of the carbon substrate.
|
|
359
|
+
tolerance (float, optional): Tolerance distance between the NP and
|
|
360
|
+
the carbon grid. Be careful: if too small, artificial chemical
|
|
361
|
+
bonds may appear at the interface.
|
|
362
|
+
instanceWulff (object): Instance of the crystalNPs class used to
|
|
363
|
+
create the Wulff nanoparticle.
|
|
364
|
+
element (str): Compound elements, as defined in
|
|
365
|
+
:meth:`create_NP_TEMimages`.
|
|
366
|
+
structure (str): Lattice type, as defined in
|
|
367
|
+
:meth:`create_NP_TEMimages`.
|
|
368
|
+
form (str): Wulff form/shape, as defined in
|
|
369
|
+
:meth:`create_NP_TEMimages`.
|
|
370
|
+
nRot (int): Number of rotations of the NP along the z-axis
|
|
371
|
+
(rotation angle = 360/nRot).
|
|
372
|
+
number (int): Index for the nanoparticle size, as defined in
|
|
373
|
+
:meth:`create_NP_TEMimages`.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
None: Generates XYZ files for each compound and size, representing
|
|
377
|
+
the nanoparticle laying on the carbon grid.
|
|
378
|
+
"""
|
|
379
|
+
|
|
380
|
+
# 1. The NP surface
|
|
381
|
+
# Place the NP on its surface
|
|
382
|
+
plane = instanceWulff.trPlanes[0]
|
|
383
|
+
# if noOutput == False :
|
|
384
|
+
# print(f'Surface plane of the NP used = {plane}.')
|
|
385
|
+
normal_plane= np.array(plane[:3]) # Normal of the NP surface plane
|
|
386
|
+
|
|
387
|
+
# 2. The carbon surface
|
|
388
|
+
# Find the plane that fits the best the cluster points (flat area of the carbon substrate surface where the NP will be on)
|
|
389
|
+
accepted_cluster_coords= self.flat_areas_atoms(xyz_gz_file, surface_indices)
|
|
390
|
+
|
|
391
|
+
# Select the flat-area cluster depending on the substrate file
|
|
392
|
+
substrate_name = str(xyz_gz_file)
|
|
393
|
+
if substrate_name.endswith('aC_relax_10x10.xyz.gz'):
|
|
394
|
+
cluster = accepted_cluster_coords[10]
|
|
395
|
+
elif substrate_name.endswith('aC_relax_5x5.xyz.gz'):
|
|
396
|
+
cluster = accepted_cluster_coords[2]
|
|
397
|
+
else:
|
|
398
|
+
cluster = accepted_cluster_coords[0] # default: first flat area
|
|
399
|
+
|
|
400
|
+
carbon_plane_positions = cluster # Positions of the atoms of the flat carbon surface
|
|
401
|
+
pca = PCA(n_components=3)
|
|
402
|
+
pca.fit(carbon_plane_positions)
|
|
403
|
+
normal_carbon = pca.components_[-1] # Normal vector but we don't know the sense (positive or negative)
|
|
404
|
+
if normal_carbon[2] < 0: # positive z
|
|
405
|
+
normal_carbon = -normal_carbon
|
|
406
|
+
normal_carbon_unit = normal_carbon / np.linalg.norm(normal_carbon)
|
|
407
|
+
center_carbon_plane = carbon_plane_positions.mean(axis=0)
|
|
408
|
+
|
|
409
|
+
# 3. Place the NPs close to the substrate
|
|
410
|
+
# Compute the carbon surface distance from the origin
|
|
411
|
+
dist_carbon = -np.dot(normal_carbon, center_carbon_plane) # -d
|
|
412
|
+
# Make the NP surface plane parallel to the carbon surface
|
|
413
|
+
rotated_positions = pyNMBu.rotateMoltoAlignItWithAxis(instanceWulff.NP.positions,axis=normal_plane,targetAxis=normal_carbon)
|
|
414
|
+
rotated_positions_on_carbon_unit = np.dot(rotated_positions - center_carbon_plane, normal_carbon_unit) # Project the NP atoms positions on the carbon surface normal vector : gives height from the carbon surface
|
|
415
|
+
|
|
416
|
+
min_point_proj=np.min(rotated_positions_on_carbon_unit) # Find the closest NP atom from the carbon surface
|
|
417
|
+
|
|
418
|
+
# 3.1 Move the np towards the carbon surface horizontally
|
|
419
|
+
center_np = rotated_positions.mean(axis=0)
|
|
420
|
+
# On veut projeter center_np sur le plan défini par center_carbon_plane et normal_carbon_unit
|
|
421
|
+
vec_to_plane = center_np - center_carbon_plane
|
|
422
|
+
dist_to_plane = np.dot(vec_to_plane, normal_carbon_unit)
|
|
423
|
+
proj_center_np_on_plane = center_np - dist_to_plane * normal_carbon_unit
|
|
424
|
+
# Décalage latéral à appliquer
|
|
425
|
+
lateral_shift = center_carbon_plane - proj_center_np_on_plane
|
|
426
|
+
# print('Horizontal translation vector = ',lateral_shift)
|
|
427
|
+
rotated_and_shifted_positions = rotated_positions + lateral_shift
|
|
428
|
+
|
|
429
|
+
# 3.2 Move the np towards the carbon surface (1 Angs above) vertically
|
|
430
|
+
# Translation_vector is for translating the NP vertically only
|
|
431
|
+
translation_vector = (-min_point_proj + tolerance) * normal_carbon_unit
|
|
432
|
+
translated_positions = rotated_and_shifted_positions + translation_vector
|
|
433
|
+
center_of_rotation = translated_positions.mean(axis=0)
|
|
434
|
+
|
|
435
|
+
# 3.3 Change the orientation of the NP along the plane xy
|
|
436
|
+
for i in np.random.randint(0, 360, nRot) :
|
|
437
|
+
angle = i
|
|
438
|
+
# translated_positions = pyNMBu.rotationMolAroundAxis(translated_positions, angle, normal_carbon_unit)
|
|
439
|
+
new_positions = pyNMBu.rotation_around_axis_through_point(translated_positions,angle_deg=angle,axis=normal_carbon,center=center_of_rotation)
|
|
440
|
+
|
|
441
|
+
# 4. Create the files for each NPs
|
|
442
|
+
self._xyz_counter += 1
|
|
443
|
+
xyz_filename = f"{path}/{element}_{structure}_{form}_{self._xyz_counter:06d}.xyz"
|
|
444
|
+
self._xyz_metadata.append({
|
|
445
|
+
"xyz_file": f"{element}_{structure}_{form}_{self._xyz_counter:06d}.xyz",
|
|
446
|
+
"orientation": "surface",
|
|
447
|
+
"angle_xy": int(angle),
|
|
448
|
+
"angle_tilt": 0,
|
|
449
|
+
"circumsphere_diameter_nm": round(circumsphere_diameter, 4),
|
|
450
|
+
"substrate_size_nm": self.substrate_size[0]
|
|
451
|
+
})
|
|
452
|
+
# Read the carbone files
|
|
453
|
+
carbon_lines = []
|
|
454
|
+
with gzip.open(xyz_gz_file, 'rt', encoding='utf-8') as f0:
|
|
455
|
+
i = 0
|
|
456
|
+
for line in f0:
|
|
457
|
+
i += 1
|
|
458
|
+
if i >= 3: # on saute les 2 premières lignes (nombre d'atomes et commentaire)
|
|
459
|
+
carbon_lines.append(line.strip())
|
|
460
|
+
n_carbon = len(carbon_lines)
|
|
461
|
+
n_au = len(new_positions)
|
|
462
|
+
total_atoms = n_carbon + n_au
|
|
463
|
+
with open(xyz_filename, "w") as f:
|
|
464
|
+
f.write(f"{total_atoms}\n")
|
|
465
|
+
f.write("Carbon substrate + translated nanoparticle\n")
|
|
466
|
+
|
|
467
|
+
# Écriture des atomes carbone
|
|
468
|
+
for line in carbon_lines:
|
|
469
|
+
f.write(f"{line}\n")
|
|
470
|
+
|
|
471
|
+
# Écriture des atomes or (NP)
|
|
472
|
+
for pos in new_positions:
|
|
473
|
+
f.write(f"{element} {pos[0]:.6f} {pos[1]:.6f} {pos[2]:.6f}\n")
|
|
474
|
+
if noOutput == False :
|
|
475
|
+
print(f" \033[34m File written : {xyz_filename} \033[0m")
|
|
476
|
+
print('')
|
|
477
|
+
|
|
478
|
+
def place_NPedge_on_grid(self, path,xyz_gz_file,surface_indices,tolerance,instanceWulff,surfaces_indices, element, structure, form, nRot, number, noOutput, circumsphere_diameter):
|
|
479
|
+
|
|
480
|
+
"""
|
|
481
|
+
Create XYZ files of nanoparticles laying on one of their edges on a carbon grid.
|
|
482
|
+
|
|
483
|
+
The alignment process follows three main steps:
|
|
484
|
+
1. The target edge and its two adjacent planes of the NP are computed.
|
|
485
|
+
2. The NP edge is oriented to be parallel to the carbon flat surface.
|
|
486
|
+
3. The bisector of the two adjacent planes is aligned with the normal of the carbon flat surface.
|
|
487
|
+
|
|
488
|
+
This configuration ensures a perfectly centered NP laying on its edge,
|
|
489
|
+
allowing for balanced tilting on both sides during TEM simulations.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
xyz_gz_file (str): Path to the gzipped (.gz) XYZ file containing
|
|
493
|
+
atomic coordinates of the carbon substrate.
|
|
494
|
+
tolerance (float, optional): Tolerance distance between the NP and
|
|
495
|
+
the carbon grid. Be careful: if too small, artificial chemical
|
|
496
|
+
bonds may appear at the interface.
|
|
497
|
+
instanceWulff (object): Instance of the crystalNPs class used to
|
|
498
|
+
create the Wulff nanoparticle.
|
|
499
|
+
surfaces_indices (list of int): Indices of the atoms belonging to
|
|
500
|
+
the identified flat carbon surface.
|
|
501
|
+
element (str): Compound elements, as defined in
|
|
502
|
+
:meth:`create_NP_TEMimages`.
|
|
503
|
+
structure (str): Lattice type, as defined in
|
|
504
|
+
:meth:`create_NP_TEMimages`.
|
|
505
|
+
form (str): Wulff form/shape, as defined in
|
|
506
|
+
:meth:`create_NP_TEMimages`.
|
|
507
|
+
nRot (int): Number of rotations of the NP (incremental
|
|
508
|
+
angle = 360/nRot).
|
|
509
|
+
number (int): Index for the nanoparticle size, as defined in
|
|
510
|
+
:meth:`create_NP_TEMimages`.
|
|
511
|
+
|
|
512
|
+
Returns:
|
|
513
|
+
None: Generates XYZ files for each compound and size, representing
|
|
514
|
+
the nanoparticle laying on one of its edges on the carbon grid.
|
|
515
|
+
|
|
516
|
+
"""
|
|
517
|
+
|
|
518
|
+
# 1. Find the NP edge and the two adjacent planes.
|
|
519
|
+
from collections import defaultdict
|
|
520
|
+
edge_count = defaultdict(int)
|
|
521
|
+
|
|
522
|
+
# Using simplices of the Hull algorithm
|
|
523
|
+
for triangle in instanceWulff.simplices:
|
|
524
|
+
for i in range(3):
|
|
525
|
+
a = triangle[i]
|
|
526
|
+
b = triangle[(i + 1) % 3]
|
|
527
|
+
edge = tuple(sorted((a, b)))
|
|
528
|
+
edge_count[edge] += 1
|
|
529
|
+
# Extract the edge
|
|
530
|
+
true_edges = [edge for edge, count in edge_count.items() if count == 2]
|
|
531
|
+
|
|
532
|
+
for edge in true_edges:
|
|
533
|
+
a, b = edge
|
|
534
|
+
atom_a = instanceWulff.NP.positions[a]
|
|
535
|
+
atom_b = instanceWulff.NP.positions[b]
|
|
536
|
+
edge = pyNMBu.vector(instanceWulff.NP.positions,a,b)
|
|
537
|
+
adjacent_planes = []
|
|
538
|
+
|
|
539
|
+
for planes in instanceWulff.trPlanes:
|
|
540
|
+
a1, b1, c1, d1 = planes
|
|
541
|
+
if (abs(a1 * atom_a[0] + b1 * atom_a[1] + c1 * atom_a[2] + d1) < 1e-2 and
|
|
542
|
+
abs(a1 * atom_b[0] + b1 * atom_b[1] + c1 * atom_b[2] + d1) < 1e-2):
|
|
543
|
+
adjacent_planes.append(planes)
|
|
544
|
+
|
|
545
|
+
if len(adjacent_planes) >= 2:
|
|
546
|
+
p1, p2 = adjacent_planes
|
|
547
|
+
n1 = np.array(p1[:3]) / np.linalg.norm(p1[:3])
|
|
548
|
+
n2 = np.array(p2[:3]) / np.linalg.norm(p2[:3])
|
|
549
|
+
bisector = n1 + n2
|
|
550
|
+
if np.linalg.norm(bisector) > 1e-6:
|
|
551
|
+
break # good edge found
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
# print('adjacent_planes',adjacent_planes)
|
|
555
|
+
if len(adjacent_planes) >= 2 :
|
|
556
|
+
# if noOutput == False :
|
|
557
|
+
# print('Two adjacent planes found.')
|
|
558
|
+
|
|
559
|
+
# Normals of the two planes in order to compute the bisector
|
|
560
|
+
normal_p1 = np.array(p1[:3])
|
|
561
|
+
normal_p2 = np.array(p2[:3])
|
|
562
|
+
normal_p1_unit = normal_p1 / np.linalg.norm(normal_p1)
|
|
563
|
+
normal_p2_unit = normal_p2 / np.linalg.norm(normal_p2)
|
|
564
|
+
|
|
565
|
+
# Compute the bisector of the two planes
|
|
566
|
+
bisector = normal_p1_unit + normal_p2_unit
|
|
567
|
+
if np.linalg.norm(bisector) < 1e-6:
|
|
568
|
+
if not noOutput :
|
|
569
|
+
print("Bisector not well defined, normals of the planes opposite.")
|
|
570
|
+
bisector_unit = bisector / np.linalg.norm(bisector)
|
|
571
|
+
|
|
572
|
+
# Carbon surface : compute the normal of the surface and a vector contained on the surface (using PCA)
|
|
573
|
+
accepted_cluster_coords = self.flat_areas_atoms(xyz_gz_file, surface_indices)
|
|
574
|
+
|
|
575
|
+
# Select the flat-area cluster depending on the substrate file
|
|
576
|
+
substrate_name = str(xyz_gz_file)
|
|
577
|
+
if substrate_name.endswith('aC_relax_10x10.xyz.gz'):
|
|
578
|
+
cluster = accepted_cluster_coords[10]
|
|
579
|
+
elif substrate_name.endswith('aC_relax_5x5.xyz.gz'):
|
|
580
|
+
cluster = accepted_cluster_coords[2]
|
|
581
|
+
|
|
582
|
+
else:
|
|
583
|
+
cluster = accepted_cluster_coords[0] # default: first flat area
|
|
584
|
+
|
|
585
|
+
carbon_plane_positions = cluster
|
|
586
|
+
pca = PCA(n_components=3)
|
|
587
|
+
pca.fit(carbon_plane_positions)
|
|
588
|
+
normal_carbon = pca.components_[-1]
|
|
589
|
+
plane_x = pca.components_[0] # vector in the plane (xy) = carbon plane
|
|
590
|
+
|
|
591
|
+
# Watchout the sign (z)
|
|
592
|
+
if normal_carbon[2] < 0:
|
|
593
|
+
normal_carbon = -normal_carbon
|
|
594
|
+
normal_carbon_unit = normal_carbon / np.linalg.norm(normal_carbon)
|
|
595
|
+
center_carbon_plane = carbon_plane_positions.mean(axis=0)
|
|
596
|
+
|
|
597
|
+
# Unique rotation to : 1) make the edge of the NP parallel to the carbon surface, and 2) align the bisector with the carbon normal
|
|
598
|
+
# Local coordinate system
|
|
599
|
+
edge_unit = edge / np.linalg.norm(edge)
|
|
600
|
+
z_NP = bisector_unit
|
|
601
|
+
x_NP = edge_unit
|
|
602
|
+
y_NP = np.cross(z_NP, x_NP)
|
|
603
|
+
y_NP /= np.linalg.norm(y_NP)
|
|
604
|
+
x_NP = np.cross(y_NP, z_NP)
|
|
605
|
+
R_NP = np.stack([x_NP, y_NP, z_NP], axis=1)
|
|
606
|
+
|
|
607
|
+
# Target coordinate system
|
|
608
|
+
z_target = normal_carbon_unit
|
|
609
|
+
x_target = plane_x / np.linalg.norm(plane_x)
|
|
610
|
+
y_target = np.cross(z_target, x_target)
|
|
611
|
+
y_target /= np.linalg.norm(y_target)
|
|
612
|
+
x_target = np.cross(y_target, z_target)
|
|
613
|
+
R_target = np.stack([x_target, y_target, z_target], axis=1)
|
|
614
|
+
|
|
615
|
+
# Global rotation
|
|
616
|
+
center_of_rotation = instanceWulff.NP.positions.mean(axis=0)
|
|
617
|
+
R = R_target @ R_NP.T
|
|
618
|
+
rotated_positions = (R @ (instanceWulff.NP.positions - center_of_rotation).T).T + center_of_rotation
|
|
619
|
+
|
|
620
|
+
# Lateral translation
|
|
621
|
+
center_np = rotated_positions.mean(axis=0)
|
|
622
|
+
vec_to_plane = center_np - center_carbon_plane
|
|
623
|
+
dist_to_plane = np.dot(vec_to_plane, normal_carbon_unit)
|
|
624
|
+
proj_center_np_on_plane = center_np - dist_to_plane * normal_carbon_unit
|
|
625
|
+
lateral_shift = center_carbon_plane - proj_center_np_on_plane
|
|
626
|
+
rotated_and_shifted_positions = rotated_positions + lateral_shift
|
|
627
|
+
|
|
628
|
+
# Vertical translation
|
|
629
|
+
rotated_positions_on_carbon_unit = np.dot(rotated_and_shifted_positions - center_carbon_plane, normal_carbon_unit)
|
|
630
|
+
min_point_proj = np.min(rotated_positions_on_carbon_unit)
|
|
631
|
+
translation_vector = (-min_point_proj + tolerance) * normal_carbon_unit
|
|
632
|
+
translated_positions = rotated_and_shifted_positions + translation_vector
|
|
633
|
+
new_center = translated_positions.mean(axis=0)
|
|
634
|
+
|
|
635
|
+
# Angle between the 2 planes
|
|
636
|
+
dot_product = np.dot(normal_p1_unit, normal_p2_unit)
|
|
637
|
+
angle_n1_n2 = np.arccos(dot_product)
|
|
638
|
+
angle_n1_n2 = np.degrees(angle_n1_n2)
|
|
639
|
+
# if not noOutput :
|
|
640
|
+
# print(f"Angle entre les 2 plans = {angle_n1_n2:.2f}°")
|
|
641
|
+
# Max angle between the planes and the (xy) plane
|
|
642
|
+
angle_max = (180 - angle_n1_n2 ) / 2
|
|
643
|
+
# if not noOutput :
|
|
644
|
+
# print(f"Angle max entre les plans et le plan (xy) = {angle_max:.2f}°")
|
|
645
|
+
from scipy.spatial.transform import Rotation as R
|
|
646
|
+
rotation_axis = edge_unit
|
|
647
|
+
|
|
648
|
+
# Tilt the NP along z
|
|
649
|
+
for angle in np.random.randint(-angle_max, angle_max, nRot) :
|
|
650
|
+
tilt_rotation = R.from_rotvec(angle * rotation_axis)
|
|
651
|
+
tilt_center = new_center
|
|
652
|
+
positions_tilted = tilt_rotation.apply(translated_positions - tilt_center) + tilt_center
|
|
653
|
+
center_of_rotation = positions_tilted.mean(axis=0)
|
|
654
|
+
|
|
655
|
+
# Change the orientation of the NP along the plane xy
|
|
656
|
+
for i in np.random.randint(0, 360, nRot) :
|
|
657
|
+
angle2 = i
|
|
658
|
+
# translated_positions = pyNMBu.rotationMolAroundAxis(translated_positions, angle, normal_carbon_unit)
|
|
659
|
+
new_positions = pyNMBu.rotation_around_axis_through_point(positions_tilted,angle_deg = angle2,axis = normal_carbon,center = center_of_rotation)
|
|
660
|
+
|
|
661
|
+
# Write and save the XYZ file
|
|
662
|
+
self._xyz_counter += 1
|
|
663
|
+
xyz_filename = f"{path}/{element}_{structure}_{form}_{self._xyz_counter:06d}.xyz"
|
|
664
|
+
self._xyz_metadata.append({
|
|
665
|
+
"xyz_file": f"{element}_{structure}_{form}_{self._xyz_counter:06d}.xyz",
|
|
666
|
+
"orientation": "edge",
|
|
667
|
+
"angle_xy": int(angle2),
|
|
668
|
+
"angle_tilt": int(angle),
|
|
669
|
+
"circumsphere_diameter_nm": round(circumsphere_diameter, 4),
|
|
670
|
+
"substrate_size_nm": self.substrate_size[0]
|
|
671
|
+
})
|
|
672
|
+
carbon_lines = []
|
|
673
|
+
with gzip.open(xyz_gz_file, 'rt', encoding='utf-8' ) as f0:
|
|
674
|
+
i = 0
|
|
675
|
+
for line in f0:
|
|
676
|
+
i += 1
|
|
677
|
+
if i >= 3:
|
|
678
|
+
carbon_lines.append(line.strip())
|
|
679
|
+
|
|
680
|
+
n_carbon = len(carbon_lines)
|
|
681
|
+
n_au = len(new_positions)
|
|
682
|
+
total_atoms = n_carbon + n_au
|
|
683
|
+
|
|
684
|
+
with open(xyz_filename, "w") as f:
|
|
685
|
+
f.write(f"{total_atoms}\n")
|
|
686
|
+
f.write("Carbon substrate + translated nanoparticle\n")
|
|
687
|
+
for line in carbon_lines:
|
|
688
|
+
f.write(f"{line}\n")
|
|
689
|
+
for pos in new_positions:
|
|
690
|
+
f.write(f"{element} {pos[0]:.6f} {pos[1]:.6f} {pos[2]:.6f}\n")
|
|
691
|
+
if not noOutput :
|
|
692
|
+
print(f"\033[32m File written : {xyz_filename} \033[0m")
|
|
693
|
+
print('')
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
def create_NP_TEMimages(self, tolerance, noOutput,min_size,max_size, nRot, path, xyz_gz_file):
|
|
697
|
+
"""
|
|
698
|
+
Generate Wulff shapes and their files for a single specific CIF coumpound.
|
|
699
|
+
|
|
700
|
+
Args:
|
|
701
|
+
cif file (file): singular cif file
|
|
702
|
+
noOutput (bool): if bool=False : details of the files
|
|
703
|
+
min_size (float, optional) : Mainimal size for the NPs, equals to the diameter of the circumscribed sphere, equals 0 nm by default.
|
|
704
|
+
max_size (float, optional) : Maximal size for the NPs, equals to the diameter of the circumscribed sphere, equals 50 nm by default.
|
|
705
|
+
path (str) : path that will contain the created xyz/CIF files
|
|
706
|
+
substrate_size (list) : size of the substrate in each dimension
|
|
707
|
+
"""
|
|
708
|
+
surface_indices = self.find_surface_atoms(xyz_gz_file, grid_size=2, z_tolerance=6)
|
|
709
|
+
for cif_name, cif_file in self.cif_data['cif file'].items():
|
|
710
|
+
|
|
711
|
+
# Extract the structure name for the name of the files, for example 'Rutile' or 'Anatase' or 'Alpha'
|
|
712
|
+
self.cif_name=cif_name
|
|
713
|
+
if len(self.cif_name.split())==2 : # For the name of the files, for example 'Rutile' or 'Anatase'
|
|
714
|
+
structure=self.cif_name.split()[1]
|
|
715
|
+
else :
|
|
716
|
+
structure=None
|
|
717
|
+
if not noOutput :
|
|
718
|
+
print(f'\n\033[1m {bg.LIGHTBLUEB} {cif_name.center(50)}\033[0m\n')
|
|
719
|
+
cif_info = pyNMBu.load_cif(self,cif_file,noOutput)
|
|
720
|
+
crystal_system_name = self.ucBL.__class__.__name__
|
|
721
|
+
direction=[0,0,1]
|
|
722
|
+
d_hkl=pyNMBu.interPlanarSpacing(direction,self.ucUnitcell,crystal_system_name)*0.1 #nm
|
|
723
|
+
if not noOutput:
|
|
724
|
+
print(f'\033[1m d_hkl={d_hkl} nm \033[0m')
|
|
725
|
+
|
|
726
|
+
if len(cif_name.split()) >= 2 : # to exclude TiO2 and NaCl
|
|
727
|
+
for form, row in self.wulff_shapes.iterrows():
|
|
728
|
+
lattices = [l.strip() for l in row['Bravais lattice'].split(',')]
|
|
729
|
+
if self.crystal_type in lattices: # Verify if the lattice of the compound matches the lattice of the wulff form
|
|
730
|
+
index=0
|
|
731
|
+
if not noOutput :
|
|
732
|
+
print(f"\n {bg.LIGHTGREENB} {self.crystal_type} corresponds to the lattices {lattices} of the Wulff {form} form. \033[0m \n")
|
|
733
|
+
number=0
|
|
734
|
+
# Create instances for each form and size
|
|
735
|
+
for i in self.sizes :
|
|
736
|
+
# number+=1
|
|
737
|
+
size= [i[0]*d_hkl]
|
|
738
|
+
index += 1
|
|
739
|
+
TestNP = cyNP.Crystal(
|
|
740
|
+
crystal=f'{cif_name}',
|
|
741
|
+
userDefCif=cif_info['cif_path'],
|
|
742
|
+
shape=f"Wulff: {form}",
|
|
743
|
+
sizesWulff= size,
|
|
744
|
+
threshold=0.001,
|
|
745
|
+
thresholdCoreSurface=2,
|
|
746
|
+
postAnalyzis=True,
|
|
747
|
+
jmolCrystalShape=True,
|
|
748
|
+
noOutput=True,
|
|
749
|
+
aseView=False,
|
|
750
|
+
skipSymmetryAnalyzis=True)
|
|
751
|
+
|
|
752
|
+
circumsphere_diameter=TestNP.radiusCircumscribedSphere*2*0.1 # Setting a maximal size of NPs : circumscribed sphere diameter
|
|
753
|
+
if min_size<=circumsphere_diameter<max_size :
|
|
754
|
+
|
|
755
|
+
number+=1
|
|
756
|
+
if not noOutput :
|
|
757
|
+
print(f'\033[1m Generating size is {size[0]:.4f} nm and is equal to dhkl multiplied by {i}.\033[0m ')
|
|
758
|
+
print(f'\033[1m Circumscribed sphere diameter ={circumsphere_diameter:.3f} nm and is in the interval [{min_size},{max_size}].\033[0m')
|
|
759
|
+
# Names of the files
|
|
760
|
+
element = self.cif_name.split()[0]
|
|
761
|
+
structure=self.crystal_type
|
|
762
|
+
if not noOutput :
|
|
763
|
+
print('')
|
|
764
|
+
print(f' Generate NPs laying on one of their surfaces.')
|
|
765
|
+
self.place_NPsurface_on_grid(path,xyz_gz_file, surface_indices,tolerance, TestNP, surface_indices, element, structure, form, nRot, number, noOutput, circumsphere_diameter)
|
|
766
|
+
if not noOutput :
|
|
767
|
+
print(f' Generate NPs laying on one of their edges.')
|
|
768
|
+
self.place_NPedge_on_grid(path, xyz_gz_file, surface_indices,tolerance, TestNP, surface_indices, element, structure, form, nRot, number, noOutput, circumsphere_diameter)
|
|
769
|
+
|
|
770
|
+
else :
|
|
771
|
+
# if min_size>=circumsphere_diameter :
|
|
772
|
+
# if not noOutput :
|
|
773
|
+
# print(f'\033[1m The circumscribed sphere diameter of the NP ={circumsphere_diameter} nm is smaller than the minimal size : {min_size}nm chosen. \033[0m')
|
|
774
|
+
|
|
775
|
+
if circumsphere_diameter>max_size :
|
|
776
|
+
print(f'\033[1m Stopping now: the circumscribed sphere diameter of the NP ={circumsphere_diameter:.3f} nm is not in the interval [{min_size},{max_size}] nm chosen anymore.\033[0m')
|
|
777
|
+
# if not noOutput :
|
|
778
|
+
# print(f'\033[1m The circumscribed sphere diameter of the NP ={circumsphere_diameter} nm is greater than the maximal size : {max_size}nm chosen. \033[0m')
|
|
779
|
+
break
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
# Add regIco
|
|
783
|
+
form = 'regico'
|
|
784
|
+
element = self.cif_name.split()[0]
|
|
785
|
+
|
|
786
|
+
|
|
787
|
+
if self.crystal_type=='fcc' :
|
|
788
|
+
dist= pyNMBu.FindInterAtomicDist(self)# Extract the interatomic distance
|
|
789
|
+
if not noOutput :
|
|
790
|
+
print(f"{bg.LIGHTGREENB}Addinng icosahedron with crystal type {self.crystal_type} and interatomic distance = {dist:.4f} nm.")
|
|
791
|
+
|
|
792
|
+
for i in np.arange(1,1000) :
|
|
793
|
+
index += 1
|
|
794
|
+
if not noOutput :
|
|
795
|
+
print(f'{bg.LIGHTBLUEB} Number of bonds is {i}')
|
|
796
|
+
TestNP2 =pNP.regIco(
|
|
797
|
+
element=element,
|
|
798
|
+
Rnn=dist,
|
|
799
|
+
nShell=i,
|
|
800
|
+
shape='regico',
|
|
801
|
+
postAnalyzis=True,
|
|
802
|
+
aseView=False,
|
|
803
|
+
thresholdCoreSurface=1,
|
|
804
|
+
skipSymmetryAnalyzis=True,
|
|
805
|
+
noOutput= True
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
circumsphere_diameter=TestNP2.radiusCircumscribedSphere()*2*0.1 # Setting a maximal size of NPs : circumscribed sphere diameter
|
|
809
|
+
|
|
810
|
+
if min_size<=circumsphere_diameter<max_size :
|
|
811
|
+
number+=1
|
|
812
|
+
if not noOutput :
|
|
813
|
+
print(f'\033[1m Generating size is {size[0]:.4f} nm.\033[0m ')
|
|
814
|
+
print(f'\033[1m Circumscribed sphere diameter ={circumsphere_diameter:.3f} nm and is in the interval [{min_size},{max_size}].\033[0m')
|
|
815
|
+
# Names of the files
|
|
816
|
+
element = self.cif_name.split()[0]
|
|
817
|
+
structure=self.crystal_type
|
|
818
|
+
if not noOutput :
|
|
819
|
+
print('')
|
|
820
|
+
print(f' Generate NPs laying on one of their surfaces.')
|
|
821
|
+
self.place_NPsurface_on_grid(path,xyz_gz_file, surface_indices,tolerance, TestNP2, surface_indices, element, structure, form, nRot, number, noOutput, circumsphere_diameter)
|
|
822
|
+
if not noOutput :
|
|
823
|
+
print(f' Generate NPs laying on one of their edges.')
|
|
824
|
+
self.place_NPedge_on_grid(path, xyz_gz_file, surface_indices,tolerance, TestNP2, surface_indices, element, structure, form, nRot, number, noOutput, circumsphere_diameter)
|
|
825
|
+
|
|
826
|
+
else :
|
|
827
|
+
# if min_size>=circumsphere_diameter :
|
|
828
|
+
# if not noOutput :
|
|
829
|
+
# print(f'\033[1m The circumscribed sphere diameter of the NP ={circumsphere_diameter} nm is smaller than the minimal size : {min_size}nm chosen. \033[0m')
|
|
830
|
+
if circumsphere_diameter>max_size :
|
|
831
|
+
print(f'\033[1m Stopping now: the circumscribed sphere diameter of the NP ={circumsphere_diameter:.3f} nm is not in the interval [{min_size:.4f},{max_size:.4f}] nm chosen anymore.\033[0m')
|
|
832
|
+
# if not noOutput :
|
|
833
|
+
# print(f'\033[1m The circumscribed sphere diameter of the NP ={circumsphere_diameter} nm is greater than the maximal size : {max_size}nm chosen. \033[0m')
|
|
834
|
+
break
|
|
835
|
+
|
|
836
|
+
elif self.crystal_type=='bcc': # do not make an icosahedron
|
|
837
|
+
# dist= pyNMBu.FindInterAtomicDist(self) # Extract the interatomic distance
|
|
838
|
+
if not noOutput :
|
|
839
|
+
print(f'{bg.LIGHTREDB} No icosahedron for bcc lattice. ')
|
|
840
|
+
|
|
841
|
+
elif self.crystal_type=='hcp': # do not make an icosahedron
|
|
842
|
+
dist= pyNMBu.FindInterAtomicDist(self) # Extract the interatomic distance
|
|
843
|
+
if not noOutput :
|
|
844
|
+
print(f'{bg.LIGHTREDB} No icosahedron for hcp lattice.')
|
|
845
|
+
else :
|
|
846
|
+
dist=None
|
|
847
|
+
if not noOutput :
|
|
848
|
+
print(f'{bg.LIGHTREDB} No interatomic distance found.')
|
|
849
|
+
|
|
850
|
+
# Save XYZ metadata CSV
|
|
851
|
+
if self._xyz_metadata:
|
|
852
|
+
df_xyz_meta = pd.DataFrame(self._xyz_metadata)
|
|
853
|
+
df_xyz_meta.to_csv(f"{path}/xyz_metadata.csv", index=False)
|
|
854
|
+
if not noOutput:
|
|
855
|
+
print(f"[CSV] Saved XYZ metadata: {path}/xyz_metadata.csv")
|
|
856
|
+
|
|
857
|
+
|
|
858
|
+
############################################## Class for HRTEM simulations
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
class CreateHRTEMImage:
|
|
862
|
+
"""
|
|
863
|
+
Simulate High-Resolution Transmission Electron Microscopy (HRTEM) images
|
|
864
|
+
from XYZ atomic structure files using the abTEM multislice framework.
|
|
865
|
+
|
|
866
|
+
This class reads XYZ files produced by ``CreateHRTEMStructure`` (nanoparticle
|
|
867
|
+
on an amorphous-carbon substrate) and generates realistic HRTEM images by
|
|
868
|
+
chaining the following physical steps:
|
|
869
|
+
|
|
870
|
+
**Simulation pipeline (per XYZ file)**
|
|
871
|
+
|
|
872
|
+
1. ``place_atoms_grid`` – Crop the carbon substrate to a thin slab.
|
|
873
|
+
2. ``calculate_potentials`` – Build frozen-phonon potentials (Debye-Waller).
|
|
874
|
+
3. ``create_wave_function`` – Create an incident plane wave.
|
|
875
|
+
4. ``perform_multislice`` – Propagate the wave through the potential slices.
|
|
876
|
+
5. ``calculate_ctf`` – Apply the CTF (Cs, defocus, astigmatism C12/phi12,
|
|
877
|
+
temporal coherence via focal_spread).
|
|
878
|
+
6. ``apply_astigmatism`` – Copy CTF for incoherent imaging.
|
|
879
|
+
7. ``apply_partial_coherence`` – Compute intensity from the ensemble of frozen-phonon
|
|
880
|
+
exit waves convolved with the CTF.
|
|
881
|
+
8. ``apply_poisson_noise`` – Add shot noise at a given electron dose.
|
|
882
|
+
9. ``calculate_mtf`` – Apply the detector Modulation Transfer Function.
|
|
883
|
+
10. ``generate_and_save_image`` – Save the PNG image and a per-image CSV metadata file.
|
|
884
|
+
11. ``generate_mask_image`` – (optional) Save a binary segmentation mask of the NP.
|
|
885
|
+
|
|
886
|
+
**Aberration notation** follows the Krivanek convention used by abTEM:
|
|
887
|
+
|
|
888
|
+
============ ============================================ ==========
|
|
889
|
+
Symbol Physical meaning Unit
|
|
890
|
+
============ ============================================ ==========
|
|
891
|
+
Cs (C30) 3rd-order spherical aberration Angstrom
|
|
892
|
+
C10 Defocus (set to Scherzer by default) Angstrom
|
|
893
|
+
C12 / phi12 2-fold astigmatism amplitude / azimuth Angstrom / rad
|
|
894
|
+
focal_spread Temporal-coherence envelope = Cc * dE / E Angstrom
|
|
895
|
+
============ ============================================ ==========
|
|
896
|
+
|
|
897
|
+
**Outputs**
|
|
898
|
+
|
|
899
|
+
For each input ``{name}.xyz`` the class produces:
|
|
900
|
+
|
|
901
|
+
- ``{name}_{index}.png`` – HRTEM image (grayscale, 512x512 px).
|
|
902
|
+
- ``{name}_{index}_metadata.csv`` – All simulation parameters + NP metadata.
|
|
903
|
+
- ``{name}_{index}_mask.png`` – Binary mask (if ``masking_images=True``).
|
|
904
|
+
|
|
905
|
+
Parameters
|
|
906
|
+
----------
|
|
907
|
+
path_input : str
|
|
908
|
+
Directory containing the input ``.xyz`` files and optionally
|
|
909
|
+
``xyz_metadata.csv`` (produced by ``CreateHRTEMStructure``).
|
|
910
|
+
path_output : str
|
|
911
|
+
Directory where output PNG and CSV files are saved.
|
|
912
|
+
sampling : float, default 0.05
|
|
913
|
+
Grid sampling for each dimension in Ångstrom per grid point.
|
|
914
|
+
Impact on the time computation.
|
|
915
|
+
masking_images : bool, default True
|
|
916
|
+
If True, generate a binary segmentation mask for each image.
|
|
917
|
+
phonon_config : int, default 8
|
|
918
|
+
The number of configurations around the equilibrium position.
|
|
919
|
+
sigmas : float, default 0.1
|
|
920
|
+
The standard deviation of the displacements in Angstrom for frozen phonons.
|
|
921
|
+
Cs_value : float, default -80 (i.e. -8e-6 * 1e10 Angstrom)
|
|
922
|
+
Spherical aberration coefficient C30 in Angstrom.
|
|
923
|
+
Negative for aberration-corrected microscopes.
|
|
924
|
+
C12 : float, default 0
|
|
925
|
+
Two-fold astigmatism amplitude in Angstrom.
|
|
926
|
+
phi12 : float, default 0
|
|
927
|
+
Azimuthal angle of the two-fold astigmatism in radians.
|
|
928
|
+
Cc_value : float, default 1e7 (i.e. 1.0e-3 * 1e10 Angstrom = 1 mm)
|
|
929
|
+
Chromatic aberration coefficient in Angstrom.
|
|
930
|
+
semiangle_cutoff_value : int, default 45
|
|
931
|
+
Objective aperture semiangle cutoff in mrad.
|
|
932
|
+
energy_spread : float, default 0.35
|
|
933
|
+
Energy spread dE of the electron source in eV.
|
|
934
|
+
Combined with Cc and E to compute the focal spread:
|
|
935
|
+
``focal_spread = Cc_value * energy_spread / energy``.
|
|
936
|
+
c1 : float, default -0.6
|
|
937
|
+
MTF parameter – asymptotic contrast floor.
|
|
938
|
+
c2 : float, default 0.1
|
|
939
|
+
MTF parameter – half-power spatial frequency scaling.
|
|
940
|
+
c3 : float, default 1.0
|
|
941
|
+
MTF parameter – roll-off exponent.
|
|
942
|
+
dose_poisson_noise : float, default 1e4
|
|
943
|
+
Electron dose for Poisson shot noise in e-/Angstrom^2.
|
|
944
|
+
slice_thickness : float, default 1
|
|
945
|
+
Thickness of each potential slice for the multislice algorithm
|
|
946
|
+
in Angstrom.
|
|
947
|
+
Smaller values are more accurate but slower.
|
|
948
|
+
noOutput : bool, default True
|
|
949
|
+
Print diagnostic information (defocus, focal_spread, etc.).
|
|
950
|
+
energy : float, default 200e3
|
|
951
|
+
Electron beam energy in eV (200 keV by default).
|
|
952
|
+
substrate_size : list of float, default [10, 10, 10]
|
|
953
|
+
Simulation super-cell size [x, y, z] in nanometers.
|
|
954
|
+
device : str, default 'gpu'
|
|
955
|
+
Computation device: ``'gpu'`` (cupy/CUDA) or ``'cpu'``.
|
|
956
|
+
microscope_step : int, default 1
|
|
957
|
+
Index of the current microscope parameter combination.
|
|
958
|
+
Passed from the outer parameter sweep loop and used in
|
|
959
|
+
output filenames: ``{xyz_name}_{microscope_step:07d}.png``.
|
|
960
|
+
|
|
961
|
+
Attributes
|
|
962
|
+
----------
|
|
963
|
+
focal_spread : float
|
|
964
|
+
Temporal-coherence envelope width computed as
|
|
965
|
+
``Cc_value * energy_spread / energy`` (Angstrom).
|
|
966
|
+
defocus : float
|
|
967
|
+
Scherzer defocus C10 in Angstrom (set after ``calculate_ctf``).
|
|
968
|
+
|
|
969
|
+
Examples
|
|
970
|
+
--------
|
|
971
|
+
>>> CreateHRTEMImage(
|
|
972
|
+
... path_input="output_xyz/",
|
|
973
|
+
... path_output="output_hrtem/",
|
|
974
|
+
... Cs_value=-8e-6 * 1e10,
|
|
975
|
+
... energy=200e3,
|
|
976
|
+
... device="gpu",
|
|
977
|
+
... )
|
|
978
|
+
"""
|
|
979
|
+
|
|
980
|
+
def __init__(self, path_input: str, path_output: str, sampling: float = 0.05, masking_images: bool = True,
|
|
981
|
+
phonon_config: int = 8, Cs_value: float = -8e-6 * 1e10, C12: float = 0,
|
|
982
|
+
phi12: float = 0, Cc_value: float = 1.0e-3 * 1e10,
|
|
983
|
+
semiangle_cutoff_value: int = 45, energy_spread: float = 0.35,
|
|
984
|
+
c1 : float = -0.6, c2 : float = 0.1, c3 : float= 1.0,
|
|
985
|
+
dose_poisson_noise: float = 1e4,
|
|
986
|
+
sigmas: float = 0.1, slice_thickness: float = 1,
|
|
987
|
+
noOutput: bool = True, energy: float = 200e3,
|
|
988
|
+
device: str = 'gpu', microscope_step: int = 1):
|
|
989
|
+
|
|
990
|
+
self.path_input = path_input
|
|
991
|
+
self.path_output = path_output
|
|
992
|
+
abtem.config.set({"device": device, "fft": "fftw"})
|
|
993
|
+
self.masking_images = masking_images
|
|
994
|
+
self.sampling = sampling
|
|
995
|
+
self.phonon_config = phonon_config
|
|
996
|
+
self.Cs_value = Cs_value
|
|
997
|
+
self.C12 = C12 # 2-fold astigmatism amplitude (Å)
|
|
998
|
+
self.phi12 = phi12 # 2-fold astigmatism angle (rad)
|
|
999
|
+
self.Cc_value = Cc_value # chromatic aberration coefficient (Å)
|
|
1000
|
+
self.semiangle_cutoff_value = semiangle_cutoff_value
|
|
1001
|
+
self.energy_spread = energy_spread # energy spread dE (eV)
|
|
1002
|
+
# focal_spread = Cc * dE / E (temporal coherence envelope)
|
|
1003
|
+
self.focal_spread = Cc_value * energy_spread / energy
|
|
1004
|
+
self.noOutput = noOutput
|
|
1005
|
+
self.energy = energy
|
|
1006
|
+
self.c1 = c1
|
|
1007
|
+
self.c2 = c2
|
|
1008
|
+
self.c3 = c3
|
|
1009
|
+
self.dose_poisson_noise = dose_poisson_noise # e⁻/Ų
|
|
1010
|
+
self.sigmas = sigmas # Debye-Waller rms displacement (Å)
|
|
1011
|
+
self.slice_thickness = slice_thickness # multislice slice thickness (Å)
|
|
1012
|
+
self.device = device
|
|
1013
|
+
self.microscope_step = microscope_step
|
|
1014
|
+
self.cluster = None
|
|
1015
|
+
self.atoms = None
|
|
1016
|
+
self.frozen_phonons = None
|
|
1017
|
+
self.potential = None
|
|
1018
|
+
self.wave = None
|
|
1019
|
+
self.exit_wave = None
|
|
1020
|
+
self.ctf = None
|
|
1021
|
+
self.incoherent_ctf = None
|
|
1022
|
+
self.measurement_ensemble = None
|
|
1023
|
+
|
|
1024
|
+
self.hrtem_image()
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def hrtem_image(self):
|
|
1028
|
+
"""Main method to generate the HRTEM image."""
|
|
1029
|
+
|
|
1030
|
+
input_files=Path(self.path_input)
|
|
1031
|
+
|
|
1032
|
+
# Load XYZ metadata (orientation, angles, size) if available
|
|
1033
|
+
xyz_meta_path = input_files / "xyz_metadata.csv"
|
|
1034
|
+
xyz_meta_lookup = {}
|
|
1035
|
+
if xyz_meta_path.exists():
|
|
1036
|
+
df_xyz_meta = pd.read_csv(xyz_meta_path)
|
|
1037
|
+
xyz_meta_lookup = {row["xyz_file"]: row.to_dict() for _, row in df_xyz_meta.iterrows()}
|
|
1038
|
+
|
|
1039
|
+
|
|
1040
|
+
for f in sorted(input_files.glob("*.xyz")): # loop on all the output XYZ files
|
|
1041
|
+
xyz_meta = xyz_meta_lookup.get(f.name, {})
|
|
1042
|
+
self.substrate_size = xyz_meta.get("substrate_size_nm")
|
|
1043
|
+
self.atoms = read(f) # from XYZ files to ASE objects for AbTEM
|
|
1044
|
+
self.place_atoms_grid()
|
|
1045
|
+
self.calculate_potentials()
|
|
1046
|
+
self.create_wave_function()
|
|
1047
|
+
self.perform_multislice()
|
|
1048
|
+
self.calculate_ctf()
|
|
1049
|
+
self.apply_astigmatism()
|
|
1050
|
+
self.apply_partial_coherence()
|
|
1051
|
+
self.apply_poisson_noise()
|
|
1052
|
+
self.calculate_mtf(f, xyz_meta)
|
|
1053
|
+
if self.masking_images:
|
|
1054
|
+
self.generate_mask_image(f)
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def place_atoms_grid(self):
|
|
1058
|
+
"""
|
|
1059
|
+
Function that only takes 2nm of the carbon substrate.
|
|
1060
|
+
"""
|
|
1061
|
+
# Define the z-limits for the substrate slice (z is perpendicular to the substrate plane)
|
|
1062
|
+
z_min = 30
|
|
1063
|
+
z_max = 100
|
|
1064
|
+
substrate_size = self.substrate_size * 10 # Convert nm to Å
|
|
1065
|
+
atoms = self.atoms[[z_min <= atom.position[2] <= z_max for atom in self.atoms]]
|
|
1066
|
+
atoms.set_cell([substrate_size, substrate_size, substrate_size]) # Convert nm to Å
|
|
1067
|
+
atoms.center(axis=2, vacuum=2)
|
|
1068
|
+
self.atoms = atoms
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def display_cluster_views(self):
|
|
1072
|
+
"""Display the cluster from different views."""
|
|
1073
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
|
|
1074
|
+
abtem.show_atoms(self.cluster, plane="xy", ax=ax1, title="Beam view")
|
|
1075
|
+
abtem.show_atoms(self.cluster, plane="yz", ax=ax2, title="Side view")
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def display_combined_views(self):
|
|
1079
|
+
"""Display the combined views of the substrate and cluster."""
|
|
1080
|
+
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
|
|
1081
|
+
abtem.show_atoms(self.atoms, plane="xy", ax=ax1, title="Beam view")
|
|
1082
|
+
abtem.show_atoms(self.atoms, plane="xz", ax=ax2, title="Side view")
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
def calculate_potentials(self):
|
|
1086
|
+
"""Calculate the potentials for the system."""
|
|
1087
|
+
self.frozen_phonons = abtem.FrozenPhonons(self.atoms, self.phonon_config, sigmas=self.sigmas)
|
|
1088
|
+
self.potential = abtem.Potential(
|
|
1089
|
+
self.frozen_phonons,
|
|
1090
|
+
sampling= self.sampling,
|
|
1091
|
+
projection="infinite",
|
|
1092
|
+
slice_thickness=self.slice_thickness,
|
|
1093
|
+
)
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def create_wave_function(self):
|
|
1097
|
+
"""Create the wave function for the simulation."""
|
|
1098
|
+
self.wave = abtem.PlaneWave(energy=self.energy)
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
def perform_multislice(self):
|
|
1102
|
+
"""Perform the multislice calculation."""
|
|
1103
|
+
self.exit_wave = self.wave.multislice(self.potential)
|
|
1104
|
+
self.exit_wave.compute()
|
|
1105
|
+
|
|
1106
|
+
|
|
1107
|
+
def calculate_ctf(self):
|
|
1108
|
+
"""Calculate the contrast transfer function (CTF) with astigmatism and focal spread."""
|
|
1109
|
+
self.ctf = abtem.CTF(
|
|
1110
|
+
Cs=self.Cs_value,
|
|
1111
|
+
energy=self.wave.energy,
|
|
1112
|
+
defocus="scherzer",
|
|
1113
|
+
semiangle_cutoff=self.semiangle_cutoff_value,
|
|
1114
|
+
C12=self.C12,
|
|
1115
|
+
phi12=self.phi12,
|
|
1116
|
+
focal_spread=self.focal_spread,
|
|
1117
|
+
)
|
|
1118
|
+
if self.noOutput == False :
|
|
1119
|
+
print(f"defocus = {self.ctf.defocus:.2f} Å")
|
|
1120
|
+
self.defocus = self.ctf.defocus # C10 in Å
|
|
1121
|
+
if self.C12 != 0:
|
|
1122
|
+
print(f"C12 (astigmatism) = {self.C12:.2f} Å, phi12 = {self.phi12:.4f} rad")
|
|
1123
|
+
if self.focal_spread != 0:
|
|
1124
|
+
print(f"focal_spread = {self.focal_spread:.2f} Å (Cc={self.Cc_value:.2e} Å, dE={self.energy_spread} eV)")
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
def apply_astigmatism(self):
|
|
1128
|
+
"""Copy CTF for incoherent imaging (astigmatism already set in calculate_ctf)."""
|
|
1129
|
+
self.incoherent_ctf = self.ctf.copy()
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
def apply_partial_coherence(self):
|
|
1133
|
+
"""Apply partial coherence to the exit wave."""
|
|
1134
|
+
self.measurement_ensemble = self.exit_wave.apply_ctf(self.incoherent_ctf).intensity()
|
|
1135
|
+
|
|
1136
|
+
|
|
1137
|
+
def apply_poisson_noise(self):
|
|
1138
|
+
measurement = self.measurement_ensemble.mean(0)
|
|
1139
|
+
self.noisy_measurement = measurement.poisson_noise(dose_per_area=self.dose_poisson_noise)
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def calculate_mtf(self, f, xyz_meta=None):
|
|
1143
|
+
"""
|
|
1144
|
+
The Modulation Transfer Function (MTF) is a measure of how well the contrast in an object is transferred to an image by a detector.*
|
|
1145
|
+
It characterizes the fidelity of the spatial frequency content of the object in the resulting image.
|
|
1146
|
+
"""
|
|
1147
|
+
if xyz_meta is None:
|
|
1148
|
+
xyz_meta = {}
|
|
1149
|
+
from numpy.fft import fft2, ifft2, fftshift, ifftshift
|
|
1150
|
+
|
|
1151
|
+
# parameters
|
|
1152
|
+
pixel_size = self.sampling
|
|
1153
|
+
# Compute the spatial frequencies q
|
|
1154
|
+
|
|
1155
|
+
# cmb de angs sont couverts par un pixel
|
|
1156
|
+
q_N = 1 / (2 * pixel_size)
|
|
1157
|
+
# q_N = abtem.transfer.nyquist_sampling(self.semiangle_cutoff_value, self.energy)
|
|
1158
|
+
# print('q_N',q_N)
|
|
1159
|
+
# Compute the spatial frequencies q
|
|
1160
|
+
# noisy_measurement is already averaged over phonon configs (2D)
|
|
1161
|
+
mean_data = self.noisy_measurement.array
|
|
1162
|
+
# mean_data = np.mean(image_data, axis=0)
|
|
1163
|
+
# print('mean_data.shape',mean_data.shape)
|
|
1164
|
+
|
|
1165
|
+
ny, nx = mean_data.shape
|
|
1166
|
+
qx = np.fft.fftfreq(nx, d=pixel_size)
|
|
1167
|
+
qy = np.fft.fftfreq(ny, d=pixel_size)
|
|
1168
|
+
qx, qy = np.meshgrid(qx, qy)
|
|
1169
|
+
q = np.sqrt(qx**2 + qy**2)
|
|
1170
|
+
|
|
1171
|
+
# for c1 in self.c1 :
|
|
1172
|
+
# for c2 in self.c2:
|
|
1173
|
+
# for c3 in self.c3:
|
|
1174
|
+
# Compute the MTF
|
|
1175
|
+
if self.device == 'cpu' :
|
|
1176
|
+
mtf = (1 - self.c1) / (1 + (q / (2 * self.c2 * q_N))**self.c3) + self.c1
|
|
1177
|
+
# Apply MTF
|
|
1178
|
+
image_fft = fft2(mean_data) # fourier transform of the image: image in the frequency space (where low and high frequencies are separated)
|
|
1179
|
+
image_fft_filtered = image_fft * fftshift(mtf) # each spatial frequency is multiplied by its MFT value
|
|
1180
|
+
self.measurement_ensemble = np.real(ifft2(image_fft_filtered)) # back to the real space of the iamge
|
|
1181
|
+
self.generate_and_save_image(f, xyz_meta)
|
|
1182
|
+
if self.device == 'gpu' :
|
|
1183
|
+
import cupy as cp
|
|
1184
|
+
from cupy.fft import fftshift
|
|
1185
|
+
mtf =cp.asarray((1 - self.c1) / (1 + (q / (2 * self.c2 * q_N))**self.c3) + self.c1)
|
|
1186
|
+
# Apply MTF
|
|
1187
|
+
image_fft = fft2(mean_data) # fourier transform of the image: image in the frequency space (where low and high frequencies are separated)
|
|
1188
|
+
image_fft_filtered = image_fft * fftshift(mtf) # each spatial frequency is multiplied by its MFT value
|
|
1189
|
+
self.measurement_ensemble = np.real(ifft2(image_fft_filtered)) # back to the real space of the iamge
|
|
1190
|
+
self.generate_and_save_image(f, xyz_meta)
|
|
1191
|
+
|
|
1192
|
+
def generate_and_save_image(self, f, xyz_meta=None):
|
|
1193
|
+
"""Generate and save the final image."""
|
|
1194
|
+
if xyz_meta is None:
|
|
1195
|
+
xyz_meta = {}
|
|
1196
|
+
|
|
1197
|
+
# 1. The PNG HRTEM image
|
|
1198
|
+
|
|
1199
|
+
self.final_filename = f'{self.path_output}/{f.stem}_{self.microscope_step:07d}'
|
|
1200
|
+
print(f"File is {self.final_filename}.png")
|
|
1201
|
+
plt.figure(figsize=(5.12, 5.12)) # Taille en pouces (ex: 6x6)
|
|
1202
|
+
plt.imshow(self.measurement_ensemble, cmap='gray', origin='lower')
|
|
1203
|
+
plt.axis('off') # Pas d’axes
|
|
1204
|
+
plt.tight_layout(pad=0) # Pas de bordures
|
|
1205
|
+
plt.savefig(f"{self.final_filename}.png", dpi=100, bbox_inches='tight', pad_inches=0)
|
|
1206
|
+
plt.close()
|
|
1207
|
+
|
|
1208
|
+
# 2. The CSV file of metadata
|
|
1209
|
+
|
|
1210
|
+
filename_csv = f'{self.final_filename}_metadata'
|
|
1211
|
+
path_csv = f"{filename_csv}.csv"
|
|
1212
|
+
# Parse element/structure/shape from simplified filename
|
|
1213
|
+
parts = f.stem.split('_')
|
|
1214
|
+
# Flatten metadata for CSV
|
|
1215
|
+
metadata_flat = {
|
|
1216
|
+
"id": f'{f.stem}_{self.microscope_step:07d}',
|
|
1217
|
+
"element": parts[0] if len(parts) > 0 else "",
|
|
1218
|
+
"crystal_structure": parts[1] if len(parts) > 1 else "",
|
|
1219
|
+
"shape": parts[2] if len(parts) > 2 else "",
|
|
1220
|
+
"orientation": xyz_meta.get("orientation", ""),
|
|
1221
|
+
"angle_xy_deg": xyz_meta.get("angle_xy", ""),
|
|
1222
|
+
"angle_tilt_deg": xyz_meta.get("angle_tilt", ""),
|
|
1223
|
+
"circumsphere_diameter_nm": xyz_meta.get("circumsphere_diameter_nm", ""),
|
|
1224
|
+
"sampling_A-1": self.sampling,
|
|
1225
|
+
"phonon_config": self.phonon_config,
|
|
1226
|
+
"Cs_value_A": self.Cs_value,
|
|
1227
|
+
"defocus_C10_A": self.defocus,
|
|
1228
|
+
"C12_A": self.C12,
|
|
1229
|
+
"phi12_rad": self.phi12,
|
|
1230
|
+
"Cc_value_A": self.Cc_value,
|
|
1231
|
+
"energy_spread_eV": self.energy_spread,
|
|
1232
|
+
"focal_spread_A": self.focal_spread,
|
|
1233
|
+
"semiangle_cutoff_value_mrad": self.semiangle_cutoff_value,
|
|
1234
|
+
"energy_eV": self.energy,
|
|
1235
|
+
"mtf_c1":self.c1,
|
|
1236
|
+
"mtf_c2":self.c2,
|
|
1237
|
+
"mtf_c3":self.c3,
|
|
1238
|
+
"dose_poisson_noise_e-A-2": self.dose_poisson_noise,
|
|
1239
|
+
"sigmas_A": self.sigmas,
|
|
1240
|
+
"slice_thickness_A": self.slice_thickness,
|
|
1241
|
+
"substrate_size_nm": self.substrate_size
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
df = pd.DataFrame([metadata_flat])
|
|
1245
|
+
df.to_csv(path_csv, index=False)
|
|
1246
|
+
print(f"[CSV] Saved metadata: {path_csv}")
|
|
1247
|
+
|
|
1248
|
+
print('--------Finished---------------')
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def generate_mask_image(self, f):
|
|
1252
|
+
"""
|
|
1253
|
+
Generate and save the masked HRTEM image for segmentation purposes.
|
|
1254
|
+
The background is set to zero and the nanoparticule to 1.
|
|
1255
|
+
A disk corresponding to the Van der Waals radius is drawn for each atom.
|
|
1256
|
+
"""
|
|
1257
|
+
from ase.data import vdw_radii, covalent_radii
|
|
1258
|
+
from skimage.draw import disk
|
|
1259
|
+
|
|
1260
|
+
# 1. Dimensions of the HRTEM image just generated
|
|
1261
|
+
shape = self.measurement_ensemble.shape
|
|
1262
|
+
pixel_resolution = self.sampling # spatial resolution between pixels (angstroms per pixel)
|
|
1263
|
+
|
|
1264
|
+
# 2. Initialize the mask image with zeros (background)
|
|
1265
|
+
mask_image = np.zeros(shape, dtype=np.uint8)
|
|
1266
|
+
|
|
1267
|
+
# 3. Get the atoms of the nanoparticle (exclude Carbon)
|
|
1268
|
+
np_atoms = [atom for atom in self.atoms if atom.symbol != 'C']
|
|
1269
|
+
|
|
1270
|
+
# 4. Draw a disk for each atom on the mask
|
|
1271
|
+
for atom in np_atoms:
|
|
1272
|
+
# Atom coordinates in Angstroms
|
|
1273
|
+
x, y = atom.position[0], atom.position[1]
|
|
1274
|
+
|
|
1275
|
+
# Convert to pixel coordinates
|
|
1276
|
+
# The image origin (0,0) is top-left, but atom coordinates can be anywhere.
|
|
1277
|
+
# We need to align them with the image grid.
|
|
1278
|
+
px, py = int(y / pixel_resolution), int(x / pixel_resolution)
|
|
1279
|
+
|
|
1280
|
+
# Get Van der Waals radius in Angstroms and convert to pixels
|
|
1281
|
+
radius_ang = vdw_radii[atom.number]
|
|
1282
|
+
if np.isnan(radius_ang): # sometimes not defined
|
|
1283
|
+
radius_ang = covalent_radii[atom.number] # Fallback to covalent radius
|
|
1284
|
+
|
|
1285
|
+
if np.isnan(radius_ang): # sometimes not defined
|
|
1286
|
+
radius_ang = 1.5 # Default value if both are undefined
|
|
1287
|
+
|
|
1288
|
+
radius_px = int(radius_ang / pixel_resolution)
|
|
1289
|
+
|
|
1290
|
+
# Draw a disk for the atom on the mask, checking boundaries
|
|
1291
|
+
rr, cc = disk((py, px), radius_px, shape=shape)
|
|
1292
|
+
mask_image[rr, cc] = 1
|
|
1293
|
+
|
|
1294
|
+
# 5. Save the mask image
|
|
1295
|
+
base_filename = self.final_filename
|
|
1296
|
+
mask_filename = f"{base_filename}_mask.png"
|
|
1297
|
+
|
|
1298
|
+
# Use origin='lower' to match the orientation of the HRTEM image
|
|
1299
|
+
|
|
1300
|
+
plt.figure(figsize=(5.12, 5.12)) # Taille en pouces (ex: 6x6)
|
|
1301
|
+
plt.imshow(mask_image, cmap='gray', origin='lower')
|
|
1302
|
+
plt.axis('off') # Pas d’axes
|
|
1303
|
+
plt.tight_layout(pad=0) # Pas de bordures
|
|
1304
|
+
plt.savefig(mask_filename, dpi=100, bbox_inches='tight', pad_inches=0)
|
|
1305
|
+
plt.close()
|
|
1306
|
+
|
|
1307
|
+
# plt.imsave(mask_filename, mask_image, cmap='gray', origin='lower')
|
|
1308
|
+
print(f"Mask image saved as {mask_filename}")
|
|
1309
|
+
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
|
|
1313
|
+
def create_csv(tem_image_paths, output_csv, noOutput=True):
|
|
1314
|
+
"""
|
|
1315
|
+
Create a single CSV file containing the metadata of all generated PNG files.
|
|
1316
|
+
|
|
1317
|
+
This utility is particularly useful for Machine Learning applications,
|
|
1318
|
+
providing a structured dataset that maps image files to their
|
|
1319
|
+
corresponding physical and structural parameters.
|
|
1320
|
+
|
|
1321
|
+
Args:
|
|
1322
|
+
tem_image_paths (str): The directory path containing the PNG
|
|
1323
|
+
image files to be indexed.
|
|
1324
|
+
output_csv (str): The filename or full path for the resulting
|
|
1325
|
+
CSV metadata file.
|
|
1326
|
+
|
|
1327
|
+
Returns:
|
|
1328
|
+
None: Generates a CSV file at the specified location containing
|
|
1329
|
+
the metadata for all indexed TEM images.
|
|
1330
|
+
"""
|
|
1331
|
+
import csv
|
|
1332
|
+
|
|
1333
|
+
# create a datadrame concatenating all the metadata CSV files
|
|
1334
|
+
all_metadata = []
|
|
1335
|
+
for img_path in Path((tem_image_paths)).iterdir():
|
|
1336
|
+
if img_path.is_file() and img_path.suffix == ".png":
|
|
1337
|
+
metadata_csv = img_path.with_name(img_path.stem + "_metadata.csv")
|
|
1338
|
+
if metadata_csv.exists():
|
|
1339
|
+
df_meta = pd.read_csv(metadata_csv)
|
|
1340
|
+
all_metadata.append(df_meta)
|
|
1341
|
+
else:
|
|
1342
|
+
if not noOutput:
|
|
1343
|
+
print(f"Metadata CSV not found for {img_path}")
|
|
1344
|
+
|
|
1345
|
+
if all_metadata:
|
|
1346
|
+
combined_df = pd.concat(all_metadata, ignore_index=True)
|
|
1347
|
+
combined_df.to_csv(output_csv, index=False)
|
|
1348
|
+
if not noOutput:
|
|
1349
|
+
print(f" CSV containing the metadata created : {output_csv}")
|
|
1350
|
+
else:
|
|
1351
|
+
if not noOutput:
|
|
1352
|
+
print(f"No metadata files found in {tem_image_paths}")
|
|
1353
|
+
|
|
1354
|
+
# save the csv
|
|
1355
|
+
if all_metadata:
|
|
1356
|
+
combined_df.to_csv(output_csv, index=False)
|
|
1357
|
+
|
|
1358
|
+
return combined_df
|
|
1359
|
+
|
|
1360
|
+
|
|
1361
|
+
|