guohanghui commited on
Commit
5cc8e15
·
verified ·
1 Parent(s): 46383d0

Upload 877 files

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +69 -0
  2. Dockerfile +18 -0
  3. README.md +27 -5
  4. app.py +45 -0
  5. atomman/mcp_output/README_MCP.md +77 -0
  6. atomman/mcp_output/analysis.json +871 -0
  7. atomman/mcp_output/diff_report.md +63 -0
  8. atomman/mcp_output/mcp_plugin/__init__.py +0 -0
  9. atomman/mcp_output/mcp_plugin/adapter.py +183 -0
  10. atomman/mcp_output/mcp_plugin/main.py +13 -0
  11. atomman/mcp_output/mcp_plugin/mcp_service.py +165 -0
  12. atomman/mcp_output/requirements.txt +16 -0
  13. atomman/mcp_output/start_mcp.py +30 -0
  14. atomman/mcp_output/workflow_summary.json +207 -0
  15. atomman/source/LICENSE.TXT +17 -0
  16. atomman/source/MANIFEST.in +9 -0
  17. atomman/source/README.rst +112 -0
  18. atomman/source/UPDATES.rst +682 -0
  19. atomman/source/__init__.py +4 -0
  20. atomman/source/atomman/VERSION +1 -0
  21. atomman/source/atomman/__init__.py +45 -0
  22. atomman/source/atomman/cluster/BondAngleMap.py +752 -0
  23. atomman/source/atomman/cluster/__init__.py +3 -0
  24. atomman/source/atomman/core/Atoms.py +572 -0
  25. atomman/source/atomman/core/Box.py +1112 -0
  26. atomman/source/atomman/core/ElasticConstants.py +1037 -0
  27. atomman/source/atomman/core/ElasticConstants2.py +1017 -0
  28. atomman/source/atomman/core/NeighborList.py +263 -0
  29. atomman/source/atomman/core/System.py +1267 -0
  30. atomman/source/atomman/core/__init__.py +14 -0
  31. atomman/source/atomman/core/displacement.py +49 -0
  32. atomman/source/atomman/core/dmag.pxd +3 -0
  33. atomman/source/atomman/core/dmag.pyx +150 -0
  34. atomman/source/atomman/core/dvect.pxd +3 -0
  35. atomman/source/atomman/core/dvect.pyx +153 -0
  36. atomman/source/atomman/core/nlist.pyx +334 -0
  37. atomman/source/atomman/defect/Boundary.py +803 -0
  38. atomman/source/atomman/defect/DifferentialDisplacement.py +733 -0
  39. atomman/source/atomman/defect/Dislocation/__init__.py +535 -0
  40. atomman/source/atomman/defect/Dislocation/_dipole.py +300 -0
  41. atomman/source/atomman/defect/Dislocation/_monopole.py +335 -0
  42. atomman/source/atomman/defect/Dislocation/_periodicarray.py +450 -0
  43. atomman/source/atomman/defect/FreeSurface.py +634 -0
  44. atomman/source/atomman/defect/GRIP.py +545 -0
  45. atomman/source/atomman/defect/GammaSurface.py +1366 -0
  46. atomman/source/atomman/defect/GrainBoundary.py +236 -0
  47. atomman/source/atomman/defect/InterstitialSite.py +168 -0
  48. atomman/source/atomman/defect/IsotropicVolterraDislocation.py +299 -0
  49. atomman/source/atomman/defect/SDVPN.py +1243 -0
  50. atomman/source/atomman/defect/StackingFault.py +667 -0
.gitattributes CHANGED
@@ -33,3 +33,72 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ atomman/source/doc/html/_images/tutorial_4.10_Differential_Displacement_Maps_20_0.png filter=lfs diff=lfs merge=lfs -text
37
+ atomman/source/doc/html/_images/tutorial_4.4._Volterra_dislocation_solutions_28_1.png filter=lfs diff=lfs merge=lfs -text
38
+ atomman/source/doc/html/_images/tutorial_4.4._Volterra_dislocation_solutions_30_1.png filter=lfs diff=lfs merge=lfs -text
39
+ atomman/source/doc/html/_images/tutorial_4.6._Dislocation_analysis_tools_14_0.png filter=lfs diff=lfs merge=lfs -text
40
+ atomman/source/doc/html/_images/tutorial_4.6._Dislocation_analysis_tools_34_0.png filter=lfs diff=lfs merge=lfs -text
41
+ atomman/source/doc/html/_images/tutorial_4.8._Strain_class_24_0.png filter=lfs diff=lfs merge=lfs -text
42
+ atomman/source/doc/html/_images/tutorial_4.8._Strain_class_24_1.png filter=lfs diff=lfs merge=lfs -text
43
+ atomman/source/doc/html/_images/tutorial_4.8._Strain_class_24_2.png filter=lfs diff=lfs merge=lfs -text
44
+ atomman/source/doc/html/_images/tutorial_4.9._Dislocation_configurations_generator_45_0.png filter=lfs diff=lfs merge=lfs -text
45
+ atomman/source/doc/html/_images/tutorial_4.9._Dislocation_configurations_generator_46_0.png filter=lfs diff=lfs merge=lfs -text
46
+ atomman/source/doc/html/_images/tutorial_4.9._Dislocation_configurations_generator_67_0.png filter=lfs diff=lfs merge=lfs -text
47
+ atomman/source/doc/html/_images/tutorial_4.9._Dislocation_configurations_generator_68_0.png filter=lfs diff=lfs merge=lfs -text
48
+ atomman/source/doc/html/.doctrees/atomman.Box.doctree filter=lfs diff=lfs merge=lfs -text
49
+ atomman/source/doc/html/.doctrees/atomman.cluster.BondAngleMap.doctree filter=lfs diff=lfs merge=lfs -text
50
+ atomman/source/doc/html/.doctrees/atomman.defect.DifferentialDisplacement.doctree filter=lfs diff=lfs merge=lfs -text
51
+ atomman/source/doc/html/.doctrees/atomman.defect.Dislocation.doctree filter=lfs diff=lfs merge=lfs -text
52
+ atomman/source/doc/html/.doctrees/atomman.defect.doctree filter=lfs diff=lfs merge=lfs -text
53
+ atomman/source/doc/html/.doctrees/atomman.defect.FreeSurface.doctree filter=lfs diff=lfs merge=lfs -text
54
+ atomman/source/doc/html/.doctrees/atomman.defect.GammaSurface.doctree filter=lfs diff=lfs merge=lfs -text
55
+ atomman/source/doc/html/.doctrees/atomman.defect.IsotropicVolterraDislocation.doctree filter=lfs diff=lfs merge=lfs -text
56
+ atomman/source/doc/html/.doctrees/atomman.defect.SDVPN.doctree filter=lfs diff=lfs merge=lfs -text
57
+ atomman/source/doc/html/.doctrees/atomman.defect.StackingFault.doctree filter=lfs diff=lfs merge=lfs -text
58
+ atomman/source/doc/html/.doctrees/atomman.defect.Stroh.doctree filter=lfs diff=lfs merge=lfs -text
59
+ atomman/source/doc/html/.doctrees/atomman.defect.VolterraDislocation.doctree filter=lfs diff=lfs merge=lfs -text
60
+ atomman/source/doc/html/.doctrees/atomman.ElasticConstants.doctree filter=lfs diff=lfs merge=lfs -text
61
+ atomman/source/doc/html/.doctrees/atomman.library.Database.doctree filter=lfs diff=lfs merge=lfs -text
62
+ atomman/source/doc/html/.doctrees/atomman.mep.BasePath.doctree filter=lfs diff=lfs merge=lfs -text
63
+ atomman/source/doc/html/.doctrees/atomman.plot.doctree filter=lfs diff=lfs merge=lfs -text
64
+ atomman/source/doc/html/.doctrees/atomman.region.doctree filter=lfs diff=lfs merge=lfs -text
65
+ atomman/source/doc/html/.doctrees/atomman.System.doctree filter=lfs diff=lfs merge=lfs -text
66
+ atomman/source/doc/html/.doctrees/atomman.tools.doctree filter=lfs diff=lfs merge=lfs -text
67
+ atomman/source/doc/html/.doctrees/atomman.unitconvert.doctree filter=lfs diff=lfs merge=lfs -text
68
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.10_Differential_Displacement_Maps_20_0.png filter=lfs diff=lfs merge=lfs -text
69
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.4._Volterra_dislocation_solutions_28_1.png filter=lfs diff=lfs merge=lfs -text
70
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.4._Volterra_dislocation_solutions_30_1.png filter=lfs diff=lfs merge=lfs -text
71
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.6._Dislocation_analysis_tools_14_0.png filter=lfs diff=lfs merge=lfs -text
72
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.6._Dislocation_analysis_tools_34_0.png filter=lfs diff=lfs merge=lfs -text
73
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.8._Strain_class_24_0.png filter=lfs diff=lfs merge=lfs -text
74
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.8._Strain_class_24_1.png filter=lfs diff=lfs merge=lfs -text
75
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.8._Strain_class_24_2.png filter=lfs diff=lfs merge=lfs -text
76
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.9._Dislocation_configurations_generator_45_0.png filter=lfs diff=lfs merge=lfs -text
77
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.9._Dislocation_configurations_generator_46_0.png filter=lfs diff=lfs merge=lfs -text
78
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.9._Dislocation_configurations_generator_67_0.png filter=lfs diff=lfs merge=lfs -text
79
+ atomman/source/doc/html/.doctrees/nbsphinx/tutorial_4.9._Dislocation_configurations_generator_68_0.png filter=lfs diff=lfs merge=lfs -text
80
+ atomman/source/doc/html/.doctrees/tutorial/0._Unit_conversions.doctree filter=lfs diff=lfs merge=lfs -text
81
+ atomman/source/doc/html/.doctrees/tutorial/1._Defining_atomic_systems.doctree filter=lfs diff=lfs merge=lfs -text
82
+ atomman/source/doc/html/.doctrees/tutorial/1.1._Box_class.doctree filter=lfs diff=lfs merge=lfs -text
83
+ atomman/source/doc/html/.doctrees/tutorial/1.2._Atoms_class.doctree filter=lfs diff=lfs merge=lfs -text
84
+ atomman/source/doc/html/.doctrees/tutorial/1.3._System_class.doctree filter=lfs diff=lfs merge=lfs -text
85
+ atomman/source/doc/html/.doctrees/tutorial/1.4.1._system_model_conversions.doctree filter=lfs diff=lfs merge=lfs -text
86
+ atomman/source/doc/html/.doctrees/tutorial/1.4.12._DFT_reference_crystal_loading.doctree filter=lfs diff=lfs merge=lfs -text
87
+ atomman/source/doc/html/.doctrees/tutorial/1.4.5._LAMMPS_data_file_conversions.doctree filter=lfs diff=lfs merge=lfs -text
88
+ atomman/source/doc/html/.doctrees/tutorial/1.5._Settings_and_databases.doctree filter=lfs diff=lfs merge=lfs -text
89
+ atomman/source/doc/html/.doctrees/tutorial/1.5._Settings_and_Library_classes.doctree filter=lfs diff=lfs merge=lfs -text
90
+ atomman/source/doc/html/.doctrees/tutorial/2._LAMMPS_functionality.doctree filter=lfs diff=lfs merge=lfs -text
91
+ atomman/source/doc/html/.doctrees/tutorial/2.1._Potential_class.doctree filter=lfs diff=lfs merge=lfs -text
92
+ atomman/source/doc/html/.doctrees/tutorial/3._Basic_support_and_analysis_tools.doctree filter=lfs diff=lfs merge=lfs -text
93
+ atomman/source/doc/html/.doctrees/tutorial/3.1._ElasticConstants_class.doctree filter=lfs diff=lfs merge=lfs -text
94
+ atomman/source/doc/html/.doctrees/tutorial/3.2._NeighborList_class.doctree filter=lfs diff=lfs merge=lfs -text
95
+ atomman/source/doc/html/.doctrees/tutorial/4.1._Point_defect_generation.doctree filter=lfs diff=lfs merge=lfs -text
96
+ atomman/source/doc/html/.doctrees/tutorial/4.2._Free_surface_generator.doctree filter=lfs diff=lfs merge=lfs -text
97
+ atomman/source/doc/html/.doctrees/tutorial/4.3._Stacking_fault_generator.doctree filter=lfs diff=lfs merge=lfs -text
98
+ atomman/source/doc/html/.doctrees/tutorial/4.4._Dislocation_solution_and_generator.doctree filter=lfs diff=lfs merge=lfs -text
99
+ atomman/source/doc/html/.doctrees/tutorial/4.4._Volterra_dislocation_solutions.doctree filter=lfs diff=lfs merge=lfs -text
100
+ atomman/source/doc/html/.doctrees/tutorial/4.5._Gamma_surface_plotting.doctree filter=lfs diff=lfs merge=lfs -text
101
+ atomman/source/doc/html/.doctrees/tutorial/4.6._Dislocation_analysis_tools.doctree filter=lfs diff=lfs merge=lfs -text
102
+ atomman/source/doc/html/.doctrees/tutorial/4.7._Semidiscrete_variational_Peierls-Nabarro_model.doctree filter=lfs diff=lfs merge=lfs -text
103
+ atomman/source/doc/html/.doctrees/tutorial/4.8._Strain_class.doctree filter=lfs diff=lfs merge=lfs -text
104
+ atomman/source/doc/html/.doctrees/tutorial/4.9._Dislocation_configurations_generator.doctree filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10
2
+
3
+ RUN useradd -m -u 1000 user && python -m pip install --upgrade pip
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+ ENV MCP_TRANSPORT=http
14
+ ENV MCP_PORT=7860
15
+
16
+ EXPOSE 7860
17
+
18
+ CMD ["python", "atomman/mcp_output/start_mcp.py"]
README.md CHANGED
@@ -1,10 +1,32 @@
1
  ---
2
- title: Atomman
3
- emoji: 🔥
4
- colorFrom: purple
5
- colorTo: yellow
6
  sdk: docker
 
 
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Atomman MCP
3
+ emoji: 🤖
4
+ colorFrom: blue
5
+ colorTo: purple
6
  sdk: docker
7
+ sdk_version: "4.26.0"
8
+ app_file: app.py
9
  pinned: false
10
  ---
11
 
12
+ # Atomman MCP Service
13
+
14
+ Auto-generated MCP service for atomman.
15
+
16
+ ## Usage
17
+
18
+ ```
19
+ https://None-atomman-mcp.hf.space/mcp
20
+ ```
21
+
22
+ ## Connect with Cursor
23
+
24
+ ```json
25
+ {
26
+ "mcpServers": {
27
+ "atomman": {
28
+ "url": "https://None-atomman-mcp.hf.space/mcp"
29
+ }
30
+ }
31
+ }
32
+ ```
app.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ import os
3
+ import sys
4
+
5
+ mcp_plugin_path = os.path.join(os.path.dirname(__file__), "atomman", "mcp_output", "mcp_plugin")
6
+ sys.path.insert(0, mcp_plugin_path)
7
+
8
+ app = FastAPI(
9
+ title="Atomman MCP Service",
10
+ description="Auto-generated MCP service for atomman",
11
+ version="1.0.0"
12
+ )
13
+
14
+ @app.get("/")
15
+ def root():
16
+ return {
17
+ "service": "Atomman MCP Service",
18
+ "version": "1.0.0",
19
+ "status": "running",
20
+ "transport": os.environ.get("MCP_TRANSPORT", "http")
21
+ }
22
+
23
+ @app.get("/health")
24
+ def health_check():
25
+ return {"status": "healthy", "service": "atomman MCP"}
26
+
27
+ @app.get("/tools")
28
+ def list_tools():
29
+ try:
30
+ from mcp_service import create_app
31
+ mcp_app = create_app()
32
+ tools = []
33
+ for tool_name, tool_func in mcp_app.tools.items():
34
+ tools.append({
35
+ "name": tool_name,
36
+ "description": tool_func.__doc__ or "No description available"
37
+ })
38
+ return {"tools": tools}
39
+ except Exception as e:
40
+ return {"error": f"Failed to load tools: {str(e)}"}
41
+
42
+ if __name__ == "__main__":
43
+ import uvicorn
44
+ port = int(os.environ.get("PORT", 7860))
45
+ uvicorn.run(app, host="0.0.0.0", port=port)
atomman/mcp_output/README_MCP.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Atomman: Atomistic Manipulation Toolkit
2
+
3
+ ## Project Introduction
4
+
5
+ Atomman is a comprehensive toolkit designed for atomistic manipulation and simulation. It provides a robust set of tools for handling atomic structures, simulating dislocations, and interfacing with LAMMPS for potential evaluations. The toolkit is particularly useful for researchers and developers working in materials science and computational physics, offering functionalities such as atomic position management, simulation box transformations, and plotting capabilities using Plotly.
6
+
7
+ ## Installation Method
8
+
9
+ To install Atomman, ensure you have Python installed along with the following dependencies:
10
+
11
+ - Required: numpy, scipy, matplotlib
12
+ - Optional: plotly, ase
13
+
14
+ You can install Atomman and its dependencies using pip:
15
+
16
+ ```
17
+ pip install atomman
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ Here's a quick example to get you started with Atomman:
23
+
24
+ 1. **Handling Atomic Positions:**
25
+
26
+ Use the `Atoms` class to manage atomic positions and velocities.
27
+
28
+ ```
29
+ from atomman import Atoms
30
+
31
+ atoms = Atoms()
32
+ atoms.set_positions([[0, 0, 0], [1, 1, 1]])
33
+ positions = atoms.get_positions()
34
+ ```
35
+
36
+ 2. **Defining Simulation Box:**
37
+
38
+ Use the `Box` class to define and manipulate the simulation box dimensions.
39
+
40
+ ```
41
+ from atomman import Box
42
+
43
+ box = Box()
44
+ box.set_dimensions([10, 10, 10])
45
+ dimensions = box.get_dimensions()
46
+ ```
47
+
48
+ 3. **Plotting Structures:**
49
+
50
+ Plot atomic structures using Plotly.
51
+
52
+ ```
53
+ from atomman.plot import plotly
54
+
55
+ plotly.plot_structure(atoms)
56
+ ```
57
+
58
+ ## Available Tools and Endpoints List
59
+
60
+ - **Atoms (source.atomman.core.Atoms):** Handles atomic positions and velocities.
61
+ - **Box (source.atomman.core.Box):** Defines the simulation box dimensions and transformations.
62
+ - **Dislocation (source.atomman.defect.Dislocation):** Models dislocations and calculates related properties.
63
+ - **Potential (source.atomman.lammps.Potential):** Handles LAMMPS potential files and evaluations.
64
+ - **Plotly (source.atomman.plot.plotly):** Provides plotting capabilities using Plotly.
65
+
66
+ ## Common Issues and Notes
67
+
68
+ - Ensure all required dependencies are installed to avoid import errors.
69
+ - Optional dependencies like Plotly and ASE enhance functionality but are not mandatory.
70
+ - The toolkit's complexity and medium intrusiveness risk suggest careful integration into larger projects.
71
+ - Performance may vary based on the size of the atomic systems being manipulated.
72
+
73
+ ## Reference Links or Documentation
74
+
75
+ For more detailed documentation and examples, visit the [Atomman GitHub Repository](https://github.com/usnistgov/atomman).
76
+
77
+ For further assistance, refer to the official documentation once the repository is indexed for code exploration and search functionality.
atomman/mcp_output/analysis.json ADDED
@@ -0,0 +1,871 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "summary": {
3
+ "repository_url": "https://github.com/usnistgov/atomman",
4
+ "summary": "Imported via zip fallback, file count: 243",
5
+ "file_tree": {
6
+ "LICENSE.TXT": {
7
+ "size": 2752
8
+ },
9
+ "atomman/__init__.py": {
10
+ "size": 1351
11
+ },
12
+ "atomman/cluster/BondAngleMap.py": {
13
+ "size": 30506
14
+ },
15
+ "atomman/cluster/__init__.py": {
16
+ "size": 66
17
+ },
18
+ "atomman/core/Atoms.py": {
19
+ "size": 21541
20
+ },
21
+ "atomman/core/Box.py": {
22
+ "size": 38743
23
+ },
24
+ "atomman/core/ElasticConstants.py": {
25
+ "size": 39453
26
+ },
27
+ "atomman/core/ElasticConstants2.py": {
28
+ "size": 41525
29
+ },
30
+ "atomman/core/NeighborList.py": {
31
+ "size": 9154
32
+ },
33
+ "atomman/core/System.py": {
34
+ "size": 47719
35
+ },
36
+ "atomman/core/__init__.py": {
37
+ "size": 485
38
+ },
39
+ "atomman/core/displacement.py": {
40
+ "size": 1602
41
+ },
42
+ "atomman/defect/Boundary.py": {
43
+ "size": 34846
44
+ },
45
+ "atomman/defect/DifferentialDisplacement.py": {
46
+ "size": 34861
47
+ },
48
+ "atomman/defect/Dislocation/__init__.py": {
49
+ "size": 22706
50
+ },
51
+ "atomman/defect/Dislocation/_dipole.py": {
52
+ "size": 12786
53
+ },
54
+ "atomman/defect/Dislocation/_monopole.py": {
55
+ "size": 13693
56
+ },
57
+ "atomman/defect/Dislocation/_periodicarray.py": {
58
+ "size": 19228
59
+ },
60
+ "atomman/defect/FreeSurface.py": {
61
+ "size": 27413
62
+ },
63
+ "atomman/defect/GRIP.py": {
64
+ "size": 25781
65
+ },
66
+ "atomman/defect/GammaSurface.py": {
67
+ "size": 54638
68
+ },
69
+ "atomman/defect/GrainBoundary.py": {
70
+ "size": 10235
71
+ },
72
+ "atomman/defect/InterstitialSite.py": {
73
+ "size": 5663
74
+ },
75
+ "atomman/defect/IsotropicVolterraDislocation.py": {
76
+ "size": 10971
77
+ },
78
+ "atomman/defect/SDVPN.py": {
79
+ "size": 48201
80
+ },
81
+ "atomman/defect/StackingFault.py": {
82
+ "size": 30033
83
+ },
84
+ "atomman/defect/Stroh.py": {
85
+ "size": 11732
86
+ },
87
+ "atomman/defect/SurfaceEnergyEstimator.py": {
88
+ "size": 7009
89
+ },
90
+ "atomman/defect/Surface_energy_formula.py": {
91
+ "size": 3087
92
+ },
93
+ "atomman/defect/TiltGrainBoundaryHelper.py": {
94
+ "size": 20221
95
+ },
96
+ "atomman/defect/VolterraDislocation.py": {
97
+ "size": 15075
98
+ },
99
+ "atomman/defect/__init__.py": {
100
+ "size": 2157
101
+ },
102
+ "atomman/defect/differential_displacement.py": {
103
+ "size": 14804
104
+ },
105
+ "atomman/defect/dislocation_array.py": {
106
+ "size": 9874
107
+ },
108
+ "atomman/defect/dislocation_dipole_displacement.py": {
109
+ "size": 5391
110
+ },
111
+ "atomman/defect/dislocation_system_basis.py": {
112
+ "size": 8440
113
+ },
114
+ "atomman/defect/dislocation_system_transform.py": {
115
+ "size": 3052
116
+ },
117
+ "atomman/defect/disregistry.py": {
118
+ "size": 4501
119
+ },
120
+ "atomman/defect/free_surface_basis.py": {
121
+ "size": 10698
122
+ },
123
+ "atomman/defect/generator_tools.py": {
124
+ "size": 6322
125
+ },
126
+ "atomman/defect/nye_tensor.py": {
127
+ "size": 6895
128
+ },
129
+ "atomman/defect/nye_tensor_p.py": {
130
+ "size": 1883
131
+ },
132
+ "atomman/defect/pn_arctan_disldensity.py": {
133
+ "size": 4344
134
+ },
135
+ "atomman/defect/pn_arctan_disregistry.py": {
136
+ "size": 4171
137
+ },
138
+ "atomman/defect/point.py": {
139
+ "size": 16357
140
+ },
141
+ "atomman/defect/solve_volterra_dislocation.py": {
142
+ "size": 4283
143
+ },
144
+ "atomman/defect/surface_energy_estimate.py": {
145
+ "size": 3087
146
+ },
147
+ "atomman/dump/__init__.py": {
148
+ "size": 1727
149
+ },
150
+ "atomman/dump/ase_Atoms/__init__.py": {
151
+ "size": 38
152
+ },
153
+ "atomman/dump/ase_Atoms/dump.py": {
154
+ "size": 1963
155
+ },
156
+ "atomman/dump/atom_data/__init__.py": {
157
+ "size": 138
158
+ },
159
+ "atomman/dump/atom_data/atoms_prop_info.py": {
160
+ "size": 14356
161
+ },
162
+ "atomman/dump/atom_data/dump.py": {
163
+ "size": 9032
164
+ },
165
+ "atomman/dump/atom_data/velocities_prop_info.py": {
166
+ "size": 3205
167
+ },
168
+ "atomman/dump/atom_dump/__init__.py": {
169
+ "size": 87
170
+ },
171
+ "atomman/dump/atom_dump/dump.py": {
172
+ "size": 10881
173
+ },
174
+ "atomman/dump/atom_dump/process_prop_info.py": {
175
+ "size": 9444
176
+ },
177
+ "atomman/dump/conventional_to_primitive/__init__.py": {
178
+ "size": 38
179
+ },
180
+ "atomman/dump/conventional_to_primitive/dump.py": {
181
+ "size": 12698
182
+ },
183
+ "atomman/dump/freud/__init__.py": {
184
+ "size": 22
185
+ },
186
+ "atomman/dump/freud/dump.py": {
187
+ "size": 1260
188
+ },
189
+ "atomman/dump/lammps_commands/__init__.py": {
190
+ "size": 22
191
+ },
192
+ "atomman/dump/lammps_commands/dump.py": {
193
+ "size": 7907
194
+ },
195
+ "atomman/dump/neb_replica/__init__.py": {
196
+ "size": 22
197
+ },
198
+ "atomman/dump/neb_replica/dump.py": {
199
+ "size": 2841
200
+ },
201
+ "atomman/dump/pdb/__init__.py": {
202
+ "size": 38
203
+ },
204
+ "atomman/dump/pdb/dump.py": {
205
+ "size": 2620
206
+ },
207
+ "atomman/dump/phonopy_Atoms/__init__.py": {
208
+ "size": 38
209
+ },
210
+ "atomman/dump/phonopy_Atoms/dump.py": {
211
+ "size": 2125
212
+ },
213
+ "atomman/dump/poscar/__init__.py": {
214
+ "size": 38
215
+ },
216
+ "atomman/dump/poscar/dump.py": {
217
+ "size": 3703
218
+ },
219
+ "atomman/dump/primitive_cell/__init__.py": {
220
+ "size": 22
221
+ },
222
+ "atomman/dump/primitive_cell/dump.py": {
223
+ "size": 1659
224
+ },
225
+ "atomman/dump/primitive_to_conventional/__init__.py": {
226
+ "size": 38
227
+ },
228
+ "atomman/dump/primitive_to_conventional/dump.py": {
229
+ "size": 2247
230
+ },
231
+ "atomman/dump/pymatgen_Structure/__init__.py": {
232
+ "size": 38
233
+ },
234
+ "atomman/dump/pymatgen_Structure/dump.py": {
235
+ "size": 1644
236
+ },
237
+ "atomman/dump/spglib_cell/__init__.py": {
238
+ "size": 38
239
+ },
240
+ "atomman/dump/spglib_cell/dump.py": {
241
+ "size": 656
242
+ },
243
+ "atomman/dump/standardize_cell/__init__.py": {
244
+ "size": 39
245
+ },
246
+ "atomman/dump/standardize_cell/dump.py": {
247
+ "size": 2772
248
+ },
249
+ "atomman/dump/system_model/__init__.py": {
250
+ "size": 38
251
+ },
252
+ "atomman/dump/system_model/dump.py": {
253
+ "size": 3743
254
+ },
255
+ "atomman/dump/table/__init__.py": {
256
+ "size": 87
257
+ },
258
+ "atomman/dump/table/dump.py": {
259
+ "size": 5842
260
+ },
261
+ "atomman/dump/table/process_prop_info.py": {
262
+ "size": 5221
263
+ },
264
+ "atomman/lammps/Log.py": {
265
+ "size": 13229
266
+ },
267
+ "atomman/lammps/NEBLog.py": {
268
+ "size": 6561
269
+ },
270
+ "atomman/lammps/Potential.py": {
271
+ "size": 3109
272
+ },
273
+ "atomman/lammps/__init__.py": {
274
+ "size": 838
275
+ },
276
+ "atomman/lammps/checkversion.py": {
277
+ "size": 1432
278
+ },
279
+ "atomman/lammps/normalize.py": {
280
+ "size": 2578
281
+ },
282
+ "atomman/lammps/run.py": {
283
+ "size": 5882
284
+ },
285
+ "atomman/lammps/seed.py": {
286
+ "size": 1171
287
+ },
288
+ "atomman/lammps/style.py": {
289
+ "size": 7053
290
+ },
291
+ "atomman/library/Database/__init__.py": {
292
+ "size": 2692
293
+ },
294
+ "atomman/library/Database/_crystal_prototype.py": {
295
+ "size": 17337
296
+ },
297
+ "atomman/library/Database/_reference_crystal.py": {
298
+ "size": 28410
299
+ },
300
+ "atomman/library/Database/_relaxed_crystal.py": {
301
+ "size": 20326
302
+ },
303
+ "atomman/library/__init__.py": {
304
+ "size": 276
305
+ },
306
+ "atomman/library/load_lammps_potential.py": {
307
+ "size": 6362
308
+ },
309
+ "atomman/library/record/CrystalPrototype.py": {
310
+ "size": 3073
311
+ },
312
+ "atomman/library/record/Dislocation.py": {
313
+ "size": 4207
314
+ },
315
+ "atomman/library/record/FreeSurface.py": {
316
+ "size": 2794
317
+ },
318
+ "atomman/library/record/GrainBoundary.py": {
319
+ "size": 3887
320
+ },
321
+ "atomman/library/record/PointDefect.py": {
322
+ "size": 4179
323
+ },
324
+ "atomman/library/record/ReferenceCrystal.py": {
325
+ "size": 4020
326
+ },
327
+ "atomman/library/record/RelaxedCrystal.py": {
328
+ "size": 6735
329
+ },
330
+ "atomman/library/record/StackingFault.py": {
331
+ "size": 3601
332
+ },
333
+ "atomman/library/record/__init__.py": {
334
+ "size": 769
335
+ },
336
+ "atomman/library/value/MillerValue.py": {
337
+ "size": 4962
338
+ },
339
+ "atomman/library/value/SystemModelValue.py": {
340
+ "size": 2325
341
+ },
342
+ "atomman/library/value/UnitVectorValue.py": {
343
+ "size": 5304
344
+ },
345
+ "atomman/library/value/VectorValue.py": {
346
+ "size": 4768
347
+ },
348
+ "atomman/library/value/__init__.py": {
349
+ "size": 301
350
+ },
351
+ "atomman/library/xsd/__init__.py": {
352
+ "size": 0
353
+ },
354
+ "atomman/library/xsl/__init__.py": {
355
+ "size": 0
356
+ },
357
+ "atomman/load/__init__.py": {
358
+ "size": 1823
359
+ },
360
+ "atomman/load/ase_Atoms/__init__.py": {
361
+ "size": 38
362
+ },
363
+ "atomman/load/ase_Atoms/load.py": {
364
+ "size": 1527
365
+ },
366
+ "atomman/load/atom_data/__init__.py": {
367
+ "size": 138
368
+ },
369
+ "atomman/load/atom_data/atoms_prop_info.py": {
370
+ "size": 14351
371
+ },
372
+ "atomman/load/atom_data/load.py": {
373
+ "size": 13573
374
+ },
375
+ "atomman/load/atom_data/velocities_prop_info.py": {
376
+ "size": 3200
377
+ },
378
+ "atomman/load/atom_dump/__init__.py": {
379
+ "size": 87
380
+ },
381
+ "atomman/load/atom_dump/load.py": {
382
+ "size": 10106
383
+ },
384
+ "atomman/load/atom_dump/process_prop_info.py": {
385
+ "size": 9377
386
+ },
387
+ "atomman/load/cif/__init__.py": {
388
+ "size": 38
389
+ },
390
+ "atomman/load/cif/load.py": {
391
+ "size": 1887
392
+ },
393
+ "atomman/load/crystal/__init__.py": {
394
+ "size": 22
395
+ },
396
+ "atomman/load/crystal/load.py": {
397
+ "size": 6261
398
+ },
399
+ "atomman/load/dft_reference/__init__.py": {
400
+ "size": 22
401
+ },
402
+ "atomman/load/dft_reference/load.py": {
403
+ "size": 2914
404
+ },
405
+ "atomman/load/phonopy_Atoms/__init__.py": {
406
+ "size": 38
407
+ },
408
+ "atomman/load/phonopy_Atoms/load.py": {
409
+ "size": 1641
410
+ },
411
+ "atomman/load/poscar/__init__.py": {
412
+ "size": 38
413
+ },
414
+ "atomman/load/poscar/load.py": {
415
+ "size": 3020
416
+ },
417
+ "atomman/load/prototype/__init__.py": {
418
+ "size": 22
419
+ },
420
+ "atomman/load/prototype/load.py": {
421
+ "size": 9535
422
+ },
423
+ "atomman/load/pymatgen_Structure/__init__.py": {
424
+ "size": 38
425
+ },
426
+ "atomman/load/pymatgen_Structure/load.py": {
427
+ "size": 1532
428
+ },
429
+ "atomman/load/spglib_cell/__init__.py": {
430
+ "size": 38
431
+ },
432
+ "atomman/load/spglib_cell/load.py": {
433
+ "size": 989
434
+ },
435
+ "atomman/load/system_model/__init__.py": {
436
+ "size": 38
437
+ },
438
+ "atomman/load/system_model/load.py": {
439
+ "size": 6126
440
+ },
441
+ "atomman/load/table/__init__.py": {
442
+ "size": 87
443
+ },
444
+ "atomman/load/table/load.py": {
445
+ "size": 5102
446
+ },
447
+ "atomman/load/table/process_prop_info.py": {
448
+ "size": 5390
449
+ },
450
+ "atomman/mep/BasePath.py": {
451
+ "size": 12325
452
+ },
453
+ "atomman/mep/ISMPath.py": {
454
+ "size": 10487
455
+ },
456
+ "atomman/mep/__init__.py": {
457
+ "size": 2103
458
+ },
459
+ "atomman/mep/gradient/__init__.py": {
460
+ "size": 100
461
+ },
462
+ "atomman/mep/gradient/central_difference.py": {
463
+ "size": 1653
464
+ },
465
+ "atomman/mep/integrator/__init__.py": {
466
+ "size": 110
467
+ },
468
+ "atomman/mep/integrator/euler.py": {
469
+ "size": 859
470
+ },
471
+ "atomman/mep/integrator/rungekutta.py": {
472
+ "size": 1095
473
+ },
474
+ "atomman/plot/__init__.py": {
475
+ "size": 385
476
+ },
477
+ "atomman/plot/get_prop_values.py": {
478
+ "size": 2651
479
+ },
480
+ "atomman/plot/interpolate_contour.py": {
481
+ "size": 15813
482
+ },
483
+ "atomman/plot/nglview.py": {
484
+ "size": 3624
485
+ },
486
+ "atomman/plot/nglview_classes.py": {
487
+ "size": 2526
488
+ },
489
+ "atomman/plot/plotly.py": {
490
+ "size": 4126
491
+ },
492
+ "atomman/plot/py3Dmol.py": {
493
+ "size": 4139
494
+ },
495
+ "atomman/plot/values_to_hexcolors.py": {
496
+ "size": 1928
497
+ },
498
+ "atomman/region/Cylinder.py": {
499
+ "size": 4072
500
+ },
501
+ "atomman/region/Plane.py": {
502
+ "size": 5378
503
+ },
504
+ "atomman/region/PlaneSet.py": {
505
+ "size": 2124
506
+ },
507
+ "atomman/region/Shape.py": {
508
+ "size": 1681
509
+ },
510
+ "atomman/region/Sphere.py": {
511
+ "size": 2216
512
+ },
513
+ "atomman/region/__init__.py": {
514
+ "size": 220
515
+ },
516
+ "atomman/thermo/EinsteinSolid.py": {
517
+ "size": 3655
518
+ },
519
+ "atomman/thermo/IdealGas.py": {
520
+ "size": 1998
521
+ },
522
+ "atomman/thermo/RDF.py": {
523
+ "size": 7969
524
+ },
525
+ "atomman/thermo/UhlenbeckFordModel.py": {
526
+ "size": 75553
527
+ },
528
+ "atomman/thermo/__init__.py": {
529
+ "size": 143
530
+ },
531
+ "atomman/tools/__init__.py": {
532
+ "size": 1072
533
+ },
534
+ "atomman/tools/approx_rational.py": {
535
+ "size": 1278
536
+ },
537
+ "atomman/tools/axes_check.py": {
538
+ "size": 1234
539
+ },
540
+ "atomman/tools/boolean.py": {
541
+ "size": 915
542
+ },
543
+ "atomman/tools/compositionstr.py": {
544
+ "size": 1035
545
+ },
546
+ "atomman/tools/crystalsystem.py": {
547
+ "size": 8935
548
+ },
549
+ "atomman/tools/duplicated_allclose.py": {
550
+ "size": 6754
551
+ },
552
+ "atomman/tools/duplicates_allclose.py": {
553
+ "size": 6072
554
+ },
555
+ "atomman/tools/filltemplate.py": {
556
+ "size": 2542
557
+ },
558
+ "atomman/tools/indexstr.py": {
559
+ "size": 1119
560
+ },
561
+ "atomman/tools/miller.py": {
562
+ "size": 22878
563
+ },
564
+ "atomman/tools/vect_angle.py": {
565
+ "size": 1672
566
+ },
567
+ "atomman/unitconvert.py": {
568
+ "size": 6010
569
+ },
570
+ "conda_environment_settings/atomman.environment.yml": {
571
+ "size": 178
572
+ },
573
+ "conda_environment_settings/atomman_3_10.environment.yml": {
574
+ "size": 153
575
+ },
576
+ "conda_environment_settings/atomman_3_8.environment.yml": {
577
+ "size": 151
578
+ },
579
+ "conda_environment_settings/atomman_3_9.environment.yml": {
580
+ "size": 151
581
+ },
582
+ "doc/copytutorial.py": {
583
+ "size": 1907
584
+ },
585
+ "doc/html/_static/_sphinx_javascript_frameworks_compat.js": {
586
+ "size": 4418
587
+ },
588
+ "doc/html/_static/doctools.js": {
589
+ "size": 4472
590
+ },
591
+ "doc/html/_static/documentation_options.js": {
592
+ "size": 330
593
+ },
594
+ "doc/html/_static/jquery-3.4.1.js": {
595
+ "size": 280364
596
+ },
597
+ "doc/html/_static/jquery-3.5.1.js": {
598
+ "size": 287630
599
+ },
600
+ "doc/html/_static/jquery-3.6.0.js": {
601
+ "size": 288580
602
+ },
603
+ "doc/html/_static/jquery.js": {
604
+ "size": 89501
605
+ },
606
+ "doc/html/_static/language_data.js": {
607
+ "size": 4758
608
+ },
609
+ "doc/html/_static/searchtools.js": {
610
+ "size": 18732
611
+ },
612
+ "doc/html/_static/sphinx_highlight.js": {
613
+ "size": 5123
614
+ },
615
+ "doc/html/_static/underscore-1.13.1.js": {
616
+ "size": 68408
617
+ },
618
+ "doc/html/_static/underscore-1.3.1.js": {
619
+ "size": 35168
620
+ },
621
+ "doc/html/_static/underscore.js": {
622
+ "size": 19530
623
+ },
624
+ "doc/html/searchindex.js": {
625
+ "size": 262132
626
+ },
627
+ "doc/source/conf.py": {
628
+ "size": 5930
629
+ },
630
+ "doc/tutorial/files/2008--Mendelev-M-I--Al--A1--Cu--fcc--111sf.json": {
631
+ "size": 135074
632
+ },
633
+ "doc/tutorial/files/potential_LAMMPS_examples/comb-demo--LAMMPS--v2.json": {
634
+ "size": 1259
635
+ },
636
+ "doc/tutorial/files/potential_LAMMPS_examples/coul_long-demo--LAMMPS--v1.json": {
637
+ "size": 732
638
+ },
639
+ "doc/tutorial/files/potential_LAMMPS_examples/eam-demo--LAMMPS--v2.json": {
640
+ "size": 1180
641
+ },
642
+ "doc/tutorial/files/potential_LAMMPS_examples/eam_alloy-demo--LAMMPS--v1.json": {
643
+ "size": 1021
644
+ },
645
+ "doc/tutorial/files/potential_LAMMPS_examples/hybrid-demo--LAMMPS--v3.json": {
646
+ "size": 2289
647
+ },
648
+ "doc/tutorial/files/potential_LAMMPS_examples/lj_cut-demo--LAMMPS--v1.json": {
649
+ "size": 1868
650
+ },
651
+ "doc/tutorial/files/potential_LAMMPS_examples/meam-demo--LAMMPS--v1.json": {
652
+ "size": 1065
653
+ },
654
+ "pyproject.toml": {
655
+ "size": 147
656
+ },
657
+ "requirements.txt": {
658
+ "size": 114
659
+ },
660
+ "setup.py": {
661
+ "size": 1903
662
+ },
663
+ "tests/core/test_Atoms.py": {
664
+ "size": 2449
665
+ },
666
+ "tests/core/test_Box.py": {
667
+ "size": 9217
668
+ },
669
+ "tests/core/test_ElasticConstants.py": {
670
+ "size": 1053
671
+ },
672
+ "tests/core/test_NeighborList_and_nlist.py": {
673
+ "size": 2315
674
+ },
675
+ "tests/core/test_System.py": {
676
+ "size": 133
677
+ },
678
+ "tests/core/test_displacement.py": {
679
+ "size": 707
680
+ },
681
+ "tests/core/test_dmag_and_dvect.py": {
682
+ "size": 1411
683
+ },
684
+ "tests/defect/test_free_surface.py": {
685
+ "size": 7584
686
+ },
687
+ "tests/dump_load/test_atom_data.py": {
688
+ "size": 4885
689
+ },
690
+ "tests/dump_load/test_primitive_conventional.py": {
691
+ "size": 3095
692
+ },
693
+ "tests/plot/test_interpolate.py": {
694
+ "size": 692
695
+ },
696
+ "tests/region/test_plane.py": {
697
+ "size": 957
698
+ },
699
+ "tests/test_dummy.py": {
700
+ "size": 41
701
+ },
702
+ "tests/test_root.py": {
703
+ "size": 350
704
+ },
705
+ "tests/test_unitconvert.py": {
706
+ "size": 1493
707
+ },
708
+ "tests/tools/test_atomic_info.py": {
709
+ "size": 720
710
+ },
711
+ "tests/tools/test_axes_check.py": {
712
+ "size": 1236
713
+ },
714
+ "tests/tools/test_crystalsystem.py": {
715
+ "size": 4175
716
+ },
717
+ "tests/tools/test_duplicates_allclose.py": {
718
+ "size": 8880
719
+ },
720
+ "tests/tools/test_filltemplate.py": {
721
+ "size": 611
722
+ },
723
+ "tests/tools/test_indexstr.py": {
724
+ "size": 617
725
+ },
726
+ "tests/tools/test_miller.py": {
727
+ "size": 8396
728
+ },
729
+ "tests/tools/test_uber_open_rmode.py": {
730
+ "size": 1124
731
+ },
732
+ "tests/tools/test_vect_angle.py": {
733
+ "size": 518
734
+ }
735
+ },
736
+ "processed_by": "zip_fallback",
737
+ "success": true
738
+ },
739
+ "structure": {
740
+ "packages": [
741
+ "source.atomman",
742
+ "source.atomman.cluster",
743
+ "source.atomman.core",
744
+ "source.atomman.defect",
745
+ "source.atomman.dump",
746
+ "source.atomman.lammps",
747
+ "source.atomman.library",
748
+ "source.atomman.load",
749
+ "source.atomman.mep",
750
+ "source.atomman.plot",
751
+ "source.atomman.region",
752
+ "source.atomman.thermo",
753
+ "source.atomman.tools"
754
+ ]
755
+ },
756
+ "dependencies": {
757
+ "has_environment_yml": false,
758
+ "has_requirements_txt": true,
759
+ "pyproject": true,
760
+ "setup_cfg": false,
761
+ "setup_py": true
762
+ },
763
+ "entry_points": {
764
+ "imports": [],
765
+ "cli": [],
766
+ "modules": []
767
+ },
768
+ "llm_analysis": {
769
+ "core_modules": [
770
+ {
771
+ "package": "source.atomman.core",
772
+ "module": "Atoms",
773
+ "functions": [
774
+ "get_positions",
775
+ "set_positions",
776
+ "get_velocities",
777
+ "set_velocities"
778
+ ],
779
+ "classes": [
780
+ "Atoms"
781
+ ],
782
+ "description": "Handles atomic positions and velocities."
783
+ },
784
+ {
785
+ "package": "source.atomman.core",
786
+ "module": "Box",
787
+ "functions": [
788
+ "get_dimensions",
789
+ "set_dimensions"
790
+ ],
791
+ "classes": [
792
+ "Box"
793
+ ],
794
+ "description": "Defines the simulation box dimensions and transformations."
795
+ },
796
+ {
797
+ "package": "source.atomman.defect",
798
+ "module": "Dislocation",
799
+ "functions": [
800
+ "calculate_displacement",
801
+ "get_dislocation_line"
802
+ ],
803
+ "classes": [
804
+ "Dislocation"
805
+ ],
806
+ "description": "Models dislocations and calculates related properties."
807
+ },
808
+ {
809
+ "package": "source.atomman.lammps",
810
+ "module": "Potential",
811
+ "functions": [
812
+ "load_potential",
813
+ "evaluate_potential"
814
+ ],
815
+ "classes": [
816
+ "Potential"
817
+ ],
818
+ "description": "Handles LAMMPS potential files and evaluations."
819
+ },
820
+ {
821
+ "package": "source.atomman.plot",
822
+ "module": "plotly",
823
+ "functions": [
824
+ "plot_structure",
825
+ "plot_displacement"
826
+ ],
827
+ "classes": [],
828
+ "description": "Provides plotting capabilities using Plotly."
829
+ }
830
+ ],
831
+ "cli_commands": [],
832
+ "import_strategy": {
833
+ "primary": "import",
834
+ "fallback": "blackbox",
835
+ "confidence": 0.85
836
+ },
837
+ "dependencies": {
838
+ "required": [
839
+ "numpy",
840
+ "scipy",
841
+ "matplotlib"
842
+ ],
843
+ "optional": [
844
+ "plotly",
845
+ "ase"
846
+ ]
847
+ },
848
+ "risk_assessment": {
849
+ "import_feasibility": 0.8,
850
+ "intrusiveness_risk": "medium",
851
+ "complexity": "medium"
852
+ }
853
+ },
854
+ "deepwiki_analysis": {
855
+ "repo_url": "https://github.com/usnistgov/atomman",
856
+ "repo_name": "atomman",
857
+ "content": "usnistgov/atomman\nAtomistic Manipulation Toolkit\nRepository Not Indexed\nThis repository hasn't been indexed yet. Indexing allows you to explore code structure, find documentation, and understand dependencies.\nIndexing typically takes 2-10 minutes to complete after it starts indexing\nOnce indexed, you'll have full access to code exploration and search functionality",
858
+ "model": "gpt-4o-2024-08-06",
859
+ "source": "selenium",
860
+ "success": true
861
+ },
862
+ "deepwiki_options": {
863
+ "enabled": true,
864
+ "model": "gpt-4o-2024-08-06"
865
+ },
866
+ "risk": {
867
+ "import_feasibility": 0.8,
868
+ "intrusiveness_risk": "medium",
869
+ "complexity": "medium"
870
+ }
871
+ }
atomman/mcp_output/diff_report.md ADDED
@@ -0,0 +1,63 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Atomman Project Difference Report
2
+
3
+ ## Project Overview
4
+
5
+ **Repository:** Atomman
6
+ **Project Type:** Python Library
7
+ **Main Features:** Basic functionality for handling atomic structures and simulations.
8
+ **Report Date:** January 31, 2026
9
+ **Workflow Status:** Success
10
+ **Test Status:** Failed
11
+
12
+ The Atomman project is a Python library designed to facilitate the manipulation and analysis of atomic structures and simulations. It provides tools for creating, modifying, and analyzing atomic configurations, making it a valuable resource for researchers and developers working in materials science and computational physics.
13
+
14
+ ## Difference Analysis
15
+
16
+ ### Summary of Changes
17
+
18
+ - **New Files Added:** 8
19
+ - **Modified Files:** 0
20
+
21
+ The recent update to the Atomman project involved the addition of eight new files. No existing files were modified in this update. The workflow status indicates that the integration process was successful, but the test status shows a failure, suggesting issues with the new additions.
22
+
23
+ ### New Files
24
+
25
+ The eight new files introduced in this update likely contain new features or enhancements to the existing functionality. However, without modifications to existing files, it appears that these additions are standalone components or extensions.
26
+
27
+ ## Technical Analysis
28
+
29
+ ### Intrusiveness
30
+
31
+ The update is classified as non-intrusive, meaning that the new files do not interfere with or alter the existing codebase. This suggests that the new features are designed to be modular and independent.
32
+
33
+ ### Test Failures
34
+
35
+ The test failures indicate that the new files may not be functioning as intended or that they are not fully integrated with the existing system. The specific reasons for the test failures need to be identified and addressed to ensure the stability and reliability of the library.
36
+
37
+ ## Recommendations and Improvements
38
+
39
+ 1. **Investigate Test Failures:** Conduct a thorough analysis of the test results to identify the root causes of the failures. Focus on understanding how the new files interact with the existing system and where the breakdowns occur.
40
+
41
+ 2. **Enhance Testing Coverage:** Ensure that comprehensive test cases are developed for the new files. This includes unit tests, integration tests, and system tests to cover all possible scenarios and edge cases.
42
+
43
+ 3. **Documentation Update:** Update the project documentation to include information about the new features and how they can be utilized. This will help users understand the new capabilities and how to integrate them into their workflows.
44
+
45
+ 4. **Code Review:** Conduct a detailed code review of the new files to ensure they adhere to the project's coding standards and best practices. This can help identify potential issues early and improve code quality.
46
+
47
+ ## Deployment Information
48
+
49
+ The deployment of the new files was successful, as indicated by the workflow status. However, due to the test failures, it is recommended to hold off on deploying these changes to a production environment until the issues are resolved.
50
+
51
+ ## Future Planning
52
+
53
+ 1. **Bug Fixes and Patches:** Prioritize fixing the issues identified in the test failures. Release patches as needed to address these problems promptly.
54
+
55
+ 2. **Feature Expansion:** Once the current issues are resolved, consider expanding the functionality of the new features based on user feedback and project goals.
56
+
57
+ 3. **Community Engagement:** Engage with the user community to gather feedback on the new features and identify areas for improvement or additional functionality.
58
+
59
+ 4. **Regular Updates:** Plan for regular updates to the library to incorporate new features, improvements, and bug fixes, ensuring the project remains relevant and useful to its users.
60
+
61
+ ## Conclusion
62
+
63
+ The recent update to the Atomman project introduces new features through the addition of eight new files. While the integration was successful, test failures highlight the need for further investigation and resolution. By addressing these issues and enhancing testing and documentation, the project can continue to provide valuable tools for its users. Future planning should focus on expanding functionality and engaging with the community to drive the project's development forward.
atomman/mcp_output/mcp_plugin/__init__.py ADDED
File without changes
atomman/mcp_output/mcp_plugin/adapter.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Path settings
5
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
6
+ sys.path.insert(0, source_path)
7
+
8
+ # Import statements
9
+ try:
10
+ from atomman.core import Atoms, Box, ElasticConstants, System
11
+ from atomman.defect import FreeSurface, GrainBoundary
12
+ from atomman.tools import miller
13
+ import numpy as np
14
+ import scipy
15
+ import matplotlib.pyplot as plt
16
+ except ImportError as e:
17
+ print(f"Import error: {e}. Ensure all dependencies are installed.")
18
+ # Fallback mode
19
+ mode = "blackbox"
20
+
21
+ # Adapter class
22
+ class Adapter:
23
+ """
24
+ Adapter class for MCP plugin, providing access to core functionalities
25
+ of the atomman package.
26
+ """
27
+
28
+ def __init__(self):
29
+ self.mode = "import"
30
+ if 'mode' in globals() and mode == "blackbox":
31
+ self.mode = "blackbox"
32
+ print("Running in fallback mode. Some functionalities may be limited.")
33
+
34
+ # Core module methods
35
+ # ------------------------------------------------------------------------
36
+
37
+ def create_atoms_instance(self, *args, **kwargs):
38
+ """
39
+ Create an instance of the Atoms class.
40
+
41
+ Parameters:
42
+ *args: Arguments for Atoms class.
43
+ **kwargs: Keyword arguments for Atoms class.
44
+
45
+ Returns:
46
+ dict: Status and instance of Atoms.
47
+ """
48
+ try:
49
+ atoms_instance = Atoms(*args, **kwargs)
50
+ return {"status": "success", "instance": atoms_instance}
51
+ except Exception as e:
52
+ return {"status": "error", "message": str(e)}
53
+
54
+ def create_box_instance(self, *args, **kwargs):
55
+ """
56
+ Create an instance of the Box class.
57
+
58
+ Parameters:
59
+ *args: Arguments for Box class.
60
+ **kwargs: Keyword arguments for Box class.
61
+
62
+ Returns:
63
+ dict: Status and instance of Box.
64
+ """
65
+ try:
66
+ box_instance = Box(*args, **kwargs)
67
+ return {"status": "success", "instance": box_instance}
68
+ except Exception as e:
69
+ return {"status": "error", "message": str(e)}
70
+
71
+ def create_elastic_constants_instance(self, *args, **kwargs):
72
+ """
73
+ Create an instance of the ElasticConstants class.
74
+
75
+ Parameters:
76
+ *args: Arguments for ElasticConstants class.
77
+ **kwargs: Keyword arguments for ElasticConstants class.
78
+
79
+ Returns:
80
+ dict: Status and instance of ElasticConstants.
81
+ """
82
+ try:
83
+ elastic_constants_instance = ElasticConstants(*args, **kwargs)
84
+ return {"status": "success", "instance": elastic_constants_instance}
85
+ except Exception as e:
86
+ return {"status": "error", "message": str(e)}
87
+
88
+ def create_system_instance(self, *args, **kwargs):
89
+ """
90
+ Create an instance of the System class.
91
+
92
+ Parameters:
93
+ *args: Arguments for System class.
94
+ **kwargs: Keyword arguments for System class.
95
+
96
+ Returns:
97
+ dict: Status and instance of System.
98
+ """
99
+ try:
100
+ system_instance = System(*args, **kwargs)
101
+ return {"status": "success", "instance": system_instance}
102
+ except Exception as e:
103
+ return {"status": "error", "message": str(e)}
104
+
105
+ # Defect module methods
106
+ # ------------------------------------------------------------------------
107
+
108
+ def create_free_surface_instance(self, *args, **kwargs):
109
+ """
110
+ Create an instance of the FreeSurface class.
111
+
112
+ Parameters:
113
+ *args: Arguments for FreeSurface class.
114
+ **kwargs: Keyword arguments for FreeSurface class.
115
+
116
+ Returns:
117
+ dict: Status and instance of FreeSurface.
118
+ """
119
+ try:
120
+ free_surface_instance = FreeSurface(*args, **kwargs)
121
+ return {"status": "success", "instance": free_surface_instance}
122
+ except Exception as e:
123
+ return {"status": "error", "message": str(e)}
124
+
125
+ def create_grain_boundary_instance(self, *args, **kwargs):
126
+ """
127
+ Create an instance of the GrainBoundary class.
128
+
129
+ Parameters:
130
+ *args: Arguments for GrainBoundary class.
131
+ **kwargs: Keyword arguments for GrainBoundary class.
132
+
133
+ Returns:
134
+ dict: Status and instance of GrainBoundary.
135
+ """
136
+ try:
137
+ grain_boundary_instance = GrainBoundary(*args, **kwargs)
138
+ return {"status": "success", "instance": grain_boundary_instance}
139
+ except Exception as e:
140
+ return {"status": "error", "message": str(e)}
141
+
142
+ # Tools module methods
143
+ # ------------------------------------------------------------------------
144
+
145
+ def call_miller_function(self, *args, **kwargs):
146
+ """
147
+ Call the miller function.
148
+
149
+ Parameters:
150
+ *args: Arguments for miller function.
151
+ **kwargs: Keyword arguments for miller function.
152
+
153
+ Returns:
154
+ dict: Status and result of miller function.
155
+ """
156
+ try:
157
+ result = miller(*args, **kwargs)
158
+ return {"status": "success", "result": result}
159
+ except Exception as e:
160
+ return {"status": "error", "message": str(e)}
161
+
162
+ # Utility methods
163
+ # ------------------------------------------------------------------------
164
+
165
+ def plot_data(self, data):
166
+ """
167
+ Plot data using matplotlib.
168
+
169
+ Parameters:
170
+ data: Data to be plotted.
171
+
172
+ Returns:
173
+ dict: Status of the plotting operation.
174
+ """
175
+ try:
176
+ plt.plot(data)
177
+ plt.show()
178
+ return {"status": "success"}
179
+ except Exception as e:
180
+ return {"status": "error", "message": str(e)}
181
+
182
+ # End of Adapter class
183
+ # ------------------------------------------------------------------------
atomman/mcp_output/mcp_plugin/main.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ MCP Service Auto-Wrapper - Auto-generated
3
+ """
4
+ from mcp_service import create_app
5
+
6
+ def main():
7
+ """Main entry point"""
8
+ app = create_app()
9
+ return app
10
+
11
+ if __name__ == "__main__":
12
+ app = main()
13
+ app.run()
atomman/mcp_output/mcp_plugin/mcp_service.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+
4
+ # Add the local source directory to sys.path
5
+ source_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "source")
6
+ if source_path not in sys.path:
7
+ sys.path.insert(0, source_path)
8
+
9
+ from fastmcp import FastMCP
10
+ from atomman.core import Atoms, Box, ElasticConstants, System
11
+ from atomman.defect import FreeSurface, GrainBoundary
12
+ from atomman.tools import miller, vect_angle
13
+
14
+ # Create the FastMCP service application
15
+ mcp = FastMCP("atomman_service")
16
+
17
+ @mcp.tool(name="create_atoms", description="Create an Atoms object")
18
+ def create_atoms(positions: list, symbols: list) -> dict:
19
+ """
20
+ Create an Atoms object.
21
+
22
+ Parameters:
23
+ - positions: List of atomic positions.
24
+ - symbols: List of atomic symbols.
25
+
26
+ Returns:
27
+ - Dictionary with success status and Atoms object.
28
+ """
29
+ try:
30
+ atoms = Atoms(positions=positions, symbols=symbols)
31
+ return {"success": True, "result": atoms}
32
+ except Exception as e:
33
+ return {"success": False, "error": str(e)}
34
+
35
+ @mcp.tool(name="create_box", description="Create a Box object")
36
+ def create_box(vects: list) -> dict:
37
+ """
38
+ Create a Box object.
39
+
40
+ Parameters:
41
+ - vects: List of box vectors.
42
+
43
+ Returns:
44
+ - Dictionary with success status and Box object.
45
+ """
46
+ try:
47
+ box = Box(vects=vects)
48
+ return {"success": True, "result": box}
49
+ except Exception as e:
50
+ return {"success": False, "error": str(e)}
51
+
52
+ @mcp.tool(name="calculate_elastic_constants", description="Calculate elastic constants")
53
+ def calculate_elastic_constants(C: list) -> dict:
54
+ """
55
+ Calculate elastic constants.
56
+
57
+ Parameters:
58
+ - C: List of elastic constants.
59
+
60
+ Returns:
61
+ - Dictionary with success status and ElasticConstants object.
62
+ """
63
+ try:
64
+ elastic_constants = ElasticConstants(C=C)
65
+ return {"success": True, "result": elastic_constants}
66
+ except Exception as e:
67
+ return {"success": False, "error": str(e)}
68
+
69
+ @mcp.tool(name="create_system", description="Create a System object")
70
+ def create_system(atoms: Atoms, box: Box) -> dict:
71
+ """
72
+ Create a System object.
73
+
74
+ Parameters:
75
+ - atoms: Atoms object.
76
+ - box: Box object.
77
+
78
+ Returns:
79
+ - Dictionary with success status and System object.
80
+ """
81
+ try:
82
+ system = System(atoms=atoms, box=box)
83
+ return {"success": True, "result": system}
84
+ except Exception as e:
85
+ return {"success": False, "error": str(e)}
86
+
87
+ @mcp.tool(name="generate_free_surface", description="Generate a free surface")
88
+ def generate_free_surface(system: System, miller_indices: list) -> dict:
89
+ """
90
+ Generate a free surface.
91
+
92
+ Parameters:
93
+ - system: System object.
94
+ - miller_indices: List of Miller indices.
95
+
96
+ Returns:
97
+ - Dictionary with success status and FreeSurface object.
98
+ """
99
+ try:
100
+ surface = FreeSurface(system=system, miller=miller_indices)
101
+ return {"success": True, "result": surface}
102
+ except Exception as e:
103
+ return {"success": False, "error": str(e)}
104
+
105
+ @mcp.tool(name="calculate_grain_boundary", description="Calculate a grain boundary")
106
+ def calculate_grain_boundary(system: System, plane: list) -> dict:
107
+ """
108
+ Calculate a grain boundary.
109
+
110
+ Parameters:
111
+ - system: System object.
112
+ - plane: List defining the grain boundary plane.
113
+
114
+ Returns:
115
+ - Dictionary with success status and GrainBoundary object.
116
+ """
117
+ try:
118
+ boundary = GrainBoundary(system=system, plane=plane)
119
+ return {"success": True, "result": boundary}
120
+ except Exception as e:
121
+ return {"success": False, "error": str(e)}
122
+
123
+ @mcp.tool(name="calculate_miller_indices", description="Calculate Miller indices")
124
+ def calculate_miller_indices(vectors: list) -> dict:
125
+ """
126
+ Calculate Miller indices.
127
+
128
+ Parameters:
129
+ - vectors: List of vectors.
130
+
131
+ Returns:
132
+ - Dictionary with success status and Miller indices.
133
+ """
134
+ try:
135
+ indices = miller(vectors)
136
+ return {"success": True, "result": indices}
137
+ except Exception as e:
138
+ return {"success": False, "error": str(e)}
139
+
140
+ @mcp.tool(name="calculate_vector_angle", description="Calculate angle between vectors")
141
+ def calculate_vector_angle(vector1: list, vector2: list) -> dict:
142
+ """
143
+ Calculate angle between two vectors.
144
+
145
+ Parameters:
146
+ - vector1: First vector.
147
+ - vector2: Second vector.
148
+
149
+ Returns:
150
+ - Dictionary with success status and angle in degrees.
151
+ """
152
+ try:
153
+ angle = vect_angle(vector1, vector2)
154
+ return {"success": True, "result": angle}
155
+ except Exception as e:
156
+ return {"success": False, "error": str(e)}
157
+
158
+ def create_app() -> FastMCP:
159
+ """
160
+ Create and return the FastMCP application instance.
161
+
162
+ Returns:
163
+ - FastMCP instance.
164
+ """
165
+ return mcp
atomman/mcp_output/requirements.txt ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastmcp
2
+ fastapi
3
+ uvicorn[standard]
4
+ pydantic>=2.0.0
5
+ xmltodict
6
+ numericalunits
7
+ DataModelDict
8
+ numpy>=1.15
9
+ scipy
10
+ pandas
11
+ matplotlib
12
+ cython
13
+ requests
14
+ toolz
15
+ potentials==0.3.8
16
+ yabadaba>=0.3.2
atomman/mcp_output/start_mcp.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ """
3
+ MCP Service Startup Entry
4
+ """
5
+ import sys
6
+ import os
7
+
8
+ project_root = os.path.dirname(os.path.abspath(__file__))
9
+ mcp_plugin_dir = os.path.join(project_root, "mcp_plugin")
10
+ if mcp_plugin_dir not in sys.path:
11
+ sys.path.insert(0, mcp_plugin_dir)
12
+
13
+ from mcp_service import create_app
14
+
15
+ def main():
16
+ """Start FastMCP service"""
17
+ app = create_app()
18
+ # Use environment variable to configure port, default 8000
19
+ port = int(os.environ.get("MCP_PORT", "8000"))
20
+
21
+ # Choose transport mode based on environment variable
22
+ transport = os.environ.get("MCP_TRANSPORT", "stdio")
23
+ if transport == "http":
24
+ app.run(transport="http", host="0.0.0.0", port=port)
25
+ else:
26
+ # Default to STDIO mode
27
+ app.run()
28
+
29
+ if __name__ == "__main__":
30
+ main()
atomman/mcp_output/workflow_summary.json ADDED
@@ -0,0 +1,207 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "repository": {
3
+ "name": "atomman",
4
+ "url": "https://github.com/usnistgov/atomman",
5
+ "local_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/atomman",
6
+ "description": "Python library",
7
+ "features": "Basic functionality",
8
+ "tech_stack": "Python",
9
+ "stars": 0,
10
+ "forks": 0,
11
+ "language": "Python",
12
+ "last_updated": "",
13
+ "complexity": "medium",
14
+ "intrusiveness_risk": "medium"
15
+ },
16
+ "execution": {
17
+ "start_time": 1769829596.3015447,
18
+ "end_time": 1769829691.7475183,
19
+ "duration": 95.44597387313843,
20
+ "status": "success",
21
+ "workflow_status": "success",
22
+ "nodes_executed": [
23
+ "download",
24
+ "analysis",
25
+ "env",
26
+ "generate",
27
+ "run",
28
+ "review",
29
+ "finalize"
30
+ ],
31
+ "total_files_processed": 13,
32
+ "environment_type": "unknown",
33
+ "llm_calls": 0,
34
+ "deepwiki_calls": 0
35
+ },
36
+ "tests": {
37
+ "original_project": {
38
+ "passed": false,
39
+ "details": {},
40
+ "test_coverage": "100%",
41
+ "execution_time": 0,
42
+ "test_files": []
43
+ },
44
+ "mcp_plugin": {
45
+ "passed": true,
46
+ "details": {},
47
+ "service_health": "healthy",
48
+ "startup_time": 0,
49
+ "transport_mode": "stdio",
50
+ "fastmcp_version": "unknown",
51
+ "mcp_version": "unknown"
52
+ }
53
+ },
54
+ "analysis": {
55
+ "structure": {
56
+ "packages": [
57
+ "source.atomman",
58
+ "source.atomman.cluster",
59
+ "source.atomman.core",
60
+ "source.atomman.defect",
61
+ "source.atomman.dump",
62
+ "source.atomman.lammps",
63
+ "source.atomman.library",
64
+ "source.atomman.load",
65
+ "source.atomman.mep",
66
+ "source.atomman.plot",
67
+ "source.atomman.region",
68
+ "source.atomman.thermo",
69
+ "source.atomman.tools"
70
+ ]
71
+ },
72
+ "dependencies": {
73
+ "has_environment_yml": false,
74
+ "has_requirements_txt": true,
75
+ "pyproject": true,
76
+ "setup_cfg": false,
77
+ "setup_py": true
78
+ },
79
+ "entry_points": {
80
+ "imports": [],
81
+ "cli": [],
82
+ "modules": []
83
+ },
84
+ "risk_assessment": {
85
+ "import_feasibility": 0.8,
86
+ "intrusiveness_risk": "medium",
87
+ "complexity": "medium"
88
+ },
89
+ "deepwiki_analysis": {
90
+ "repo_url": "https://github.com/usnistgov/atomman",
91
+ "repo_name": "atomman",
92
+ "content": "usnistgov/atomman\nAtomistic Manipulation Toolkit\nRepository Not Indexed\nThis repository hasn't been indexed yet. Indexing allows you to explore code structure, find documentation, and understand dependencies.\nIndexing typically takes 2-10 minutes to complete after it starts indexing\nOnce indexed, you'll have full access to code exploration and search functionality",
93
+ "model": "gpt-4o-2024-08-06",
94
+ "source": "selenium",
95
+ "success": true
96
+ },
97
+ "code_complexity": {
98
+ "cyclomatic_complexity": "medium",
99
+ "cognitive_complexity": "medium",
100
+ "maintainability_index": 75
101
+ },
102
+ "security_analysis": {
103
+ "vulnerabilities_found": 0,
104
+ "security_score": 85,
105
+ "recommendations": []
106
+ }
107
+ },
108
+ "plugin_generation": {
109
+ "files_created": [
110
+ "mcp_output/start_mcp.py",
111
+ "mcp_output/mcp_plugin/__init__.py",
112
+ "mcp_output/mcp_plugin/mcp_service.py",
113
+ "mcp_output/mcp_plugin/adapter.py",
114
+ "mcp_output/mcp_plugin/main.py",
115
+ "mcp_output/requirements.txt",
116
+ "mcp_output/README_MCP.md"
117
+ ],
118
+ "main_entry": "start_mcp.py",
119
+ "requirements": [
120
+ "fastmcp>=0.1.0",
121
+ "pydantic>=2.0.0"
122
+ ],
123
+ "readme_path": "/export/zxcpu1/shiweijie/code/ghh/Code2MCP/workspace/atomman/mcp_output/README_MCP.md",
124
+ "adapter_mode": "import",
125
+ "total_lines_of_code": 0,
126
+ "generated_files_size": 0,
127
+ "tool_endpoints": 0,
128
+ "supported_features": [
129
+ "Basic functionality"
130
+ ],
131
+ "generated_tools": [
132
+ "Basic tools",
133
+ "Health check tools",
134
+ "Version info tools"
135
+ ]
136
+ },
137
+ "code_review": {},
138
+ "errors": [],
139
+ "warnings": [],
140
+ "recommendations": [
141
+ "Improve test coverage by adding more unit tests for uncovered modules",
142
+ "optimize large files by breaking them into smaller",
143
+ "more manageable components",
144
+ "ensure consistent use of docstrings and comments for better code readability",
145
+ "update the documentation to reflect recent changes and improvements",
146
+ "consider indexing the repository for better code exploration and search functionality",
147
+ "review and optimize dependencies to ensure only necessary packages are included",
148
+ "enhance the import strategy to increase feasibility and reduce intrusiveness risk",
149
+ "improve the setup process by including a `setup.cfg` file for better configuration management",
150
+ "streamline the plugin integration process by ensuring all endpoints are thoroughly tested",
151
+ "conduct a performance review to identify and address any bottlenecks or inefficiencies."
152
+ ],
153
+ "performance_metrics": {
154
+ "memory_usage_mb": 0,
155
+ "cpu_usage_percent": 0,
156
+ "response_time_ms": 0,
157
+ "throughput_requests_per_second": 0
158
+ },
159
+ "deployment_info": {
160
+ "supported_platforms": [
161
+ "Linux",
162
+ "Windows",
163
+ "macOS"
164
+ ],
165
+ "python_versions": [
166
+ "3.8",
167
+ "3.9",
168
+ "3.10",
169
+ "3.11",
170
+ "3.12"
171
+ ],
172
+ "deployment_methods": [
173
+ "Docker",
174
+ "pip",
175
+ "conda"
176
+ ],
177
+ "monitoring_support": true,
178
+ "logging_configuration": "structured"
179
+ },
180
+ "execution_analysis": {
181
+ "success_factors": [
182
+ "Successful execution of all workflow nodes",
183
+ "Healthy service status of the MCP plugin"
184
+ ],
185
+ "failure_reasons": [],
186
+ "overall_assessment": "excellent",
187
+ "node_performance": {
188
+ "download_time": "Completed successfully, indicating efficient data retrieval",
189
+ "analysis_time": "Completed successfully, indicating effective code analysis",
190
+ "generation_time": "Completed successfully, indicating efficient code generation",
191
+ "test_time": "Original project tests failed, but MCP plugin tests passed"
192
+ },
193
+ "resource_usage": {
194
+ "memory_efficiency": "No data available for memory usage",
195
+ "cpu_efficiency": "No data available for CPU usage",
196
+ "disk_usage": "Generated files are minimal in size, indicating efficient disk usage"
197
+ }
198
+ },
199
+ "technical_quality": {
200
+ "code_quality_score": 75,
201
+ "architecture_score": 80,
202
+ "performance_score": 70,
203
+ "maintainability_score": 75,
204
+ "security_score": 85,
205
+ "scalability_score": 70
206
+ }
207
+ }
atomman/source/LICENSE.TXT ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Licensing
2
+ =========
3
+
4
+ License Information for NIST data (other than Standard Reference Data (SRD))
5
+
6
+ This data was developed by employees of the National Institute of Standards and Technology (NIST), an agency of the Federal Government. Pursuant to title 15 United States Code Section 105, works of NIST employees are not subject to copyright protection in the United States and are considered to be in the public domain.
7
+
8
+ The data is provided by NIST as a public service and is expressly provided “AS IS.” NIST MAKES NO WARRANTY OF ANY KIND, EXPRESS, IMPLIED OR STATUTORY, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTY OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT AND DATA ACCURACY. NIST does not warrant or make any representations regarding the use of the data or the results thereof, including but not limited to the correctness, accuracy, reliability or usefulness of the data. NIST SHALL NOT BE LIABLE AND YOU HEREBY RELEASE NIST FROM LIABILITY FOR ANY INDIRECT, CONSEQUENTIAL, SPECIAL, OR INCIDENTAL DAMAGES (INCLUDING DAMAGES FOR LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION, AND THE LIKE), WHETHER ARISING IN TORT, CONTRACT, OR OTHERWISE, ARISING FROM OR RELATING TO THE DATA (OR THE USE OF OR INABILITY TO USE THIS DATA), EVEN IF NIST HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
9
+ To the extent that NIST may hold copyright in countries other than the United States, you are hereby granted the non-exclusive irrevocable and unconditional right to print, publish, prepare derivative works and distribute the NIST data, in any medium, or authorize others to do so on your behalf, on a royalty-free basis throughout the World.
10
+
11
+ You may improve, modify, and create derivative works of the data or any portion of the data, and you may copy and distribute such modifications or works. Modified works should carry a notice stating that you changed the data and should note the date and nature of any such change. Please explicitly acknowledge the National Institute of Standards and Technology as the source of the data.
12
+
13
+ Permission to use this data is contingent upon your acceptance of the terms of this agreement and upon your providing appropriate acknowledgments of NIST’s creation of the data.
14
+
15
+ Copyright Protection for NIST Standard Reference Data (SRD)
16
+
17
+ Copyright protection on this compilation of data has been secured by the U.S. Department of Commerce in the United States and in other countries that are parties to the Universal Copyright Convention, pursuant to Section 290(e) of Title 15 of the United States Code. NIST Standard Reference Data (SRD); ©Copyright ©2013 by the U.S. Secretary of Commerce on behalf of the United States of America. All rights reserved.
atomman/source/MANIFEST.in ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ include README.rst
2
+ include atomman/VERSION
3
+ include UPDATES.rst
4
+ include LICENSE.TXT
5
+
6
+ include atomman/core/*.pyx
7
+ include atomman/core/*.pxd
8
+ include atomman/defect/*.pyx
9
+ include atomman/defect/*.pyd
atomman/source/README.rst ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ AtomMan
2
+ =======
3
+
4
+ Atomistic Manipulation Toolkit
5
+
6
+ AtomMan: the Atomistic Manipulation Toolkit is a Python library for
7
+ creating, representing, manipulating, and analyzing large-scale atomic
8
+ systems of atoms. The focus of the package is to facilitate the rapid design
9
+ and development of simulations that are fully documented and easily adaptable
10
+ to new potentials, configurations, etc. The code has no requirements that
11
+ limit which systems it can be used on, i.e. it should work on Linux, Mac and
12
+ Windows computers.
13
+
14
+ Features:
15
+
16
+ 1. Allows for efficient and fast calculations on millions of atoms, each with
17
+ many freely defined per-atom properties.
18
+
19
+ 2. Built-in tools for generating and analyzing crystalline defects, such as
20
+ point defects, stacking faults, and dislocations.
21
+
22
+ 4. Call LAMMPS directly from Python and instantly retrieve the resulting data
23
+ or LAMMPS error statement.
24
+
25
+ 5. Easily convert systems to/from the other Python atomic representations, such
26
+ as ase.Atoms and pymatgen.Structure.
27
+
28
+ 6. Can read and dump crystal structure information from a number of formats,
29
+ such as LAMMPS data and dump files, and POSCAR.
30
+
31
+ 7. Built-in unit conversions.
32
+
33
+ Installation
34
+ ------------
35
+
36
+ The atomman package is compatible with Python 3.7+.
37
+
38
+ The latest release can be installed using pip::
39
+
40
+ pip install atomman
41
+
42
+ or using conda from the conda-forge channel::
43
+
44
+ conda install -c conda-forge atomman
45
+
46
+ For Windows users, it is recommended to use an Anaconda distribution and use
47
+ conda to install numpy, scipy, matplotlib, pandas and cython prior to
48
+ installing atomman.
49
+
50
+ Alternatively, all code and documentation can be downloaded from GitHub.
51
+
52
+ - The stable releases are available at
53
+ `https://github.com/usnistgov/atomman <https://github.com/usnistgov/atomman>`__.
54
+
55
+ - The working development versions are at
56
+ `https://github.com/lmhale99/atomman <https://github.com/lmhale99/atomman>`__.
57
+
58
+ Documentation
59
+ -------------
60
+
61
+ Web-based documentation for the atomman package is available at
62
+ `https://www.ctcms.nist.gov/potentials/atomman <https://www.ctcms.nist.gov/potentials/atomman>`__.
63
+
64
+ Source code for the documentation can be found in the
65
+ `github doc directory <https://github.com/usnistgov/atomman/tree/master/doc/>`__.
66
+ The doc directory contains the information both as the source RestructuredText
67
+ files and as unformatted HTML. If you download a copy, you can view the HTML
68
+ version offline by
69
+
70
+ cd {atomman_path}/doc/html
71
+ python -m http.server
72
+
73
+ Then, opening localhost:8000 in a web browser.
74
+
75
+ The documentation consists of two main components:
76
+
77
+ 1. **Tutorial Jupyter Notebooks:**
78
+ `Online html version <https://www.ctcms.nist.gov/potentials/atomman/tutorial/index.html>`__,
79
+ `Downloadable Notebook version <https://github.com/usnistgov/atomman/tree/master/doc/tutorial>`__.
80
+ The tutorials starting with ##. provide a general overview/example of the
81
+ various capabilities. The tutorials starting with ##.#. give more detailed
82
+ descriptions and list options available to the tools mentioned in the
83
+ overview tutorials.
84
+
85
+ 2. **Code Documentation:**
86
+ `Online html version <https://www.ctcms.nist.gov/potentials/atomman/atomman.html>`__.
87
+ This provides a rendering of the Python docstrings for the included
88
+ functions and classes.
89
+
90
+
91
+ Optional packages
92
+ -----------------
93
+
94
+ This is a list of additional Python packages that are needed for some of the
95
+ optional features of the package.
96
+
97
+ - `diffpy.Structure <http://www.diffpy.org/diffpy.Structure/>`__:
98
+ CIF reader. Required for loading systems from CIF files.
99
+
100
+ - `ase <https://wiki.fysik.dtu.dk/ase/>`__:
101
+ The Atomic Simulation Environment for interacting with small systems
102
+ and DFT calculations. Required for converting to/from ase.Atoms objects.
103
+
104
+ - `pymatgen <http://pymatgen.org/>`__:
105
+ The Python Materials Genomics package used by the Materials
106
+ Project for DFT calculations. Required for converting to/from
107
+ pymatgen.Structure objects.
108
+
109
+ - `spglib <https://atztogo.github.io/spglib/python-spglib.html>`__:
110
+ A Python interface to the spglib spacegroup analysis code. spglib
111
+ can be used to analyze and determine the spacegroup for an atomic system.
112
+ Required for converting to/from spglib.cell objects.
atomman/source/UPDATES.rst ADDED
@@ -0,0 +1,682 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Updates
2
+ =======
3
+
4
+ Version 1.5.2
5
+ -------------
6
+
7
+ - **atomman.ElasticConstants2** added as an alternative. This likely
8
+ behaves the same as the original, but with improvements in representations
9
+ of the various crystal families. Separate for now as I haven't had the
10
+ chance to fully verify it.
11
+
12
+ - **atomman.library.Database.download_all() no longer downloads relaxed_crystal
13
+ records. There are too many for this method to be practical.
14
+
15
+ - **atomman.lammps.run** now reads errors from log files if screen = False.
16
+ This should hopefully avoid LAMMPSErrors that have no message.
17
+
18
+ - bug fixes for atomman.Box.position_cartesian_to_relative(),
19
+ atomman.library.Database.get_relaxed_crystal(), atomman.defect.GrainBoundary,
20
+ and atomman.defect.TiltGrainBoundaryHelper.
21
+
22
+ Version 1.5.1
23
+ -------------
24
+
25
+ - **atomman.unitconvert** has been updated to use yabadaba.UnitConverter
26
+ ensuring full compatibility between unit conversions with the two packages.
27
+ Despite the code changing, atomman.unitconvert should still behave
28
+ identically to before. **WARNING**: make sure yabadaba is at least version
29
+ 0.3.2 to fix a bug associated this change!!!
30
+
31
+ - **atomman.defect.GRIP** added that manages inputs for the grand-canonical
32
+ interface predictor (GRIP) algorithm for grain boundary generation and
33
+ relaxation.
34
+
35
+ - **atomman.defect.GrainBoundary.from_model** class method is added that reads
36
+ in grain boundary configuration information from a grain_boundary record.
37
+
38
+ - **atomman.defect.GrainBoundary.dlat** method added that computes the lattice
39
+ thickness for the grain boundary interface, which is used by GRIP.
40
+
41
+ - Miller vector support added to **atomman.library.record.GrainBoundary**.
42
+
43
+ - Some code cleanup and reorganization in **atomman.defect.Boundary.dlat**.
44
+
45
+ - **atomman.Box.d_hkl** method added for computing interplanar spacings.
46
+
47
+ - **atomman.mep.ISMPath** updated to use tqdm for the calculation status
48
+ progress rather than print statements.
49
+
50
+ - **atomman.defect.InterstitialSite** added, which uses Voronoi analysis to
51
+ identify interstitial sites in a given system.
52
+
53
+ - **atomman.lammps.newseed** method added that generates a new
54
+ LAMMPS-compatible random number seed. **atomman.lammps.seed** method added
55
+ that tests if a given int can be used as a LAMMPS random number seed.
56
+
57
+ - **atomman.lammps.run** tinkered with again related to trying to get the
58
+ LAMMPS errors to be passed to python in an informative way.
59
+
60
+ - Minor doc and docstring updates for typos and syntax warnings.
61
+
62
+ Version 1.5.0
63
+ -------------
64
+
65
+ - Overhaul of the Record objects with the new yabadaba version that greatly
66
+ reduces the code and better generalizes record interactions.
67
+
68
+ - Fixes to support numpy 2.0.
69
+
70
+ - **atomman.lammps.run** now has a partition parameter to support multi-replica
71
+ simulations, like NEB and temperature-accelerated dynamics. Updates (again)
72
+ to hopefully print better error messages extracted from LAMMPS runs.
73
+
74
+ - **atomman.defect.SurfaceEnergyEstimator** and
75
+ **atomman.defect.surface_energy_estimate()** added that provide quick
76
+ estimates of high index free surface energies based on a few low index values.
77
+ The former is a class for performing the calculations, and the latter a
78
+ function.
79
+
80
+ - Bug fix for **ase_Atoms** dump to use scaled positions instead of absolute to
81
+ ensure box and atoms remain aligned with the loss of box origin control.
82
+
83
+ - **atomman.tools.miller.tostring** method added for generating Miller indices
84
+ strings for planes and vectors. This serves as the inverse function of
85
+ fromstring.
86
+
87
+ - **standardize_cell** dump style added that serves as a wrapper around the
88
+ more generalized spglib.standardize_cell() method for identifying and
89
+ normalizing systems to their corresponding unit cells.
90
+
91
+ - **Database.fetch_mp_crystal(s)** have been updated for the new Materials
92
+ Project APIs.
93
+
94
+ Version 1.4.11
95
+ --------------
96
+
97
+ - The structure generation methods of **atomman.defect.Dislocation** now all
98
+ have a center parameter that allows for the dislocation's position to be
99
+ shifted. This is useful for setting up NEB runs.
100
+
101
+ - **atomman.lammps.run** now supports lammps_command paths that contain spaces.
102
+
103
+ - **atomman.defect.Boundary** has been further improved and some supporting
104
+ tools developed. Not fully finalized yet.
105
+
106
+ - **neb_replica** dump style added that creates the atomic configurations used
107
+ by LAMMPS for defining the final (and intermediate) replicas.
108
+
109
+ - More tools and operations related to Miller crystal vectors and planes have
110
+ been added. These should make it possible for future revisions of the
111
+ defect generators to be simplified and made more uniform.
112
+
113
+ - Record classes have been updated to support a URL field for assigning
114
+ persistent identification (PID) values once uploaded to a CDCS database.
115
+
116
+ Version 1.4.10
117
+ --------------
118
+
119
+ - **atomman.lammps.Log** now has more options during flatten allowing for only
120
+ a subset of simulation runs to be merged together. Bug fix related to type
121
+ identification during flatten.
122
+
123
+ - **pdb** dump style for protein database file format. Mostly useful for the
124
+ plotting tools that natively interpret this format.
125
+
126
+ - Transformations added to support hexagonal conventional cells of trigonal
127
+ systems. These 't1' and 't2' options are now available in
128
+ **atomman.tools.miller.vector_conventional_to_primitive**,
129
+ **atomman.tools.miller.vector_primitive_to_conventional**,
130
+ and the **conventional_to_primitive** and **primitive_to_conventional** dump
131
+ styles.
132
+
133
+ - **atomman.tools.miller.fromstring** method added for interpreting formatted
134
+ string representations of Miller vectors and planes as numpy arrays.
135
+
136
+ - From **atomman.defect**, **FreeSurface**, **StackingFault** and
137
+ **Dislocation** now have fromrecord() and fromdatabase() methods. The
138
+ fromrecord() methods read in parameters from an associated defect parameter
139
+ set record, and fromdatabase() allows for a matching defect parameter set
140
+ record to be fetched from a database.
141
+
142
+ - **atomman.defect.Dislocation** updated to recognize Miller-Bravais 4 index
143
+ vectors and planes.
144
+
145
+ - All Record classes now have a "database" attribute that sets a default
146
+ database to associate with the record. This makes it possible for the Record
147
+ classes to have methods that fetch additional content from the database as
148
+ needed.
149
+
150
+ - **atomman.defect.Boundary** class added for generating phase boundary and
151
+ grain boundary systems. The basic core of the generator is there and works
152
+ for cubic systems. To be improved in the next few versions as time allows.
153
+
154
+ - **atomman.plot** has new methods to allow for interactive 3D plots when
155
+ working in a Jupyter environment. Substantial changes to these plotting
156
+ tools likely in subsequent versions of atomman.
157
+
158
+ Version 1.4.9
159
+ -------------
160
+
161
+ - **atomman.thermo.RDF** updates to support more general use.
162
+
163
+ - Handling of record queries updated to be consistent with yabadaba 0.2.0.
164
+
165
+ Version 1.4.8
166
+ -------------
167
+
168
+ - **atomman.thermo** module added that provides tools and reference models
169
+ related to thermodynamic calculations.
170
+
171
+ - **atomman.tools.vect_angle** updated to allow for comparisons of multiple
172
+ vector values at once.
173
+
174
+ - **conventional_to_primitive** and **primitive_to_conventional** dump styles
175
+ added that convert between standard conventional and primitive unit cell
176
+ settings. These were created to make it easy to generate compatible cells
177
+ in the two settings for performing crystal and lattice vector operations.
178
+
179
+ - **atomman.tools.miller.vector_conventional_to_primitive** and
180
+ **atomman.tools.miller.vector_primitive_to_conventional** transformation
181
+ operations for face-centered basis settings have been changed to coincide
182
+ with the new dump methods above.
183
+
184
+ - **atomman.defect.VolterraDislocation** classes now support specifying the m
185
+ and n dislocation axes using str 'x', 'y', 'z' values. The dislocation
186
+ transformation basis code is now integrated into the class as well.
187
+
188
+ - **atomman.defect.Dislocation** A new init parameter ucell_setting allows for
189
+ the lattice setting of the ucell to be given so that all lattice vectors can
190
+ be used and explored rather than just integer crystal vectors relative to
191
+ ucell. Also, a dipole method has been added that allows for the generation
192
+ of stable dislocation dipole atomic configurations.
193
+
194
+ - **atomman.defect.Strain** bug fix related to numpy changing how irregular
195
+ arrays are represented.
196
+
197
+ - **atomman.plot.interpolate_contour** figsize option fixed and improved.
198
+
199
+ Version 1.4.7
200
+ -------------
201
+
202
+ - **atomman.load** Import of load styles is now fully modular. Note that as a
203
+ result of this, the individual **load_{style}** function calls have been
204
+ removed.
205
+
206
+ - **atomman.load** and **atomman.dump** Fix to make the individual load
207
+ and dump styles optional dependent on any additional package requirements.
208
+ Versions 1.4.4-1.4.6 accidentally required that these optional packages be
209
+ installed.
210
+
211
+ - **atomman.lammps.Log.flatten** Fix for a rare case associated with stopped
212
+ LAMMPS simulations and restarts that occasionally resulted in columns
213
+ inadvertently being interpreted as str values instead of float values.
214
+
215
+ Version 1.4.6
216
+ -------------
217
+
218
+ - Import of dump styles is now fully modular and delayed. This allows for
219
+ new styles to be introduced that have additional package requirements without
220
+ breaking all of atomman. The delayed loading also makes it possible for dump
221
+ methods to call other dump or load methods without import errors. Note that
222
+ as a result of this, the individual **dump_{style}** function calls have been
223
+ removed.
224
+
225
+ - **atomman.dump.primitive_cell** now works properly because of the above.
226
+
227
+ - Internal use of **atomman.tools.crystalsystem** functions changed to use the
228
+ corresponding **atomman.Box** methods introduced in 1.4.4. instead.
229
+
230
+ - XSL and XSD files added for the defined record styles to better support
231
+ integration of the reference records with potentials.nist.gov.
232
+
233
+
234
+ Version 1.4.5
235
+ -------------
236
+
237
+ - **atomman.defect.FreeSurface** now has a unique_shifts() method that uses
238
+ crystal symmetry to filter out most symmetrically equivalent termination
239
+ planes.
240
+
241
+ - **atomman.dump.primitive_cell** has been added that uses spglib to take an
242
+ atomic system and return a new system corresponding to the identified
243
+ primitive unit cell.
244
+
245
+ - **atomman.dump.phonopy_Atoms** is updated for newer versions of phonopy.
246
+
247
+ - **strain** methods have been added to **atomman.defect.VolterraDislocation**
248
+ and its subclasses to provide the strain associated with the dislocation
249
+ solutions. Stress and displacement methods in **atomman.defect.Stroh** have
250
+ been adjusted to improve calculation speed.
251
+
252
+ - **atomman.defect.pn_arctan_disldensity** added and
253
+ **atomman.defect.pn_arctan_disregistry** updated for consistency and new
254
+ options. These give classic Peierls-Nabarro dislocation width models.
255
+
256
+ - **load_table** method has been added to **atomman.cluster.BondAngleMap**
257
+ allowing for the data generated by save_table to be read back in.
258
+
259
+ Version 1.4.4
260
+ -------------
261
+
262
+ - New methods added to **atomman.Box**
263
+
264
+ - **reciprocal_vects** method added that computes the reciprocal lattice
265
+ vectors associated with the Box's vectors.
266
+
267
+ - **vector_crystal_to_cartesian** and **plane_crystal_to_cartesian** from
268
+ **atomman.tools.miller** have been added as Box methods.
269
+
270
+ - **position_relative_to_cartesian** replaces **atomman.System.unscale** and
271
+ **position_cartesian_to_relative** replaces **atomman.System.scale** as the
272
+ new operations are better named and only relate to Box information.
273
+
274
+ - **identifyfamily**, **iscubic**, **ishexagonal**, **istetragonal**,
275
+ **isrhombohedral**, **isorthorhombic**, **ismonoclinic** and
276
+ **ismonoclinic** methods added from **atomman.tools**.
277
+
278
+ - **atomman.region.Plane** has new methods **operate**, **__eq__** and
279
+ **isclose** for transforming and comparing Planes.
280
+
281
+ - **atomman.plot.interpolate_contour** reworked to allow
282
+ matplotlib.pyplot.axes to be passed through allowing for the color contour
283
+ plots to be added on top of existing plots. Options also added to turn off
284
+ features.
285
+
286
+ - **atomman.defect.DifferentialDisplacement** has new plot_with_nye method that
287
+ overlays Nye tensor color contours with the differential displacement plots.
288
+
289
+ - Overhaul of **atomman.library** operations reflecting that underlying
290
+ database handling is now branched off into the separate yabadaba package.
291
+
292
+ - **Dislocation**, **FreeSurface**, **PointDefect** and **StackingFault**
293
+ Record classes related to defect parameter sets have been moved from iprPy
294
+ to **atomman.library.record** This is to support future updates where these
295
+ parameter sets can be directly passed to the defect generator classes.
296
+
297
+ - Typing hints added to all of atomman's code.
298
+
299
+ Version 1.4.3
300
+ -------------
301
+
302
+ - **atomman.library.Database** query options better ordered and default values
303
+ updated. retrieve methods added to allow for database records to be copied
304
+ to local files.
305
+
306
+ - Bug fix for composition queries of relaxed and reference crystal records.
307
+
308
+ - Updates for KIM model handling due to updates with the potentials package.
309
+
310
+
311
+ Version 1.4.2
312
+ -------------
313
+
314
+ - **atomman.dump.pymatgen_Structure** updated for new pymatgen versions.
315
+
316
+ - **atomman.defect.DifferentialDisplacement** bug fix related to handling
317
+ the atomcolor and atomcmap parameters.
318
+
319
+ - **atomman.tools** now imports aslist, iaslist, screen_input, uber_open_rmode,
320
+ and atomic_info from potentials to remove duplicate code.
321
+
322
+ - **atomman.library** various updates related to keeping record handling
323
+ consistent with updates in potentials version 0.3.1.
324
+
325
+ Version 1.4.1
326
+ -------------
327
+
328
+ - **atomman.lammps.Log** bug fix for properly reading performance data
329
+ for restart runs.
330
+
331
+ Version 1.4.0
332
+ -------------
333
+
334
+ - **atomman.library** and **atomman.settings** modules updated to reflect
335
+ the reworked potentials package version 0.3.0.
336
+
337
+ - **atomman.load_lammps_potential** and **atomman.load** options 'prototype'
338
+ and 'crystal' updated for the new library module. load style
339
+ 'dft_reference' added.
340
+
341
+ - **atomman.lammps.Potential** now is a function that returns either a
342
+ potentials.record.PotentialLAMMPS or potentials.record.PotentialLAMMPSKIM
343
+ object.
344
+
345
+ - **atomman.lammps.run** now has options for passing string input scripts
346
+ rather than reading from files, and for turning off log file output.
347
+ **atomman.lammps.checkversion** simplified due to the changes to run.
348
+
349
+ - **atomman.cluster.BondAngleMap** added for characterizing the three-body
350
+ interactions as predicted by interatomic potentials.
351
+
352
+ Version 1.3.7
353
+ -------------
354
+
355
+ - **atomman.dump.atom_data** bug fix for kim model potentials (now they work).
356
+
357
+ - **atomman.lammps.Log** now captures performance output. A Simulation class
358
+ is added to better represent each run/simulation. The flatten method is
359
+ updated to return a new Simulation rather than overwriting the current data.
360
+ New 'all' style added to flatten that will merge all runs without filtering
361
+ out duplicate timesteps.
362
+
363
+ - **atomman.defect.differential_displacement** option added to pass an existing
364
+ matplotlib axes object to plot on rather than generating a new figure. This
365
+ allows for subplots to be constructed.
366
+
367
+ - **atomman.defect.DifferentialDisplacement** option added to pass an existing
368
+ matplotlib axes object to plot on rather than generating a new figure. This
369
+ allows for subplots to be constructed.
370
+
371
+ - **atomman.mep** subpackage added for performing minimum energy pathway
372
+ calculations. The contained Path classes represent an energy path and have
373
+ built-in iteration methods. The ISMPath uses the improved string method.
374
+
375
+ **atomman.defect.GammaSurface** updated with path and build_path methods
376
+ that help build mep Path objects for the GammaSurface.
377
+
378
+ **atomman.defect.Strain** class added that improves upon the nye_tensor
379
+ function. The new class uses Cython for roughly a 2X speedup and is
380
+ designed to be easier to use.
381
+
382
+ **atomman.defect.SDVPN** The sign of tau used by stress_energy with
383
+ fullstress=False is flipped to correspond to the behavior of
384
+ stress_energy with fullstress=True. New parameter added allowing for
385
+ additional kwargs to be passed to the underlying scipy.optimize.minimize().
386
+
387
+ Version 1.3.6
388
+ -------------
389
+
390
+ - **atomman.tools.atomic_info** updated for recently assigned element names
391
+ and to be more lenient for isotopes.
392
+
393
+ - **atomman.dump.atom_data** updated to support using kim commands for kim
394
+ model potentials.
395
+
396
+ - **atomman.dump.lammps_commands** added - NOT DEBUGGED FOR
397
+ NON-CUBIC/ORTHORHOMBIC SYSTEMS!
398
+
399
+ Version 1.3.5
400
+ -------------
401
+
402
+ - **atomman.defect.GammaSurface** updates and fixes related to the units
403
+ parameters for the plotting methods.
404
+
405
+ - **atomman.defect.SDVPN** bug fixes related to model() generation, loading,
406
+ and the units parameters for the plotting methods.
407
+
408
+ - **atomman.Settings** is now a renaming/import of potentials.Settings.
409
+
410
+ Version 1.3.4
411
+ -------------
412
+
413
+ - **atomman.defect.Dislocation** class added that handles the generation of
414
+ dislocation monopole and periodic array of dislocation atomic configurations
415
+ in a more user-friendly interface than the previous functions.
416
+
417
+ - **atomman.region.PlaneSet** class added that allows for a region/shape to be
418
+ defined using a list of planes. This allows for the construction of
419
+ multi-faceted and/or open-ended shapes.
420
+
421
+ - **atomman.Box.planes** changed so that the order of the planes returned is
422
+ consistent with the underlying indices.
423
+
424
+ - **atomman.build_lammps_potential** inherited from potentials package.
425
+
426
+ Version 1.3.3
427
+ -------------
428
+
429
+ - **atomman.Settings** class added that inherits from the corresponding class
430
+ in the potentials package. This makes it possible for atomman to access the
431
+ same local directory of records as the potentials package.
432
+
433
+ - **atomman.library** module added that extends the corresponding module from
434
+ the potentials package to include support for crystal_prototype and
435
+ relaxed_crystal records.
436
+
437
+ - **atomman.load_lammps_potential** added that loads LAMMPS potential
438
+ information and downloads parameter files from the NIST Interatomic
439
+ Potentials Repository.
440
+
441
+ - **atomman.load_prototype** and **atomman.load_crystal** load options added
442
+ that allow for new Systems to be generated based on crystal_prototype and
443
+ relaxed_crystal records in the NIST Interatomic Potentials Repository.
444
+
445
+ - **atomman.defect.GammaSurface** class updated so that the RBF interpolated
446
+ energies are smoothed across the periodic cell boundaries.
447
+
448
+ - Fix to keep the code compatible with Python 3.6 (which broke in version
449
+ 1.3.2)
450
+
451
+ Version 1.3.2
452
+ -------------
453
+
454
+ - **System.r0** added which finds the shortest interatomic spacing.
455
+
456
+ - **System.rotate** made more robust.
457
+
458
+ - **atomman.tools.miller.plane_crystal_to_cartesian** added that identifies
459
+ the Cartesian normal associated with a crystallographic plane.
460
+
461
+ - **atomman.lammps.Potential** made consistent with
462
+ potentials.LAMMPSPotential. Upcoming versions of atomman will have
463
+ potentials as a requirement eliminating the duplication: (this class will
464
+ simply be a renaming of the class from potentials).
465
+
466
+ - **atomman.lammps.LammpsError** error type added.
467
+
468
+ - **atomman.defect.dislocation_system_basis** and
469
+ **atomman.defect.dislocation_system_transform** functions added supporting
470
+ the identification of dislocation system orientations based on
471
+ material-specific parameters.
472
+
473
+ - The "n" parameter in **atomman.defect.free_surface_basis** was renamed to
474
+ maxindex consistency with the new dislocation_system functions.
475
+
476
+ - **atomman.defect.VolterraDislocation**, **atomman.defect.Stroh**,
477
+ **atomman.defect.IsotropicVolterraDislocation**, and
478
+ **atomman.defect.solve_volterra_dislocation** were updated by integrating in
479
+ the dislocation_system functions. This makes it possible to now easily define
480
+ dislocation solutions based on the slip plane, line direction and Burgers
481
+ vector alone.
482
+
483
+ - **atomman.defect.dislocation_periodic_array** was updated to add an old_id
484
+ parameter to the returned dislocation system making it easier to map the atoms
485
+ in the defect system back to the perfect crystal base system used during
486
+ construction.
487
+
488
+ - **atomman.defect.FreeSurface** class for generating free surface
489
+ configurations from a unit cell and (hkl) plane was added.
490
+
491
+ - **atomman.defect.StackingFault** class completely rebuilt as a subclass of
492
+ FreeSurface to make it easier to use, i.e. systems can be generated directly
493
+ from unit cell, (hkl) and shift values.
494
+
495
+ - **atomman.defect.DifferentialDisplacement** class created. This class offers
496
+ more plotting options than the old differential_displacement function while
497
+ dividing the calculation and plotting into separate steps to make it easier
498
+ to work with.
499
+
500
+ - **atomman.defect.SDVPN** class updated to allow for VolterraDislocation
501
+ objects to be directly used as input parameters. This makes it easier to
502
+ work with as the transformations between dislocation orientations and gamma
503
+ surface orientations can be automatically identified and handled.
504
+ Additionally, solution summary and plotting tools incorporated into the
505
+ class for convenience.
506
+
507
+ Version 1.3.1
508
+ -------------
509
+
510
+ - **Atoms.prop_atype** updated for new atype handling.
511
+
512
+ - **defect.GammaSurface** default plotting behavior improved.
513
+
514
+
515
+ Version 1.3.0
516
+ -------------
517
+
518
+ - **Support for Python < 3.6 removed.** Python 2 support removed due to its
519
+ imminent end at the new year. Minimal version of 3.6 selected to take
520
+ advantage of f-strings.
521
+
522
+ - **Atoms and System natype, atypes** behavior changed to allow for unassigned
523
+ atype values and/or symbols. Now, atype values must be > 0 and natypes =
524
+ max(atype). CAUTION: this could conceivably break backwards compatibility.
525
+
526
+ - **lammps.Potential** expanded.
527
+
528
+ - **allsymbols** property added to support pair_styles that require all
529
+ symbols to be listed in the pair_coeff lines even if they are not used.
530
+ - **status** property added that indicates if the potential is known to
531
+ have been superseded by a newer version or retracted for being invalid.
532
+ - **pair_info** now supports an optional masses parameter for overriding
533
+ default mass values.
534
+
535
+ - **load.atom_data** now recognizes image flags in the Atoms tables, and reads
536
+ values from the Masses tables. Parameter checking is performed allowing for
537
+ more informative errors to be thrown.
538
+
539
+ - **dump.atom_data** updated to allow Potential objects to be passed directly,
540
+ and for pair_info to be included in the generated info LAMMPS input lines.
541
+
542
+ - **System.masses** attribute added. This is used for saving mass values from
543
+ load.atom_data, and for overriding default Potential.masses values in
544
+ dump.atom_data.
545
+
546
+ - **defect.dislocation_array** debugged, documented, and made consistent with
547
+ Volterra solutions.
548
+
549
+ - **defect.IsotropicVolterraDislocation** displacements fixed and adjusted to
550
+ predict displacements and stresses consistent with values from defect.Stroh.
551
+
552
+ - **defect.solve_volterra_dislocation** simplified to remove unnecessary
553
+ pre-check of elastic constants.
554
+
555
+ - **region** submodule added that allows for geometries in space to be defined
556
+ and used to slice systems and per-atom properties.
557
+
558
+ - **Box** is now a subclass of region.Shape allowing it to be used for
559
+ region-based selection as well.
560
+
561
+ Version 1.2.8
562
+ -------------
563
+
564
+ - **defect.GammaSurface** support added for setting shift vectors using
565
+ Miller-Bravais 4-term vectors.
566
+
567
+ - **tools.duplicates_allclose** added that identifies unique value sets
568
+ based on absolute tolerances.
569
+
570
+ - **load('phonopy'), System.dump('phonopy')** bug fixes.
571
+
572
+ - **System.atoms_ix** compatibility checks changed and reduced from throwing
573
+ an error to throwing a warning.
574
+
575
+ - **Atoms.extend and System.atoms_extend** methods added for adding atoms to
576
+ existing Atoms/System objects.
577
+
578
+ Version 1.2.7
579
+ -------------
580
+
581
+ - **Atoms.model and Box.model** added to create/read data model
582
+ representations of the objects.
583
+
584
+ - **System.composition** added that returns string composition.
585
+
586
+ - **System.model, load('system_model'), System.dump('system_model')**
587
+ data model format improved to capture all system information.
588
+
589
+ - **tools.Miller** functions for converting between Miller and Miller-Bravais
590
+ crystal planes.
591
+
592
+ - **defect.GammaSurface** combining of multiple plots better supported.
593
+
594
+ - **defect.StackingFault** minimum r parameter added allowing all atoms to
595
+ be at least a certain distance apart.
596
+
597
+ - **defect.free_surface_basis** added for identifying system orientations
598
+ associated with free surface configurations.
599
+
600
+ Version 1.2.6
601
+ -------------
602
+
603
+ - **lammps.NEBLog** added for nudged elastic band calculation log files.
604
+
605
+ - **tools.Miller** transformations now all take float values and
606
+ primitive-conventional cell conversions added.
607
+
608
+ - **Box.volume** bug fix to ensure returned volume is always positive.
609
+
610
+ - **defect.StackingFault** stacking fault configuration generator added.
611
+
612
+ - **nlist, dvect, dmag, defect.slip_vector** routines improved using Cython,
613
+ alternate implementations of routines removed.
614
+
615
+ Version 1.2.5
616
+ -------------
617
+
618
+ - **Box.volume** parameter added. Also, new class methods for initializing boxes
619
+ based on crystal systems (cubic, hexagonal, etc.).
620
+
621
+ - **load('poscar')** now supports excess per-atom lines.
622
+
623
+ - **System.atoms_ix** added for indexing atoms at the system level.
624
+
625
+ - **defect.GammaSurface** reworked with improved design and features.
626
+
627
+ Version 1.2.4
628
+ -------------
629
+
630
+ - **Atoms.prop_atype()** added to allow properties to be assigned by prop_atype.
631
+
632
+ - **ElasticConstants.normalized_as()** and **ElasticConstants.is_normal()** added to
633
+ force/check crystallographic symmetry of elastic tensors.
634
+
635
+ - **load('atom_data')** updated to support reading files containing # comments.
636
+
637
+ - **lammps.Potential** now supports specifying potentials with static charges.
638
+
639
+ - **defect.IsotropicVolterraDislocation** class added as **defect.Stroh** could not calculate
640
+ isotropic solutions. Both classes are now children of **defect.VolterraDislocation**,
641
+ and wrapper function **defect.solve_volterra_dislocation()** has been added.
642
+
643
+ - **defect.dislocation_array()** added that transforms a bulk system into a periodic array of
644
+ dislocations, where the two system boundaries in the slip plane are periodic, and
645
+ the third boundary is not.
646
+
647
+ - **defect.differential_displacement()** updated to provide users more options and control over
648
+ the plots.
649
+
650
+ - MANIFEST.in corrected so non-code files should be properly copied during installation.
651
+
652
+ Version 1.2.3
653
+ -------------
654
+
655
+ - **load()** updated with more uniform parameters across the different styles.
656
+ Style 'phonopy_Atoms' added.
657
+
658
+ - **System.wrap()** made slightly more robust.
659
+
660
+ Version 1.2.2
661
+ -------------
662
+ - **System** scale/unscale bug fix.
663
+
664
+ - **defect.GammaSurface.model()** returned format improved for saving/loading results.
665
+
666
+ - **load('system_model')** updated with symbols parameter.
667
+
668
+ Version 1.2.1
669
+ -------------
670
+
671
+ - Corrections to setup.py for properly loading/building cython code.
672
+
673
+ Version 1.2.0
674
+ -------------
675
+
676
+ - Overhaul for Python 2/3 compatibility.
677
+
678
+ - Reorganization of code and renaming of some features.
679
+
680
+ - Cython routines added for dvect and neighbor list calculations.
681
+
682
+ - Improved documentation.
atomman/source/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ atomman Project Package Initialization File
4
+ """
atomman/source/atomman/VERSION ADDED
@@ -0,0 +1 @@
 
 
1
+ 1.5.2
atomman/source/atomman/__init__.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+
3
+ # Standard Python imports
4
+ from importlib import resources
5
+
6
+ # potentials imports
7
+ from potentials import build_lammps_potential, settings
8
+
9
+ # atomman imports
10
+ from . import unitconvert
11
+ from . import tools
12
+ from . import thermo
13
+ from . import mep
14
+ from . import region
15
+ from . import lammps
16
+ from .dump import dump, set_dump_styles
17
+ from .dump import __all__ as dump_all
18
+ from .core import *
19
+ from .core import __all__ as core_all
20
+ from . import cluster
21
+ from . import library
22
+ from .library import load_lammps_potential
23
+ from .load import load, FileFormatError
24
+ from .load import __all__ as load_all
25
+ from . import plot
26
+ from . import defect
27
+
28
+ # Set dump styles
29
+ set_dump_styles()
30
+
31
+ # Read version from VERSION file
32
+ if hasattr(resources, 'files'):
33
+ __version__ = resources.files('atomman').joinpath('VERSION').read_text(encoding='UTF-8')
34
+ else:
35
+ __version__ = resources.read_text('atomman', 'VERSION', encoding='UTF-8').strip()
36
+
37
+ # Build all list
38
+ __all__ = ['__version__', 'load_lammps_potential', 'build_lammps_potential', 'settings',
39
+ 'tools', 'thermo', 'mep', 'region', 'lammps', 'dump', 'cluster', 'library',
40
+ 'load', 'FileFormatError', 'plot', 'defect']
41
+ __all__ += dump_all + core_all + load_all
42
+ __all__.sort()
43
+
44
+ # Define default working units
45
+ unitconvert.reset_units(length = 'angstrom', mass = 'amu', energy='eV', charge='e')
atomman/source/atomman/cluster/BondAngleMap.py ADDED
@@ -0,0 +1,752 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+
3
+ # Standard Python libraries
4
+ import datetime
5
+ import io
6
+ from typing import Generator, Optional, Union, Tuple
7
+
8
+ # https://github.com/usnistgov/DataModelDict
9
+ from DataModelDict import DataModelDict as DM
10
+
11
+ # https://numpy.org/
12
+ import numpy as np
13
+ import numpy.typing as npt
14
+
15
+ # https://pandas.pydata.org/
16
+ import pandas as pd
17
+
18
+ # https://matplotlib.org/
19
+ import matplotlib.pyplot as plt
20
+
21
+ # atomman imports
22
+ from .. import Atoms, Box, System
23
+ from ..tools import aslist
24
+ import atomman.unitconvert as uc
25
+
26
+ class BondAngleMap():
27
+ """
28
+ Class for generating and analyzing energies of three atom clusters that
29
+ explore a range of interatomic distances and bond angles. Can be used to
30
+ characterize the 3 atom bond nature of interatomic potentials.
31
+ """
32
+
33
+ def __init__(self,
34
+ model: Union[str, io.IOBase, DM, None] = None,
35
+ rmin: Optional[float] = None,
36
+ rmax: Optional[float] = None,
37
+ rnum: Optional[int] = None,
38
+ thetamin: Optional[float] = None,
39
+ thetamax: Optional[float] = None,
40
+ thetanum: Optional[int] = None,
41
+ r_ij: Optional[npt.ArrayLike] = None,
42
+ r_ik: Optional[npt.ArrayLike] = None,
43
+ theta: Optional[npt.ArrayLike] = None,
44
+ energy: Optional[npt.ArrayLike] = None,
45
+ symbols: Union[str, list, None] = None):
46
+ """
47
+ Class initializer. The cluster coordinates (r distances and theta
48
+ angles) are required and can be specified in one of three ways.
49
+ - A data model containing all the information.
50
+ - Input ranges (rmin, rmax, rnum, thetamin, thetamax, thetanum). The r
51
+ range parameters will be used to generate values for both r_ij and
52
+ r_ik.
53
+ - Explicitly giving r_ij, r_ik and theta values.
54
+
55
+ Parameters
56
+ ----------
57
+ model : str, path-like object or DataModelDict, optional
58
+ Collected data model results from a series of runs. Contains both
59
+ coordinate information and energy values.
60
+ rmin : float, optional
61
+ The minimum value used for the r_ij and r_ik spacings.
62
+ rmax : float, optional
63
+ The maximum value used for the r_ij and r_ik spacings.
64
+ rnum : float, optional
65
+ The number of values used for the r_ij and r_ik spacings.
66
+ thetamin : float, optional
67
+ The minimum value used for the theta angles.
68
+ thetamax : float, optional
69
+ The maximum value used for the theta angles.
70
+ thetanum : float, optional
71
+ The number of values used for the theta angles.
72
+ r_ij : array-like, optional
73
+ All r_ij values used. If given, the lengths of r_ij, r_ik and
74
+ theta need to be the same.
75
+ r_ik : array-like, optional
76
+ All r_ik values used. If given, the lengths of r_ij, r_ik and
77
+ theta need to be the same.
78
+ theta : array-like, optional
79
+ All theta values used. If given, the lengths of r_ij, r_ik and
80
+ theta need to be the same.
81
+ energy : array-like, optional
82
+ All measured energies. If r_ij, r_ik and theta are given then
83
+ all should be the same length. If the coordinate range parameters
84
+ are given, then the energies should be of length rnum*rnum*thetanum
85
+ and ordered to correspond to three embedded loops with r_ij
86
+ iterating in the outside loop, r_ik in the middle and theta in the
87
+ inside. If energy is not given, then all values will initially be
88
+ set to np.nan.
89
+ symbols : str or list, optional
90
+ Element model symbol(s) to associate with the three atoms if/when
91
+ systems are created. Can either be a single symbol to assign to
92
+ all atoms, or three symbols to assign to atoms i, j, and k
93
+ individually. Not needed if systems are not generated by this
94
+ class.
95
+ """
96
+ self.symbols = symbols
97
+ if model is not None:
98
+ try:
99
+ assert rmin is None and rmax is None and rnum is None
100
+ assert thetamin is None and thetamax is None and thetanum is None
101
+ assert r_ij is None and r_ik is None and theta is None
102
+ assert energy is None
103
+ except AssertionError as e:
104
+ raise ValueError('model cannot be given energy or coordinate parameters') from e
105
+ self.model(model)
106
+ else:
107
+ self.set(energy=energy, rmin=rmin, rmax=rmax, rnum=rnum,
108
+ thetamin=thetamin, thetamax=thetamax, thetanum=thetanum,
109
+ r_ij=r_ij, r_ik=r_ik, theta=theta)
110
+
111
+ @property
112
+ def rmin(self) -> Optional[float]:
113
+ """float or None: The minimum value used for the r_ij and r_ik spacings."""
114
+ return self.__rmin
115
+
116
+ @property
117
+ def rmax(self) -> Optional[float]:
118
+ """float or None: The maximum value used for the r_ij and r_ik spacings."""
119
+ return self.__rmax
120
+
121
+ @property
122
+ def rnum(self) -> Optional[int]:
123
+ """int or None: The number of values used for the r_ij and r_ik spacings."""
124
+ return self.__rnum
125
+
126
+ @property
127
+ def thetamin(self) -> Optional[float]:
128
+ """float or None: The minimum value used for the theta angles."""
129
+ return self.__thetamin
130
+
131
+ @property
132
+ def thetamax(self) -> Optional[float]:
133
+ """float or None: The maximum value used for the theta angles."""
134
+ return self.__thetamax
135
+
136
+ @property
137
+ def thetanum(self) -> Optional[int]:
138
+ """int or None: The number of values used for the theta angles."""
139
+ return self.__thetanum
140
+
141
+ @property
142
+ def df(self) -> pd.DataFrame:
143
+ """pandas.Dataframe : The cluster coordinates and energies."""
144
+ return self.__df
145
+
146
+ @property
147
+ def symbols(self) -> Optional[list]:
148
+ """list or None: the atomic symbols associated with the three atoms"""
149
+ return self.__symbols
150
+
151
+ @symbols.setter
152
+ def symbols(self, value: Union[str, list]):
153
+
154
+ if value is not None:
155
+ value = aslist(value)
156
+ if len(value) != 1 and len(value) != 3:
157
+ raise ValueError('Number of symbols must be 1 or 3')
158
+
159
+ self.__symbols = value
160
+
161
+ def model(self,
162
+ model: Union[str, io.IOBase, DM, None] = None,
163
+ length_unit: str = 'angstrom',
164
+ energy_unit: str = 'eV') -> Optional[DM]:
165
+ """
166
+ Loads or generates a bond angle map data model.
167
+
168
+ Note: Generating data models is currently limited to regular values,
169
+ i.e. ones which the coordinates correspond to embedded loops with
170
+ r_ij iterating in the outside loop, r_ik in the middle loop and
171
+ theta in the inside loop.
172
+
173
+ Parameters
174
+ ----------
175
+ model : str, file-like object or DataModelDict, optional
176
+ The data model content or file containing the bond angle map data.
177
+ If given, the content will be read in and set to the current object.
178
+ If not given, then a data model will be returned for the object.
179
+ length_unit : str, optional
180
+ The unit of length to save the rmin and max values in when
181
+ generating a data model. Default is 'angstrom'.
182
+ value
183
+ energy_unit : str, optional
184
+ The unit of energy to save the energy values in when generating a
185
+ data model. Default value is 'eV'.
186
+
187
+ Returns
188
+ -------
189
+ DataModelDict.DataModelDict
190
+ The data model containing the bond angle map coordinate information
191
+ and measured energies. Only returned if model is not given as a
192
+ parameter.
193
+
194
+ Raises
195
+ ------
196
+ ValueError
197
+ If the data is irregular, i.e. coordinates do not conform to
198
+ embedded loops with r_ij in the outer loop, r_ik in the middle loop
199
+ and theta in the inside loop.
200
+ """
201
+ # Read in a data model
202
+ if model is not None:
203
+ cluster = DM(model).find('bond-angle-map')
204
+
205
+ rmin = uc.value_unit(cluster['minimum-r-spacing'])
206
+ rmax = uc.value_unit(cluster['maximum-r-spacing'])
207
+ rnum = int(cluster['number-of-r-spacings'])
208
+
209
+ thetamin = float(cluster['minimum-angle'])
210
+ thetamax = float(cluster['maximum-angle'])
211
+ thetanum = int(cluster['number-of-angles'])
212
+
213
+ energy = uc.value_unit(cluster['energy'])
214
+
215
+ self.set(energy=energy, rmin=rmin, rmax=rmax, rnum=rnum,
216
+ thetamin=thetamin, thetamax=thetamax, thetanum=thetanum)
217
+
218
+ else:
219
+ if self.rnum is None:
220
+ raise ValueError('model does not support irregular measurements')
221
+
222
+ model = DM()
223
+ model['bond-angle-map'] = cluster = DM()
224
+
225
+ cluster['minimum-r-spacing'] = uc.model(self.rmin, length_unit)
226
+ cluster['maximum-r-spacing'] = uc.model(self.rmax, length_unit)
227
+ cluster['number-of-r-spacings'] = self.rnum
228
+
229
+ cluster['minimum-angle'] = self.thetamin
230
+ cluster['maximum-angle'] = self.thetamax
231
+ cluster['number-of-angles'] = self.thetanum
232
+
233
+ cluster['energy'] = uc.model(self.df.energy, energy_unit)
234
+
235
+ return model
236
+
237
+ def set(self,
238
+ rmin: Optional[float] = None,
239
+ rmax: Optional[float] = None,
240
+ rnum: Optional[int] = None,
241
+ thetamin: Optional[float] = None,
242
+ thetamax: Optional[float] = None,
243
+ thetanum: Optional[int] = None,
244
+ r_ij: Optional[npt.ArrayLike] = None,
245
+ r_ik: Optional[npt.ArrayLike] = None,
246
+ theta: Optional[npt.ArrayLike] = None,
247
+ energy: Optional[npt.ArrayLike] = None):
248
+ """
249
+ Sets the bond angle coordinates and the associated energies, if given.
250
+
251
+ Parameters
252
+ ----------
253
+ energy : array-like, optional
254
+ All measured energies. If r_ij, r_ik and theta are given then
255
+ all should be the same length. If the range parameters are given
256
+ then the length should be thetanum*rnum*rnum. If not given, then
257
+ a new array of nan values will be constructed.
258
+ rmin : float, optional
259
+ The minimum value used for the r_ij and r_ik spacings.
260
+ rmax : float, optional
261
+ The maximum value used for the r_ij and r_ik spacings.
262
+ rnum : float, optional
263
+ The number of values used for the r_ij and r_ik spacings.
264
+ thetamin : float, optional
265
+ The minimum value used for the theta angles.
266
+ thetamax : float, optional
267
+ The maximum value used for the theta angles.
268
+ thetanum : float, optional
269
+ The number of values used for the theta angles.
270
+ r_ij : array-like, optional
271
+ All r_ij values used. If given, the lengths of r_ij, r_ik and
272
+ theta need to be the same.
273
+ r_ik : array-like, optional
274
+ All r_ik values used. If given, the lengths of r_ij, r_ik and
275
+ theta need to be the same.
276
+ theta : array-like, optional
277
+ All theta values used. If given, the lengths of r_ij, r_ik and
278
+ theta need to be the same.
279
+ energy : array-like, optional
280
+ All measured energies. If r_ij, r_ik and theta are given then
281
+ all should be the same length. If the coordinate range parameters
282
+ are given, then the energies should be of length rnum*rnum*thetanum
283
+ and ordered to correspond to three embedded loops with r_ij
284
+ iterating in the outside loop, r_ik in the middle and theta in the
285
+ inside. If energy is not given, then all values will initially be
286
+ set to np.nan.
287
+ """
288
+
289
+ # Set coordinate values based on ranges
290
+ if rmin is not None:
291
+ if r_ij is not None or r_ik is not None or theta is not None:
292
+ raise ValueError('range parameters and explicit values cannot be mixed')
293
+ try:
294
+ rvals = np.linspace(rmin, rmax, rnum)
295
+ tvals = np.linspace(thetamin, thetamax, thetanum)
296
+ except Exception as e:
297
+ raise ValueError('Invalid range parameters') from e
298
+
299
+ # Set range parameters as class properties
300
+ self.__rmin = rmin
301
+ self.__rmax = rmax
302
+ self.__rnum = rnum
303
+ self.__thetamin = thetamin
304
+ self.__thetamax = thetamax
305
+ self.__thetanum = thetanum
306
+
307
+ # Generate the input parameters
308
+ r_ij, r_ik, theta = np.meshgrid(rvals, rvals, tvals, indexing='ij')
309
+ r_ij = r_ij.flatten()
310
+ r_ik = r_ik.flatten()
311
+ theta = theta.flatten()
312
+
313
+ # Explicitly set coordinate values
314
+ else:
315
+ try:
316
+ assert rmax is None and rnum is None
317
+ assert thetamin is None and thetamax is None and thetanum is None
318
+ except AssertionError as e:
319
+ raise ValueError('range parameters and explicit values cannot be mixed') from e
320
+ try:
321
+ if len(r_ij) != len(r_ik) or len(r_ij) != len(theta):
322
+ raise ValueError('Equal numbers of r_ij, r_ik, and theta values must be given')
323
+ except Exception as e:
324
+ raise ValueError('Invalid parameters') from e
325
+
326
+ r_ij = np.asarray(r_ij)
327
+ r_ik = np.asarray(r_ik)
328
+ theta = np.asarray(theta)
329
+
330
+ # Try to extract range parameters
331
+ rmin = r_ij.min()
332
+ rmax = r_ij.max()
333
+ rnum = len(np.unique(r_ij))
334
+
335
+ thetamin = theta.min()
336
+ thetamax = theta.max()
337
+ thetanum = len(np.unique(theta))
338
+
339
+ try:
340
+ assert len(r_ij) == thetanum * rnum * rnum
341
+ rvals = np.linspace(rmin, rmax, rnum)
342
+ tvals = np.linspace(thetamin, thetamax, thetanum)
343
+ r_ij_test, r_ik_test, theta_test = np.meshgrid(rvals, rvals, tvals, indexing='ij')
344
+ assert np.allclose(r_ij, r_ij_test.flatten())
345
+ assert np.allclose(r_ik, r_ik_test.flatten())
346
+ assert np.allclose(theta, theta_test.flatten())
347
+ except:
348
+ # Set range parameters as None to indicate not correctly ordered
349
+ self.__rmin = None
350
+ self.__rmax = None
351
+ self.__rnum = None
352
+ self.__thetamin = None
353
+ self.__thetamax = None
354
+ self.__thetanum = None
355
+ else:
356
+ # Set range parameters
357
+ self.__rmin = rmin
358
+ self.__rmax = rmax
359
+ self.__rnum = rnum
360
+ self.__thetamin = thetamin
361
+ self.__thetamax = thetamax
362
+ self.__thetanum = thetanum
363
+
364
+ # Check energy values
365
+ if energy is None:
366
+ energy = np.full(len(r_ij), np.nan)
367
+ elif len(energy) != len(r_ij):
368
+ raise ValueError('Mismatch between number of energies given and expected')
369
+
370
+ # Compute r_jk values
371
+ r_jk = np.sqrt(r_ij**2 + r_ik**2 - r_ij * r_ik * 2 * np.cos(np.radians(theta)))
372
+
373
+ # Build DataFrame
374
+ df = {}
375
+ df['r_ij'] = r_ij
376
+ df['r_ik'] = r_ik
377
+ df['r_jk'] = r_jk
378
+ df['theta'] = theta
379
+ df['energy'] = energy
380
+ self.__df = pd.DataFrame(df)
381
+
382
+ def itercoords(self) -> Generator[Tuple[float, float, float, float], None, None]:
383
+ """
384
+ Iterates through the three-body coordinates, which can be used as inputs for
385
+ computing energies.
386
+
387
+ Yields
388
+ ------
389
+ r_ij : float
390
+ The radial distance between atoms i and j.
391
+ r_ik : float
392
+ The radial distance between atoms i and k.
393
+ r_jk : float
394
+ The radial distance between atoms j and k.
395
+ theta : float
396
+ The angle between i-j and i-k in degrees.
397
+ """
398
+ for i in self.df.index:
399
+ series = self.df.loc[i]
400
+ yield series.r_ij, series.r_ik, series.r_jk, series.theta
401
+
402
+ def itersystem(self,
403
+ symbols: Union[str, list, None] = None,
404
+ copy: bool = False
405
+ ) -> Generator[System, None, None]:
406
+ """
407
+ Iterates through the three-body coordinates and returns a System for each.
408
+ Useful for generating configuration files for simulators. The atom
409
+ coordinates will be set such that atom 0 is at [0,0,0], atom 1 at
410
+ [r_ij,0,0] and atom 2 is in the xy plane based on r_ik and theta.
411
+
412
+ Parameters
413
+ ----------
414
+ symbols : str or list, optional
415
+ The element model symbols to assign to the atoms. Can either be
416
+ one value for all atoms, or three values for each atom individually.
417
+ If not given here, will use the values set during class
418
+ initialization.
419
+ copy : bool, optional
420
+ If False (default), then the yielded system is the same object with
421
+ the coordinates shifted. If True, each yielded system is a new object.
422
+
423
+ Yields
424
+ ------
425
+ atomman.System
426
+ The atomic system containing the three-body cluster.
427
+ """
428
+ # Set symbols
429
+ if symbols is not None:
430
+ self.symbols = symbols
431
+ symbols = self.symbols
432
+
433
+ # Identify box bounds based on r_ij values
434
+ rhi = 3 * self.df.r_ij.max()
435
+ rlo = - rhi
436
+
437
+ # Identify atypes from symbols
438
+ if symbols is None or len(symbols) == 1:
439
+ atype = np.array([1,1,1])
440
+ elif len(symbols) == 3:
441
+ symbols, atype = np.unique(symbols, return_inverse=True)
442
+ else:
443
+ print(symbols)
444
+ raise ValueError('Invalid symbols somehow...')
445
+
446
+ # Copy = True means generate new system each iteration
447
+ if copy:
448
+ for r_ij, r_ik, r_jk, theta in self.itercoords():
449
+
450
+ # Build the pos array
451
+ j_x = r_ij
452
+ k_x = r_ik * np.cos(np.radians(theta))
453
+ k_y = r_ik * np.sin(np.radians(theta))
454
+ pos = np.array([[0.0, 0.0, 0.0],
455
+ [j_x, 0.0, 0.0],
456
+ [k_x, k_y, 0.0]])
457
+
458
+ # Build and yield a new system
459
+ box = Box(xlo=rlo, xhi=rhi, ylo=rlo, yhi=rhi, zlo=-1.0, zhi=1.0)
460
+ atoms = Atoms(atype=atype, pos=pos)
461
+ yield System(atoms=atoms, box=box, symbols=symbols, pbc=[False, False, False])
462
+
463
+ # Copy = False means only generate one system and modify pos
464
+ else:
465
+ # Build system with all coordinates at [0,0,0]
466
+ box = Box(xlo=rlo, xhi=rhi, ylo=rlo, yhi=rhi, zlo=-1.0, zhi=1.0)
467
+ atoms = Atoms(atype=atype, pos=np.zeros([3,3]))
468
+ system = System(atoms=atoms, box=box, symbols=symbols, pbc=[False, False, False])
469
+
470
+ for r_ij, r_ik, r_jk, theta in self.itercoords():
471
+
472
+ # Modify the three non-zero coordinates
473
+ system.atoms.pos[1,0] = r_ij
474
+ system.atoms.pos[2,0] = r_ik * np.cos(np.radians(theta))
475
+ system.atoms.pos[2,1] = r_ik * np.sin(np.radians(theta))
476
+
477
+ yield system
478
+
479
+ def save_table(self,
480
+ filename: str,
481
+ include_header: bool = True):
482
+ """
483
+ Saves a tabulated representation of the coordinates and energy values to a file.
484
+
485
+ Parameters
486
+ ----------
487
+ filename : str
488
+ The path to the file where the table will be saved.
489
+ include_header : bool
490
+ If True (default) then header comments will be listed at the top of the file.
491
+ """
492
+ with open(filename, 'w', encoding='UTF-8') as f:
493
+
494
+ if include_header:
495
+ emin = self.df.energy.min()
496
+
497
+ # Create the header comment lines
498
+ f.write(f'# Comment: this is a file containing an r12, r13, theta - energy-block E(r12, r13, theta) E_min={emin:18.14} eV\n')
499
+ f.write('# File format: After 9 comment lines (starting with #) there are three lines with (angle in degrees):\n')
500
+ f.write('# r12_min, r12_max, n_bins_r12; r13_min, r13_max, n_bins_r13, theta_min, theta_max, n_bins_theta followed by\n')
501
+ f.write('# the data block in the format index_r12, index_r13_ index_theta, E(i_r12,i_r13,i_theta) [eV] one per line.\n')
502
+ f.write('# Conversion from index to value: val = val_min + (index-1)*(val_max - val_min)/(n_bins-1)\n')
503
+ f.write(f'# file created by xxx via code atomman on date {datetime.date.today}\n')
504
+ #f.write(f'# This is an example for 3-body MD-potential C-W; Reference: J. Jones, CPPC 34, p.123-432 (2019)\n')
505
+ #f.write(f'# Atom 1: C, Atom 2: W, Atom 3: W\n')
506
+ #f.write(f'# Version 0.2 (2021-02-15) by U v. Toussaint (IPP)\n')
507
+ f.write(f'{self.rmin:18.14} {self.rmax:18.14} {self.rnum}\n')
508
+ f.write(f'{self.rmin:18.14} {self.rmax:18.14} {self.rnum}\n')
509
+ f.write(f'{self.thetamin:18.14} {self.thetamax:18.14} {self.thetanum}\n')
510
+
511
+ l = 0
512
+ for i in range(self.rnum):
513
+ for j in range(self.rnum):
514
+ for k in range(self.thetanum):
515
+ f.write(f'{i+1:6} {j+1:6} {k+1:6} {self.df.energy.values[l]:18.14}\n')
516
+ l += 1
517
+
518
+ def load_table(self,
519
+ filename: str,
520
+ length_unit: str = 'angstrom',
521
+ energy_unit: str = 'eV'):
522
+ """
523
+ Loads a tabulated representation of the coordinates and energy values
524
+ from a file.
525
+
526
+ Parameters
527
+ ----------
528
+ filename : str
529
+ The path to the file where the tabulated data is stored.
530
+ length_unit : str, optional
531
+ The units of length used in the file. Default value is 'angstrom'.
532
+ energy_unit : str, optional
533
+ The units of energy used in the file. Default value is 'eV'.
534
+ """
535
+
536
+ with open(filename, encoding='UTF-8') as f:
537
+ lines = f.readlines()
538
+
539
+ count = 0
540
+ energies = []
541
+ i = 1
542
+ j = 1
543
+ k = 1
544
+ for line in lines:
545
+ terms = line.split()
546
+ if line.strip()[0] == '#' or len(terms) == 0:
547
+ continue
548
+
549
+ # Get r_ij min, max, num
550
+ if count == 0:
551
+ rmin = float(terms[0])
552
+ rmax = float(terms[1])
553
+ rnum = int(terms[2])
554
+ count += 1
555
+
556
+ # Check r_ik min, max, num
557
+ elif count == 1:
558
+ assert np.isclose(rmin, float(terms[0])), 'only identical rij and rik ranges currently supported'
559
+ assert np.isclose(rmax, float(terms[1])), 'only identical rij and rik ranges currently supported'
560
+ assert np.isclose(rnum, int(terms[2])), 'only identical rij and rik ranges currently supported'
561
+ count += 1
562
+
563
+ # Get theta min, max, num
564
+ elif count == 2:
565
+ thetamin = float(terms[0])
566
+ thetamax = float(terms[1])
567
+ thetanum = int(terms[2])
568
+ count += 1
569
+ energies = np.empty(rnum*rnum*thetanum)
570
+
571
+ # Get energies
572
+ else:
573
+ i = int(terms[0]) - 1
574
+ j = int(terms[1]) - 1
575
+ k = int(terms[2]) - 1
576
+ index = k + j * thetanum + i * rnum * thetanum
577
+ energies[index] = float(terms[3])
578
+
579
+ # Convert units
580
+ rmin = uc.set_in_units(rmin, length_unit)
581
+ rmax = uc.set_in_units(rmax, length_unit)
582
+ energies = uc.set_in_units(energies, energy_unit)
583
+
584
+ self.set(rmin=rmin, rmax=rmax, rnum=rnum, thetamin=thetamin,
585
+ thetamax=thetamax, thetanum=thetanum, energy=energies)
586
+
587
+ def pdf(self,
588
+ nbins: int = 301,
589
+ energymin: float = -15.0,
590
+ energymax: float = 15.0) -> Tuple[np.ndarray, np.ndarray]:
591
+ """
592
+ Returns the probability density function for the energy
593
+
594
+ Parameters
595
+ ----------
596
+ nbins : int, optional
597
+ The number of histogram bins to use. Default value is 301.
598
+ energymin : float, optional
599
+ The minimum energy bound to consider. Default value is -15.0.
600
+ energymax : float, optional
601
+ The maximum energy bound to consider. Default value is 15.0.
602
+
603
+ Returns
604
+ -------
605
+ pdf : numpy.NDArray
606
+ The probability density function associated with each bin.
607
+ centers : numpy.NDArray
608
+ The center values for each bin.
609
+ """
610
+
611
+ hist, edges = np.histogram(self.df.energy, bins=nbins, range=(energymin, energymax))
612
+
613
+ # Divide the historgram count by total number of measurements
614
+ pdf = hist/len(self.df)
615
+
616
+ # Average the bin edges to get the bin centers
617
+ centers = (edges[:-1] + edges[1:]) / 2
618
+
619
+ return pdf, centers
620
+
621
+ def cumulative_pdf(self,
622
+ nbins: int = 301,
623
+ energymin: float = -15.0,
624
+ energymax: float = 15.0) -> Tuple[np.ndarray, np.ndarray]:
625
+ """
626
+ Returns the cumulative probability density function for the energy.
627
+
628
+ Parameters
629
+ ----------
630
+ nbins : int, optional
631
+ The number of histogram bins to use. Default value is 301.
632
+ energymin : float, optional
633
+ The minimum energy bound to consider. Default value is -15.0.
634
+ energymax : float, optional
635
+ The maximum energy bound to consider. Default value is 15.0.
636
+
637
+ Returns
638
+ -------
639
+ cum_pdf : numpy.NDArray
640
+ The cumulative probability density function associated with each bin.
641
+ centers : numpy.NDArray
642
+ The center values for each bin.
643
+ """
644
+
645
+ # Get pdf and centers in the range
646
+ pdf, centers = self.pdf(nbins=nbins, energymin=energymin, energymax=energymax)
647
+
648
+ # Calculate cumulative pdf below energymin
649
+ shift = np.sum(self.df.energy < energymin) / len(self.df)
650
+
651
+ # Calculate the cumulative sum of pdf and apply the shift
652
+ cum_pdf = np.cumsum(pdf) + shift
653
+
654
+ return cum_pdf, centers
655
+
656
+ def plot_pdf(self,
657
+ nbins: int = 301,
658
+ energymin: float = -15.0,
659
+ energymax: float = 15.0,
660
+ matplotlib_axes: Optional[plt.axes] = None,
661
+ **kwargs) -> Optional[plt.figure]:
662
+ """
663
+ Generates a plot of the probability density function of the energy.
664
+
665
+ Parameters
666
+ ----------
667
+ nbins : int, optional
668
+ The number of histogram bins to use. Default value is 301.
669
+ energymin : float, optional
670
+ The minimum energy bound to consider. Default value is -15.0.
671
+ energymax : float, optional
672
+ The maximum energy bound to consider. Default value is 15.0.
673
+ matplotlib_axes : matplotlib.Axes.axes, optional, optional
674
+ An existing plotting axis to add the pdf plot to. If not given,
675
+ a new figure object will be generated.
676
+ **kwargs : any, optional
677
+ Any additional key word arguments will be passed to
678
+ matplotlib.pyplot.figure for generating a new figure object (if
679
+ axis is not given).
680
+
681
+ Returns
682
+ -------
683
+ matplotlib.Figure
684
+ The generated figure. Not returned if matplotlib_axes is given.
685
+ """
686
+ # Get pdf and centers in the range
687
+ pdf, centers = self.pdf(nbins=nbins, energymin=energymin, energymax=energymax)
688
+
689
+ # Initial plot setup and parameters
690
+ if matplotlib_axes is None:
691
+ fig = plt.figure(**kwargs)
692
+ ax1 = fig.add_subplot(111)
693
+ else:
694
+ ax1 = matplotlib_axes
695
+
696
+ ax1.plot(centers, pdf)
697
+ ax1.set_xlim(energymin, energymax)
698
+
699
+ ax1.set_xlabel('Potential Energy [eV]')
700
+ ax1.set_ylabel('pdf(E)')
701
+
702
+ if matplotlib_axes is None:
703
+ return fig
704
+
705
+ def plot_cumulative_pdf(self,
706
+ nbins: int = 301,
707
+ energymin: float = -15.0,
708
+ energymax: float = 15.0,
709
+ matplotlib_axes: Optional[plt.axes] = None,
710
+ **kwargs) -> Optional[plt.figure]:
711
+ """
712
+ Generates a plot of the cumulative probability density function of the energy.
713
+
714
+ Parameters
715
+ ----------
716
+ nbins : int, optional
717
+ The number of histogram bins to use. Default value is 301.
718
+ energymin : float, optional
719
+ The minimum energy bound to consider. Default value is -15.0.
720
+ energymax : float, optional
721
+ The maximum energy bound to consider. Default value is 15.0.
722
+ matplotlib_axes : matplotlib.Axes.axes, optional, optional
723
+ An existing plotting axis to add the pdf plot to. If not given,
724
+ a new figure object will be generated.
725
+ **kwargs : any, optional
726
+ Any additional key word arguments will be passed to
727
+ matplotlib.pyplot.figure for generating a new figure object (if
728
+ axis is not given).
729
+
730
+ Returns
731
+ -------
732
+ matplotlib.Figure
733
+ The generated figure. Not returned if matplotlib_axes is given.
734
+ """
735
+ # Get pdf and centers in the range
736
+ cum_pdf, centers = self.cumulative_pdf(nbins=nbins, energymin=energymin, energymax=energymax)
737
+
738
+ # Initial plot setup and parameters
739
+ if matplotlib_axes is None:
740
+ fig = plt.figure(**kwargs)
741
+ ax1 = fig.add_subplot(111)
742
+ else:
743
+ ax1 = matplotlib_axes
744
+
745
+ ax1.plot(centers, cum_pdf)
746
+ ax1.set_xlim(energymin, energymax)
747
+
748
+ ax1.set_xlabel('Potential Energy [eV]')
749
+ ax1.set_ylabel('cumulative pdf(E)')
750
+
751
+ if matplotlib_axes is None:
752
+ return fig
atomman/source/atomman/cluster/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .BondAngleMap import BondAngleMap
2
+
3
+ __all__ = ['BondAngleMap']
atomman/source/atomman/core/Atoms.py ADDED
@@ -0,0 +1,572 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ from __future__ import annotations
4
+ import io
5
+ from copy import deepcopy
6
+ from collections import OrderedDict
7
+ from typing import Any, Optional, Union
8
+
9
+ # http://www.numpy.org/
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+
13
+ # https://pandas.pydata.org/
14
+ import pandas as pd
15
+
16
+ # https://github.com/usnistgov/DataModelDict
17
+ from DataModelDict import DataModelDict as DM
18
+
19
+ # atomman imports
20
+ import atomman.unitconvert as uc
21
+ from ..tools import indexstr
22
+
23
+ class Atoms(object):
24
+ """
25
+ Class for representing a collection of atoms.
26
+ """
27
+
28
+ class PropertyDict(OrderedDict):
29
+ """Extends OrderedDict to work with Atoms"""
30
+
31
+ def __init__(self, host: Atoms):
32
+ """PropertyDict needs to know its host Atoms object."""
33
+ self.__host = host
34
+ super(Atoms.PropertyDict, self).__init__()
35
+
36
+ def __setitem__(self, key: str, value: np.ndarray):
37
+ """
38
+ Modifies OrderedDict__setitem__() such that
39
+ 1. All values are converted to numpy.ndarrays.
40
+ 2. For new keys, checks are done that data is compatible with host.natoms.
41
+ 3. New keys are also added as attributes to host Atoms object, if allowed.
42
+ 4. For existing keys, new values are saved over the old ones as opposed to
43
+ only changing the name assignment.
44
+
45
+ Parameters
46
+ ----------
47
+ key : str
48
+ The property key name to assign values to.
49
+ value : numpy.ndarray
50
+ The values to assign.
51
+ """
52
+ # Shortcut to host
53
+ host = self.__host
54
+
55
+ # Python 2: change to unicode if needed.
56
+ try:
57
+ key = key.decode('UTF-8')
58
+ except:
59
+ pass
60
+
61
+ # Convert to numpy.ndarray if needed
62
+ value = np.asarray(value)
63
+
64
+ # Broadcast if needed and allowed
65
+ if value.shape == ():
66
+ value = np.array(np.broadcast_to(value, (host.natoms,) + value.shape))
67
+ elif value.shape[0] == 1:
68
+ value = np.array(np.broadcast_to(value, (host.natoms,) + value.shape[1:]))
69
+ elif value.shape[0] != host.natoms:
70
+ raise ValueError('First dimension of value must be 1 or natoms')
71
+
72
+ # Check that atype values are 1 or greater
73
+ if key == 'atype' and len(value) > 0 and np.min(value) < 1:
74
+ raise ValueError('atype values must be >= 1')
75
+
76
+ # If key is already assigned, save value over existing values
77
+ if key in self.keys():
78
+ self[key][:] = value
79
+
80
+ # Otherwise, set new item and try assigning attribute to host
81
+ else:
82
+ super(Atoms.PropertyDict, self).__setitem__(key, value)
83
+ try:
84
+ assert key not in dir(host)
85
+ super(Atoms, host).__setattr__(key, value) # pylint: disable=bad-super-call
86
+ except:
87
+ pass
88
+
89
+ def __init__(self,
90
+ natoms: Optional[int] = None,
91
+ atype: Union[int, npt.ArrayLike, None] = None,
92
+ pos: Optional[npt.ArrayLike] = None,
93
+ prop: Optional[dict] = None,
94
+ model: Union[str, io.IOBase, DM, None] = None,
95
+ safecopy: bool = False,
96
+ **kwargs):
97
+ """
98
+ Class initializer.
99
+
100
+ Parameters
101
+ ==========
102
+ natoms : int, optional
103
+ The number of atoms. If not given, will be inferred from other
104
+ parameters if possible, or set to 1 if not possible.
105
+ atype : int or list/ndarray of int, optional
106
+ The integer atomic types to assign to all atoms. Default is to
107
+ set all atypes to 1.
108
+ pos : list/ndarray of float, optional
109
+ The atomic positions to assign to all atoms. Default is to set
110
+ each atom's position to [0,0,0].
111
+ model : str or DataModelDict, optional
112
+ File path or content of a JSON/XML data model containing all
113
+ atom information. Cannot be given with any other parameters.
114
+ prop : dict, optional
115
+ Dictionary containing all per-atom properties. Can be used
116
+ instead of atype, pos, and kwargs. This is for backwards
117
+ compatibility support with atomman version 1.
118
+ safecopy : bool, optional
119
+ Flag indicating if values are to be copied before setting. For
120
+ property values given as numpy arrays, direct setting (False,
121
+ default) may result in the Atoms' property pointing to the original
122
+ numpy array. Using safecopy=True deep copies the property values
123
+ before setting to avoid this. Note that safecopy=True may be
124
+ considerably slower for large numbers of atoms and/or properties.
125
+ kwargs : any
126
+ All additional key/value pairs are assigned as properties.
127
+ """
128
+
129
+ # Check for model
130
+ if model is not None:
131
+ try:
132
+ assert natoms is None
133
+ assert atype is None
134
+ assert pos is None
135
+ assert prop is None
136
+ assert len(kwargs) == 0
137
+ except:
138
+ raise ValueError('model cannot be given with any other parameters')
139
+
140
+ # Extract natoms and properties from data model
141
+ model = DM(model).find('atoms')
142
+ natoms = model['natoms']
143
+ prop = OrderedDict()
144
+ for propmodel in model.aslist('property'):
145
+ prop[propmodel['name']] = uc.value_unit(propmodel['data'])
146
+
147
+ # Check for prop dictionary
148
+ if prop is not None:
149
+ if atype is not None and pos is not None and len(kwargs) > 0:
150
+ raise ValueError('prop dict cannot be given with keyword properties')
151
+
152
+ # Divide prop into keyword arguments
153
+ atype = prop.pop('atype', None)
154
+ pos = prop.pop('pos', None)
155
+ kwargs = prop
156
+
157
+ # Check atype parameter values
158
+ if atype is not None:
159
+ atype = np.asarray(atype)
160
+
161
+ # Handle single atype
162
+ if atype.ndim == 0:
163
+ natoms_atype = 1
164
+
165
+ # Handle list of atype
166
+ elif atype.ndim == 1:
167
+ natoms_atype = atype.shape[0]
168
+ else:
169
+ raise ValueError('Only one atype per atom allowed')
170
+ else:
171
+ atype = np.array([1], dtype='uint64')
172
+ natoms_atype = 1
173
+
174
+ # Check pos parameter values
175
+ if pos is not None:
176
+ pos = np.asarray(pos)
177
+
178
+ # Handle single pos
179
+ if pos.ndim == 1:
180
+ if pos.shape[0] == 3:
181
+ natoms_pos = 1
182
+ else:
183
+ raise ValueError('pos per atom must be 3-dimensional')
184
+
185
+ # Handle list of pos
186
+ elif pos.ndim == 2:
187
+ if pos.shape[1] == 3:
188
+ natoms_pos = pos.shape[0]
189
+ else:
190
+ raise ValueError('pos per atom must be 3-dimensional')
191
+ else:
192
+ raise ValueError('too many dimensions for pos')
193
+ else:
194
+ pos = np.zeros((1, 3), dtype='float64')
195
+ natoms_pos = 1
196
+
197
+ # Check natoms parameter values
198
+ if natoms is not None:
199
+ natoms = int(natoms)
200
+ if ((natoms_atype == 1 or natoms_atype == natoms)
201
+ and (natoms_pos == 1 or natoms_pos == natoms)):
202
+ pass
203
+ else:
204
+ raise ValueError('natoms and length of atype/pos not compatible')
205
+ else:
206
+ if natoms_atype == natoms_pos:
207
+ natoms = natoms_atype
208
+ elif natoms_atype == 1:
209
+ natoms = natoms_pos
210
+ elif natoms_pos == 1:
211
+ natoms = natoms_atype
212
+ else:
213
+ raise ValueError('lengths of atype and pos not compatible')
214
+
215
+ # Initialize underlying private class attributes
216
+ super(Atoms, self).__setattr__('_Atoms__natoms', natoms)
217
+ super(Atoms, self).__setattr__('_Atoms__view', Atoms.PropertyDict(self))
218
+ super(Atoms, self).__setattr__('_Atoms__dir', deepcopy(dir(self)))
219
+
220
+ # Set properties
221
+ if safecopy:
222
+ self.view['atype'] = deepcopy(atype)
223
+ self.view['pos'] = deepcopy(pos)
224
+ for key, value in kwargs.items():
225
+ self.view[key] = deepcopy(value)
226
+ else:
227
+ self.view['atype'] = atype
228
+ self.view['pos'] = pos
229
+ for key, value in kwargs.items():
230
+ self.view[key] = value
231
+
232
+ def model(self,
233
+ prop_name: Optional[list] = None,
234
+ unit: Optional[list] = None,
235
+ prop_unit: Optional[dict] = None) -> DM:
236
+ """
237
+ Generates a data model for the Atoms object.
238
+
239
+ Parameters
240
+ ----------
241
+ prop_name : list, optional
242
+ The Atoms properties to include. If neither prop_name nor prop_unit
243
+ are given, all system properties will be included.
244
+ unit : list, optional
245
+ Lists the units for each prop_name as stored in the table. For a
246
+ value of None, no conversion will be performed for that property.
247
+ If neither unit nor prop_units given, pos will be
248
+ given in Angstroms and all other values will not be converted.
249
+ prop_unit : dict, optional
250
+ dictionary where the keys are the property keys to include, and
251
+ the values are units to use. If neither unit nor prop_units given,
252
+ pos will be given in Angstroms and all other values will not be
253
+ converted.
254
+
255
+ Returns
256
+ -------
257
+ DataModelDict.DataModelDict
258
+ A JSON/XML data model for the current Atoms object.
259
+ """
260
+
261
+ # Set prop_unit if needed
262
+ if prop_unit is None:
263
+ if prop_name is None:
264
+ prop_name = self.prop()
265
+ if unit is None:
266
+ unit = [None for i in range(len(prop_name))]
267
+
268
+ if len(unit) != len(prop_name):
269
+ raise ValueError('')
270
+ prop_unit = {}
271
+ for p, u in zip(prop_name, unit):
272
+ prop_unit[p] = u
273
+
274
+ elif prop_name is not None or unit is not None:
275
+ raise ValueError('prop_unit cannot be given with prop_name or unit')
276
+
277
+ # Set default pos unit
278
+ if 'pos' in prop_unit and prop_unit['pos'] is None:
279
+ prop_unit['pos'] = 'angstrom'
280
+
281
+ # Generate DataModelDict
282
+ model = DM()
283
+ model['atoms'] = DM()
284
+ model['atoms']['natoms'] = self.natoms
285
+ for prop in prop_unit:
286
+ unit = prop_unit.get(prop, None)
287
+ propmodel = DM()
288
+ propmodel['name'] = prop
289
+ propmodel['data'] = uc.model(self.prop(prop), unit)
290
+ model['atoms'].append('property', propmodel)
291
+
292
+ return model
293
+
294
+ def __str__(self) -> str:
295
+ """string output of Atoms data"""
296
+ atype = self.atype # pylint: disable=no-member
297
+ pos = self.pos # pylint: disable=no-member
298
+ lines = ['per-atom properties = ' + str(self.prop())]
299
+ lines.append(' id | atype | pos[0] | pos[1] | pos[2]')
300
+ for i in range(self.natoms):
301
+ lines.append('%7i | %7i | %7.3f | %7.3f | %7.3f' % (i, atype[i], pos[i,0], pos[i,1], pos[i,2]))
302
+
303
+ return '\n'.join(lines)
304
+
305
+ def __setattr__(self, name: str, value: Any):
306
+ """Control setting of user-defined attributes"""
307
+ if not hasattr(self, name) or name in self.view:
308
+ self.view[name] = value
309
+ else:
310
+ super(Atoms, self).__setattr__(name, value)
311
+
312
+ def __intslice(self, intnum):
313
+ if intnum == -1:
314
+ return slice(intnum, None)
315
+ else:
316
+ return slice(intnum, intnum+1)
317
+
318
+ def __deepcopy__(self, memo) -> Atoms:
319
+ """Properly handle deepcopy"""
320
+ d = OrderedDict()
321
+ atype = deepcopy(self.view['atype'])
322
+ pos = deepcopy(self.view['pos'])
323
+ for key in self.view:
324
+ if key not in ['atype', 'pos']:
325
+ d[key] = deepcopy(self.view[key])
326
+ return Atoms(atype=atype, pos=pos, **d)
327
+
328
+ def __getitem__(self, index) -> Atoms:
329
+ """Index getting of Atoms."""
330
+ view = OrderedDict()
331
+ if isinstance(index, (int, np.integer)):
332
+ index = self.__intslice(index)
333
+ for key in self.view.keys():
334
+ view[key] = self.view[key][index]
335
+ return Atoms(**view)
336
+
337
+ def __setitem__(self, index, value):
338
+ """Index setting of Atoms."""
339
+ try:
340
+ assert isinstance(value, Atoms)
341
+ assert sorted(value.view.keys()) == sorted(self.view.keys())
342
+ except:
343
+ raise ValueError('Can only set Atoms with matching properties')
344
+
345
+ if isinstance(index, (int, np.integer)):
346
+ index = self.__intslice(index)
347
+
348
+ for key in self.view.keys():
349
+ self.view[key][index] = value.view[key]
350
+
351
+ def __len__(self) -> int:
352
+ """len of atoms = natoms."""
353
+ return self.natoms
354
+
355
+ @property
356
+ def natoms(self) -> int:
357
+ """int : The number of atoms in the Atoms class."""
358
+ return self.__natoms
359
+
360
+ @property
361
+ def atypes(self) -> tuple:
362
+ """tuple : List of int atom types."""
363
+ return tuple(range(1, self.natypes+1))
364
+
365
+ @property
366
+ def natypes(self) -> int:
367
+ """int : The number of atom types in the Atoms class."""
368
+ if np.min(self.atype) < 1:
369
+ raise ValueError('atype values < 1 not allowed')
370
+
371
+ return int(np.max(self.atype))
372
+
373
+ @property
374
+ def view(self) -> PropertyDict:
375
+ """PropertyDict : All assigned per-atom properties."""
376
+ return self.__view
377
+
378
+ def prop(self,
379
+ key: Optional[str] = None,
380
+ index: Union[int, list, slice, None] = None,
381
+ value: Optional[Any] = None,
382
+ a_id: Optional[int] = None
383
+ ) -> Union[list, Atoms, np.ndarray, None]:
384
+ """
385
+ Accesses the per-atom properties for controlled getting and setting.
386
+ For getting values, prop() always returns a copy of the underlying
387
+ data as opposed to the data itself.
388
+
389
+ Parameters
390
+ ----------
391
+ key : str, optional
392
+ Per-atom property name.
393
+ index : int, list, slice, optional
394
+ Index of atoms.
395
+ value : any, optional
396
+ Property values to assign.
397
+ a_id : int, optional
398
+ Integer atom index. Left in for backwards compatibility.
399
+
400
+ Returns
401
+ -------
402
+ list
403
+ If no parameters given, returns a list of all assigned property
404
+ keys.
405
+ atomman.Atoms
406
+ If index or a_id is given without value or key, returns a new
407
+ Atoms instance for the specified atom indices.
408
+ numpy.ndarray
409
+ If key (and index/a_id) is given without value, returns a copy of
410
+ the data associated with that property key.
411
+
412
+ raises
413
+ ------
414
+ ValueError
415
+ If a_id and index are both given, or only value is given.
416
+ """
417
+
418
+ # Handle a_id
419
+ if a_id is not None:
420
+ if index is not None:
421
+ raise ValueError('a_id and index cannot both be given')
422
+ index = a_id
423
+
424
+ # Get values if value is None
425
+ if value is None:
426
+ if key is None:
427
+ # Return list of property names when no parameters are given
428
+ if index is None:
429
+ return list(self.view.keys())
430
+ # Return copy of slice if only index is given
431
+ else:
432
+ return deepcopy(self[index])
433
+ # If key is given, return copy of property values
434
+ else:
435
+ if index is None:
436
+ return deepcopy(self.view[key])
437
+ else:
438
+ return deepcopy(self.view[key][index])
439
+
440
+ # Set values if value is given
441
+ else:
442
+ # If no key, set value to atoms (or a slice)
443
+ if key is None:
444
+ if not isinstance(value, Atoms):
445
+ raise TypeError('If key is None, value must be instance of atomman.Atoms')
446
+ if index is None:
447
+ self[:] = value
448
+ else:
449
+ self[index] = value
450
+
451
+ # If key is given, set copy of value to property
452
+ else:
453
+ if index is None:
454
+ self.view[key] = deepcopy(value)
455
+ else:
456
+ self.view[key][index] = value
457
+
458
+ def prop_atype(self,
459
+ key: str,
460
+ value: Any,
461
+ atype: Optional[int] = None):
462
+ """
463
+ Allows for per-atom properties to be assigned according to
464
+ Atoms.atypes.
465
+
466
+ Parameters
467
+ ----------
468
+ key : str
469
+ Per-atom property name.
470
+ value : list, any
471
+ Property value(s) to assign. If atype is not given, this should be
472
+ an object of length Atoms.natypes. Otherwise, should be a single per-atom
473
+ value.
474
+ atype : int, optional
475
+ A specific atype to assign value to.
476
+
477
+ Raises
478
+ ------
479
+ ValueError
480
+ If length of value does not match Atoms.natypes or atype is not in
481
+ Atoms.atypes.
482
+ """
483
+
484
+ # Set values across all atype values
485
+ if atype is None:
486
+ value = np.asarray(value)
487
+ if len(value) >= self.natypes:
488
+ self.view[key] = value[self.atype - 1] # pylint: disable=no-member
489
+ else:
490
+ raise ValueError('length of value less than natypes')
491
+
492
+ # Set values for only one atype
493
+ else:
494
+ if atype in self.atypes:
495
+ if key not in self.prop():
496
+ self.view[key] = np.zeros_like(value)
497
+ self.view[key][self.atype==atype] = value # pylint: disable=no-member
498
+ else:
499
+ raise ValueError('atype not found')
500
+
501
+ def df(self) -> pd.DataFrame:
502
+ """
503
+ Returns a pandas.DataFrame of all atomic properties. Multi-dimensional
504
+ per-atom data will be converted into multiple table columns.
505
+ """
506
+
507
+ # Initialize new dictionary of values
508
+ values = OrderedDict()
509
+ for key in self.view.keys():
510
+
511
+ # Flatten multidimensional arrays
512
+ for index, istr in indexstr(self.view[key].shape[1:]):
513
+ newkey = key + istr
514
+
515
+ # Copy values over
516
+ if index == ():
517
+ values[newkey] = self.view[key]
518
+ else:
519
+ values[newkey] = self.view[key][(Ellipsis, ) + index]
520
+
521
+ # Return DataFrame
522
+ return pd.DataFrame(values)
523
+
524
+ def extend(self, value: Union[Atoms, int]) -> Atoms:
525
+ """
526
+ Allows additional atoms to be added to the end of the atoms list.
527
+
528
+ Parameters
529
+ ----------
530
+ value : atomman.Atoms or int
531
+ An int value will result in the atoms object being extended by
532
+ that number of atoms, with all per-atom properties having default
533
+ values (atype = 1, everything else = 0). For an Atoms value, the
534
+ current atoms list will be extended by the correct number of atoms
535
+ and all per-atom properties in value will be copied over. Any
536
+ properties defined in one Atoms object and not the other will be
537
+ set to default values.
538
+
539
+ Returns
540
+ -------
541
+ atomman.Atoms
542
+ A new Atoms object containing all atoms and properties of the
543
+ current object plus the additional atoms.
544
+ """
545
+
546
+ # Handle different value types
547
+ if isinstance(value, (int, np.integer)):
548
+ natoms = value
549
+ atoms = Atoms(natoms=natoms)
550
+ elif isinstance(value, Atoms):
551
+ natoms = value.natoms
552
+ atoms = value
553
+ else:
554
+ raise TypeError('can only add Atoms or an int # of atoms')
555
+
556
+ # Generate newatoms by copying values of self + natoms extras
557
+ index = list(range(self.natoms)) + [0 for i in range(natoms)]
558
+ newatoms = self[index]
559
+
560
+ # Create empty values for atoms.props not in newatoms (self)
561
+ for prop in atoms.prop():
562
+ if prop not in newatoms.prop():
563
+ newatoms.view[prop] = np.zeros((newatoms.natoms, ) + atoms.view[prop][0].shape, dtype=atoms.view[prop][0].dtype)
564
+
565
+ # Copy values to the extra atoms in newatoms
566
+ for prop in newatoms.prop():
567
+ if prop in atoms.prop():
568
+ newatoms.view[prop][self.natoms:] = atoms.view[prop]
569
+ else:
570
+ newatoms.view[prop][self.natoms:] = np.zeros((natoms, ) + self.view[prop][0].shape, dtype=self.view[prop][0].dtype)
571
+
572
+ return newatoms
atomman/source/atomman/core/Box.py ADDED
@@ -0,0 +1,1112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+
3
+ # Standard Python libraries
4
+ from __future__ import annotations
5
+ from copy import deepcopy
6
+ import io
7
+ from typing import Any, Optional, Union, Tuple
8
+
9
+ # http://www.numpy.org/
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+
13
+ # https://github.com/usnistgov/DataModelDict
14
+ from DataModelDict import DataModelDict as DM
15
+
16
+ # local imports
17
+ import atomman.unitconvert as uc
18
+ from ..tools import vect_angle, miller
19
+ from ..region import Shape, Plane
20
+
21
+ class Box(Shape, object):
22
+ """
23
+ A representation of a triclinic (parallelepiped) box.
24
+ """
25
+
26
+ def __init__(self, **kwargs):
27
+ """
28
+ Initializes a Box. If parameters besides origin are given they
29
+ must completely define the box. Allowed parameter sets are:
30
+
31
+ - no parameters -> box is set to square unit box with origin = [0,0,0].
32
+
33
+ - origin. -> Only origin is changed (same as setting origin directly).
34
+
35
+ - vects, (and origin).
36
+
37
+ - avect, bvect, cvect, (and origin).
38
+
39
+ - a, b, c, (alpha, beta, gamma, and origin).
40
+
41
+ - lx, ly, lz, (xy, xz, yz, and origin).
42
+
43
+ - xlo, xhi, ylo, yhi, zlo, zhi, (xy, xz, and yz).
44
+
45
+ - model
46
+
47
+ See the description of class methods and attributes for more details
48
+ on the allowed parameters.
49
+
50
+ """
51
+ self.__vects = np.eye(3, dtype='float64')
52
+ self.__origin = np.zeros(3, dtype='float64')
53
+ self.__reciprocal_vects = None
54
+
55
+ if len(kwargs) > 0:
56
+ if 'model' in kwargs:
57
+ if len(kwargs) > 1:
58
+ raise ValueError('model cannot be given with other parameters')
59
+ self.model(kwargs['model'])
60
+ else:
61
+ self.set(**kwargs)
62
+
63
+ @classmethod
64
+ def cubic(cls, a: float) -> Box:
65
+ """
66
+ Initializes a Box in standard cubic setting using only cubic lattice
67
+ parameters.
68
+
69
+ a = b = c, alpha = beta = gamma = 90
70
+
71
+ Parameters
72
+ ----------
73
+ - a : float
74
+ The a lattice constant
75
+
76
+ Returns
77
+ -------
78
+ atomman.Box
79
+ """
80
+ return cls(a=a, b=a, c=a, alpha=90, beta=90, gamma=90)
81
+
82
+ @classmethod
83
+ def hexagonal(cls, a: float, c: float) -> Box:
84
+ """
85
+ Initializes a Box in standard hexagonal setting using only hexagonal lattice
86
+ parameters.
87
+
88
+ a = b != c, alpha = beta = 90, gamma = 120
89
+
90
+ Parameters
91
+ ----------
92
+ - a : float
93
+ The a lattice constant
94
+ - c : float
95
+ The c lattice constant
96
+
97
+ Returns
98
+ -------
99
+ atomman.Box
100
+ """
101
+ if a == c:
102
+ raise ValueError('hexagonal lattice constants must be different')
103
+
104
+ return cls(a=a, b=a, c=c, alpha=90, beta=90, gamma=120)
105
+
106
+ @classmethod
107
+ def tetragonal(cls, a: float, c: float) -> Box:
108
+ """
109
+ Initializes a Box in standard tetragonal setting using only tetragonal lattice
110
+ parameters.
111
+
112
+ a = b != c, alpha = beta = gamma = 90
113
+
114
+ Parameters
115
+ ----------
116
+ - a : float
117
+ The a lattice constant
118
+ - c : float
119
+ The c lattice constant
120
+
121
+ Returns
122
+ -------
123
+ atomman.Box
124
+ """
125
+ if a == c:
126
+ raise ValueError('tetragonal lattice constants must be different')
127
+
128
+ return cls(a=a, b=a, c=c, alpha=90, beta=90, gamma=90)
129
+
130
+ @classmethod
131
+ def trigonal(cls, a: float, alpha: float) -> Box:
132
+ """
133
+ Initializes a Box in standard trigonal setting using only trigonal lattice
134
+ parameters.
135
+
136
+ a = b = c, alpha = beta = gamma < 120
137
+
138
+ Parameters
139
+ ----------
140
+ - a : float
141
+ The a lattice constant
142
+ - alpha : float
143
+ The alpha lattice angle in degrees
144
+
145
+ Returns
146
+ -------
147
+ atomman.Box
148
+ """
149
+ if alpha >= 120.0:
150
+ raise ValueError('trigonal alpha angle must be less than 120 degrees')
151
+
152
+ return cls(a=a, b=a, c=a, alpha=alpha, beta=alpha, gamma=alpha)
153
+
154
+ @classmethod
155
+ def orthorhombic(cls, a: float, b: float, c: float) -> Box:
156
+ """
157
+ Initializes a Box in standard orthorhombic setting using only orthorhombic lattice
158
+ parameters.
159
+
160
+ a != b != c, alpha = beta = gamma = 90
161
+
162
+ Parameters
163
+ ----------
164
+ - a : float
165
+ The a lattice constant
166
+ - b : float
167
+ The b lattice constant
168
+ - c : float
169
+ The c lattice constant
170
+
171
+ Returns
172
+ -------
173
+ atomman.Box
174
+ """
175
+ if a == b or a == c:
176
+ raise ValueError('orthorhombic lattice constants must be different')
177
+
178
+ return cls(a=a, b=b, c=c, alpha=90, beta=90, gamma=90)
179
+
180
+ @classmethod
181
+ def monoclinic(cls, a: float, b: float, c: float, beta: float) -> Box:
182
+ """
183
+ Initializes a Box in standard monoclinic setting using only monoclinic lattice
184
+ parameters.
185
+
186
+ a != b != c, alpha = gamma = 90, beta > 90
187
+
188
+ Parameters
189
+ ----------
190
+ - a : float
191
+ The a lattice constant
192
+ - b : float
193
+ The b lattice constant
194
+ - c : float
195
+ The c lattice constant
196
+ - beta : float
197
+ The beta lattice angle in degrees
198
+
199
+ Returns
200
+ -------
201
+ atomman.Box
202
+ """
203
+ if a == b or a == c:
204
+ raise ValueError('monoclinic lattice constants must be different')
205
+ if beta <= 90.0:
206
+ raise ValueError('monoclinic angle beta must be greater than 90 degrees')
207
+
208
+ return cls(a=a, b=b, c=c, alpha=90, beta=beta, gamma=90)
209
+
210
+ @classmethod
211
+ def triclinic(cls, a: float, b: float, c: float,
212
+ alpha: float, beta: float, gamma: float) -> Box:
213
+ """
214
+ Initializes a Box in standard triclinic setting using only triclinic lattice
215
+ parameters.
216
+
217
+ a != b != c, alpha != beta != gamma
218
+
219
+ Parameters
220
+ ----------
221
+ - a : float
222
+ The a lattice constant
223
+ - b : float
224
+ The b lattice constant
225
+ - c : float
226
+ The c lattice constant
227
+ - alpha : float
228
+ The alpha lattice angle in degrees
229
+ - beta : float
230
+ The beta lattice angle in degrees
231
+ - gamma : float
232
+ The gamma lattice angle in degrees
233
+
234
+ Returns
235
+ -------
236
+ atomman.Box
237
+ """
238
+ if a == b or a == c:
239
+ raise ValueError('monoclinic lattice constants must be different')
240
+ if alpha == beta or alpha == gamma:
241
+ raise ValueError('monoclinic lattice angles must be different')
242
+
243
+ return cls(a=a, b=b, c=c, alpha=alpha, beta=beta, gamma=gamma)
244
+
245
+ @property
246
+ def vects(self) -> np.ndarray:
247
+ """numpy.ndarray : Array containing all three box vectors. Can be set directly."""
248
+ return deepcopy(self.__vects)
249
+
250
+ @vects.setter
251
+ def vects(self, value: npt.ArrayLike):
252
+ self.__vects[:] = value
253
+
254
+ # Zero out near zero terms
255
+ self.__vects[np.isclose(self.__vects/abs(self.__vects).max(), 0.0, atol=1e-9)] = 0.0
256
+
257
+ # Reset reciprocal_vects
258
+ self.__reciprocal_vects = None
259
+
260
+ @property
261
+ def reciprocal_vects(self) -> np.ndarray:
262
+ """
263
+ numpy.ndarray : Array of the crystallographic reciprocal box vectors.
264
+ These have not been scaled by the factor of 2 pi.
265
+ """
266
+ if self.__reciprocal_vects is None:
267
+ self.__reciprocal_vects = np.linalg.inv(self.vects).T
268
+
269
+ return self.__reciprocal_vects
270
+
271
+ @property
272
+ def origin(self) -> np.ndarray:
273
+ """numpy.ndarray : Box origin position where vects are added to define the box. Can be set directly."""
274
+ return deepcopy(self.__origin)
275
+
276
+ @origin.setter
277
+ def origin(self, value: npt.ArrayLike):
278
+ self.__origin[:] = value
279
+
280
+ @property
281
+ def avect(self) -> np.ndarray:
282
+ """numpy.ndarray : Vector associated with the a box dimension."""
283
+ return self.vects[0]
284
+
285
+ @property
286
+ def bvect(self) -> np.ndarray:
287
+ """numpy.ndarray : Vector associated with the b box dimension."""
288
+ return self.vects[1]
289
+
290
+ @property
291
+ def cvect(self) -> np.ndarray:
292
+ """numpy.ndarray : Vector associated with the c box dimension."""
293
+ return self.vects[2]
294
+
295
+ @property
296
+ def a(self) -> float:
297
+ """float : The a lattice parameter (magnitude of avect)."""
298
+ return (self.__vects[0,0]**2 + self.__vects[0,1]**2 + self.__vects[0,2]**2)**0.5
299
+
300
+ @property
301
+ def b(self) -> float:
302
+ """float : The b lattice parameter (magnitude of avect)."""
303
+ return (self.__vects[1,0]**2 + self.__vects[1,1]**2 + self.__vects[1,2]**2)**0.5
304
+
305
+ @property
306
+ def c(self) -> float:
307
+ """float : The c lattice parameter (magnitude of avect)."""
308
+ return (self.__vects[2,0]**2 + self.__vects[2,1]**2 + self.__vects[2,2]**2)**0.5
309
+
310
+ @property
311
+ def alpha(self) -> float:
312
+ """float : The alpha lattice angle in degrees (angle between bvect and cvect)."""
313
+ return vect_angle(self.__vects[1], self.__vects[2])
314
+
315
+ @property
316
+ def beta(self) -> float:
317
+ """float : The beta lattice angle in degrees (angle between avect and cvect)."""
318
+ return vect_angle(self.__vects[0], self.__vects[2])
319
+
320
+ @property
321
+ def gamma(self) -> float:
322
+ """float : The gamma lattice angle in degrees (angle between avect and bvect)."""
323
+ return vect_angle(self.__vects[0], self.__vects[1])
324
+
325
+ @property
326
+ def lx(self) -> float:
327
+ """float : LAMMPS lx box length (avect[0] for normalized boxes)."""
328
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
329
+ return self.__vects[0,0]
330
+
331
+ @property
332
+ def ly(self) -> float:
333
+ """float : LAMMPS ly box length (bvect[1] for normalized boxes)."""
334
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
335
+ return self.__vects[1,1]
336
+
337
+ @property
338
+ def lz(self) -> float:
339
+ """float : LAMMPS lz box length (cvect[2] for normalized boxes)."""
340
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
341
+ return self.__vects[2,2]
342
+
343
+ @property
344
+ def xy(self) -> float:
345
+ """float : LAMMPS xy box tilt factor (bvect[0] for normalized boxes)."""
346
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
347
+ return self.__vects[1,0]
348
+
349
+ @property
350
+ def xz(self) -> float:
351
+ """float : LAMMPS xz box tilt factor (cvect[0] for normalized boxes)."""
352
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
353
+ return self.__vects[2,0]
354
+
355
+ @property
356
+ def yz(self) -> float:
357
+ """float : LAMMPS yz box tilt factor (cvect[1] for normalized boxes)."""
358
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
359
+ return self.__vects[2,1]
360
+
361
+ @property
362
+ def xlo(self) -> float:
363
+ """float : LAMMPS xlo box lo term (origin[0] for normalized boxes)."""
364
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
365
+ return self.__origin[0]
366
+
367
+ @property
368
+ def ylo(self) -> float:
369
+ """float : LAMMPS ylo box lo term (origin[1] for normalized boxes)."""
370
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
371
+ return self.__origin[1]
372
+
373
+ @property
374
+ def zlo(self) -> float:
375
+ """float : LAMMPS zlo box lo term (origin[2] for normalized boxes)."""
376
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
377
+ return self.__origin[2]
378
+
379
+ @property
380
+ def xhi(self) -> float:
381
+ """float : LAMMPS xhi box hi term (origin[0] + lx for normalized boxes)."""
382
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
383
+ return self.__origin[0] + self.__vects[0,0]
384
+
385
+ @property
386
+ def yhi(self) -> float:
387
+ """float : LAMMPS yhi box hi term (origin[1] + ly for normalized boxes)."""
388
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
389
+ return self.__origin[1] + self.__vects[1,1]
390
+
391
+ @property
392
+ def zhi(self) -> float:
393
+ """float : LAMMPS zhi box hi term (origin[2] + lz for normalized boxes)."""
394
+ assert self.is_lammps_norm(), 'Box is not normalized for LAMMPS style parameters'
395
+ return self.__origin[2] + self.__vects[2,2]
396
+
397
+ @property
398
+ def volume(self) -> float:
399
+ """float : The volume of the box."""
400
+ return np.abs(np.dot(self.avect, np.cross(self.bvect, self.cvect)))
401
+
402
+ @property
403
+ def planes(self) -> Tuple[Plane]:
404
+ """tuple : The box's planes represented as atomman.region.Plane objects."""
405
+ return (Plane(np.cross(self.cvect, self.bvect), self.origin),
406
+ Plane(np.cross(self.avect, self.cvect), self.origin),
407
+ Plane(np.cross(self.bvect, self.avect), self.origin),
408
+ Plane(np.cross(self.bvect, self.cvect), self.origin + self.avect),
409
+ Plane(np.cross(self.cvect, self.avect), self.origin + self.bvect),
410
+ Plane(np.cross(self.avect, self.bvect), self.origin + self.cvect))
411
+
412
+ def __str__(self) -> str:
413
+ """
414
+ The string representation of the box. Lists the three vectors and origin.
415
+ """
416
+ return '\n'.join(['avect = [%6.3f, %6.3f, %6.3f]' % (self.__vects[0,0], self.__vects[0,1], self.__vects[0,2]),
417
+ 'bvect = [%6.3f, %6.3f, %6.3f]' % (self.__vects[1,0], self.__vects[1,1], self.__vects[1,2]),
418
+ 'cvect = [%6.3f, %6.3f, %6.3f]' % (self.__vects[2,0], self.__vects[2,1], self.__vects[2,2]),
419
+ 'origin = [%6.3f, %6.3f, %6.3f]' % (self.__origin[0], self.__origin[1], self.__origin[2])])
420
+
421
+ def model(self,
422
+ model: Union[str, io.IOBase, DM, None] = None,
423
+ length_unit: str = 'angstrom'
424
+ ) -> Optional[DM]:
425
+ """
426
+ Reads or generates a data model for the box.
427
+
428
+ Parameters
429
+ ----------
430
+ model : str or DataModelDict, optional
431
+ JSON/XML formatted data, or path to file containing said data. If
432
+ not given, then a model for the current box will be returned.
433
+ length_unit : str, optional
434
+ Unit of length to save box values in if data model is to be
435
+ generated. Default value is 'angstrom'.
436
+
437
+ Returns
438
+ -------
439
+ DataModelDict.DataModelDict
440
+ A JSON/XML equivalent data model for the box. Returned if model
441
+ is not given
442
+ """
443
+ # Set values if model given
444
+ if model is not None:
445
+
446
+ # Find box element
447
+ model = DM(model).find('box')
448
+ avect = uc.value_unit(model['avect'])
449
+ bvect = uc.value_unit(model['bvect'])
450
+ cvect = uc.value_unit(model['cvect'])
451
+ origin = uc.value_unit(model['origin'])
452
+ self.set(avect=avect, bvect=bvect, cvect=cvect, origin=origin)
453
+
454
+ # Return DataModelDict if model not given
455
+ else:
456
+ model = DM()
457
+ model['box'] = DM()
458
+ model['box']['avect'] = uc.model(self.avect, length_unit)
459
+ model['box']['bvect'] = uc.model(self.bvect, length_unit)
460
+ model['box']['cvect'] = uc.model(self.cvect, length_unit)
461
+ model['box']['origin']= uc.model(self.origin, length_unit)
462
+
463
+ return model
464
+
465
+ def set(self, **kwargs):
466
+ """
467
+ Sets a Box's dimensions. If parameters besides origin are given they
468
+ must completely define the box. Allowed parameter sets are:
469
+
470
+ - no parameters -> box is set to square unit box with origin = [0,0,0].
471
+
472
+ - origin. -> Only origin is changed (same as setting origin directly).
473
+
474
+ - vects, (and origin).
475
+
476
+ - avect, bvect, cvect, (and origin).
477
+
478
+ - a, b, c, (alpha, beta, gamma, and origin).
479
+
480
+ - lx, ly, lz, (xy, xz, yz, and origin).
481
+
482
+ - xlo, xhi, ylo, yhi, zlo, zhi, (xy, xz, and yz).
483
+
484
+ See the description of class methods and attributes for more details
485
+ on the allowed parameters.
486
+ """
487
+
488
+ # Set default values if no kwargs given
489
+ if len(kwargs) == 0:
490
+ self.vects = np.eye(3)
491
+ self.origin = np.zeros(3)
492
+
493
+ # Set directly if vects given
494
+ elif 'vects' in kwargs:
495
+ vects = kwargs.pop('vects')
496
+ origin = kwargs.pop('origin', [0.0, 0.0, 0.0])
497
+ assert len(kwargs) == 0, 'Invalid arguments'
498
+ self.vects = vects
499
+ self.origin = origin
500
+
501
+ # Call set_vectors if vect inputs given
502
+ elif 'avect' in kwargs:
503
+ self.set_vectors(**kwargs)
504
+
505
+ # Call set_lengths if length inputs given
506
+ elif 'lx' in kwargs:
507
+ self.set_lengths(**kwargs)
508
+
509
+ # Call set_hi_los if hi/lo inputs given
510
+ elif 'xlo' in kwargs:
511
+ self.set_hi_los(**kwargs)
512
+
513
+ # Call set_abc if vector magnitudes are given
514
+ elif 'a' in kwargs:
515
+ self.set_abc(**kwargs)
516
+
517
+ # Set only origin if given alone
518
+ elif 'origin' in kwargs:
519
+ origin = kwargs.pop('origin')
520
+ assert len(kwargs) == 0, 'Invalid arguments'
521
+ self.origin = origin
522
+
523
+ else:
524
+ raise TypeError('Invalid arguments')
525
+
526
+ def set_vectors(self,
527
+ avect: npt.ArrayLike,
528
+ bvect: npt.ArrayLike,
529
+ cvect: npt.ArrayLike,
530
+ origin: Optional[npt.ArrayLike] = None):
531
+ """
532
+ Set the box using the three box vectors.
533
+
534
+ Parameters
535
+ ----------
536
+ avect : array-like object
537
+ The 3D vector for the a box dimension.
538
+ bvect : array-like object
539
+ The 3D vector for the b box dimension.
540
+ cvect : array-like object
541
+ The 3D vector for the c box dimension.
542
+ origin : array-like object, optional
543
+ The 3D vector for the box origin position. Default value is
544
+ (0,0,0).
545
+ """
546
+ # Set default origin
547
+ if origin is None:
548
+ origin = [0.0, 0.0, 0.0]
549
+
550
+ # Combine avect, bvect and cvect into vects and set directly
551
+ self.vects = [avect, bvect, cvect]
552
+ self.origin = origin
553
+
554
+ def set_abc(self,
555
+ a: float, b: float, c: float,
556
+ alpha: float = 90.0,
557
+ beta: float = 90.0,
558
+ gamma: float = 90.0,
559
+ origin: Optional[npt.ArrayLike] = None):
560
+ """
561
+ Set the box using crystal cell lattice parameters and angles.
562
+
563
+ Parameters
564
+ ----------
565
+ a : float
566
+ The a lattice parameter.
567
+ b : float
568
+ The b lattice parameter.
569
+ c : float
570
+ The c lattice parameter.
571
+ alpha : float, optional
572
+ The alpha lattice angle in degrees (angle between b and c vectors).
573
+ Default value is 90.0.
574
+ beta : float, optional
575
+ The beta lattice angle in degrees (angle between a and c vectors).
576
+ Default value is 90.0.
577
+ gamma : float, optional
578
+ The gamma lattice angle in degrees (angle between a and b vectors).
579
+ Default value is 90.0.
580
+ origin : array-like object, optional
581
+ The 3D vector for the box origin position. Default value is
582
+ (0,0,0).
583
+ """
584
+ # Check that angles are between 0 and 180
585
+ if alpha <= 0 or alpha >=180 or beta <= 0 or beta >= 180 or gamma <=0 or gamma >=180:
586
+ raise ValueError('lattice angles must be between 0 and 180 degrees')
587
+
588
+ # Convert to lx, ly, lz, xy, xz, yz
589
+ lx = a
590
+ xy = b * np.cos(gamma * np.pi / 180)
591
+ xz = c * np.cos(beta * np.pi / 180)
592
+ ly = (b**2 - xy**2)**0.5
593
+ yz = (b * c * np.cos(alpha * np.pi / 180) - xy * xz) / ly
594
+ lz = (c**2 - xz**2 - yz**2)**0.5
595
+
596
+ # Call set_lengths
597
+ self.set_lengths(lx=lx, ly=ly, lz=lz, xy=xy, xz=xz, yz=yz, origin=origin)
598
+
599
+ def set_lengths(self,
600
+ lx: float, ly: float, lz: float,
601
+ xy: float = 0.0, xz: float = 0.0, yz: float = 0.0,
602
+ origin: Optional[npt.ArrayLike] = None):
603
+ """
604
+ Set the box using LAMMPS box lengths and tilt factors.
605
+
606
+ Parameters
607
+ ----------
608
+ lx : float
609
+ The LAMMPS box length in the x direction.
610
+ ly : float
611
+ The LAMMPS box length in the y direction.
612
+ lz : float
613
+ The LAMMPS box length in the z direction.
614
+ xy : float, optional
615
+ The LAMMPS box tilt factor in the xy direction. Default value is
616
+ 0.0.
617
+ xz : float, optional
618
+ The LAMMPS box tilt factor in the xz direction. Default value is
619
+ 0.0.
620
+ yz : float, optional
621
+ The LAMMPS box tilt factor in the yz direction. Default value is
622
+ 0.0.
623
+ origin : array-like object, optional
624
+ The 3D vector for the box origin position. Default value is
625
+ (0,0,0).
626
+ """
627
+
628
+ assert lx > 0 and ly > 0 and lz > 0, 'box lengths must be positive'
629
+
630
+ # Set default origin
631
+ if origin is None:
632
+ origin = [0.0, 0.0, 0.0]
633
+
634
+ # Construct vects array
635
+ self.vects = [[lx, 0.0, 0.0],
636
+ [xy, ly, 0.0],
637
+ [xz, yz, lz]]
638
+ self.origin = origin
639
+
640
+ def set_hi_los(self,
641
+ xlo: float, xhi: float,
642
+ ylo: float, yhi: float,
643
+ zlo: float, zhi: float,
644
+ xy: float = 0.0, xz: float = 0.0, yz: float = 0.0):
645
+ """
646
+ Set the box using LAMMPS box hi's, lo's and tilt factors.
647
+
648
+ Parameters
649
+ ----------
650
+ xlo : float
651
+ The LAMMPS xlo box lo term.
652
+ xhi : float
653
+ The LAMMPS xhi box hi term.
654
+ ylo : float
655
+ The LAMMPS ylo box lo term.
656
+ yhi : float
657
+ The LAMMPS yhi box hi term.
658
+ zlo : float
659
+ The LAMMPS zlo box lo term.
660
+ zhi : float
661
+ The LAMMPS zhi box hi term.
662
+ xy : float, optional
663
+ The LAMMPS box tilt factor in the xy direction. Default value is
664
+ 0.0.
665
+ xz : float, optional
666
+ The LAMMPS box tilt factor in the xz direction. Default value is
667
+ 0.0.
668
+ yz : float, optional
669
+ The LAMMPS box tilt factor in the yz direction. Default value is
670
+ 0.0.
671
+ """
672
+
673
+ # Convert to hi and lo term to lengths and origin
674
+ lx = xhi - xlo
675
+ ly = yhi - ylo
676
+ lz = zhi - zlo
677
+ origin = [xlo, ylo, zlo]
678
+
679
+ # Call set_lengths
680
+ self.set_lengths(lx=lx, ly=ly, lz=lz, xy=xy, xz=xz, yz=yz, origin=origin)
681
+
682
+ def is_lammps_norm(self) -> bool:
683
+ """
684
+ Tests if box is compatible with LAMMPS.
685
+ Note: large box tilt factors not checked. The LAMMPS command
686
+ 'box tilt large' may be needed to run LAMMPS.
687
+ """
688
+ return (self.__vects[0,1] == 0.0
689
+ and self.__vects[0,2] == 0.0
690
+ and self.__vects[1,2] == 0.0
691
+ and self.__vects[0,0] > 0.0
692
+ and self.__vects[1,1] > 0.0
693
+ and self.__vects[2,2] > 0.0)
694
+
695
+ def inside(self,
696
+ pos: npt.ArrayLike,
697
+ inclusive: bool = True) -> np.ndarray:
698
+ """
699
+ Indicates if position(s) are inside the shape.
700
+
701
+ Parameters
702
+ ----------
703
+ pos : array-like object
704
+ Nx3 array of coordinates.
705
+ inclusive : bool, optional
706
+ Indicates if points on the shape's boundaries are to be included.
707
+ Default value is True.
708
+
709
+ Returns
710
+ -------
711
+ numpy.NDArray
712
+ N array of bool values: True if inside shape
713
+ """
714
+ # Retrieve the Box's planes
715
+ planes = self.planes
716
+
717
+ # Find all points below each plane, i.e. inside the box
718
+ return ( planes[0].below(pos, inclusive=inclusive)
719
+ & planes[1].below(pos, inclusive=inclusive)
720
+ & planes[2].below(pos, inclusive=inclusive)
721
+ & planes[3].below(pos, inclusive=inclusive)
722
+ & planes[4].below(pos, inclusive=inclusive)
723
+ & planes[5].below(pos, inclusive=inclusive))
724
+
725
+ def vector_crystal_to_cartesian(self, indices: npt.ArrayLike) -> np.ndarray:
726
+ """
727
+ Converts crystal indices to Cartesian vectors relative
728
+ to the box's lattice vectors.
729
+
730
+ Parameters
731
+ ----------
732
+ indices : array-like object
733
+ (..., 3) array of [uvw] Miller crystallographic indices or
734
+ (..., 4) array of [uvtw] Miller-Bravais crystallographic indices.
735
+
736
+ Returns
737
+ -------
738
+ np.ndarray of float
739
+ (..., 3) array of Cartesian vectors.
740
+
741
+ Raises
742
+ ------
743
+ ValueError
744
+ If indices dimensions are not (..., 3) or (..., 4), or if
745
+ hexagonal indices given with non-hexagonal box.
746
+ """
747
+ return miller.vector_crystal_to_cartesian(indices, self)
748
+
749
+ def plane_crystal_to_cartesian(self, indices: npt.ArrayLike) -> np.ndarray:
750
+ """
751
+ Converts crystal planar indices to Cartesian plane normal vectors based
752
+ on the box's lattice vectors. Note: the algorithm used requires that the
753
+ planar indices be integers.
754
+
755
+ Parameters
756
+ ----------
757
+ indices : array-like object
758
+ (..., 3) array of [hkl] Miller crystallographic indices or
759
+ (..., 4) array of [hkil] Miller-Bravais crystallographic indices.
760
+ box : atomman.Box
761
+ Box that defines the lattice cell vectors to use.
762
+
763
+ Returns
764
+ -------
765
+ np.ndarray of float
766
+ (..., 3) array of Cartesian vectors corresponding to plane normals.
767
+
768
+ Raises
769
+ ------
770
+ ValueError
771
+ If indices dimensions are not (..., 3) or (..., 4), or if
772
+ hexagonal indices given with non-hexagonal box.
773
+ """
774
+ return miller.plane_crystal_to_cartesian(indices, self)
775
+
776
+ def position_relative_to_cartesian(self, relpos: npt.ArrayLike) -> np.ndarray:
777
+ """
778
+ Converts position vectors from relative box coordinates to absolute
779
+ Cartesian coordinates based on the box's vects and origin.
780
+
781
+ Parameters
782
+ ----------
783
+ relpos : array-like object
784
+ (..., 3) array of relative position vectors.
785
+
786
+ Returns
787
+ -------
788
+ numpy.ndarray
789
+ (..., 3) array of the absolute Cartesian positions corresponding
790
+ to relpos.
791
+
792
+ Raises
793
+ ------
794
+ ValueError
795
+ If relpos dimensions are not (..., 3).
796
+ """
797
+ # Check/convert relpos
798
+ relpos = np.asarray(relpos, dtype=float)
799
+ if relpos.shape[-1] != 3:
800
+ raise ValueError('Invalid position dimensions')
801
+
802
+ # Convert and return
803
+ return relpos.dot(self.vects) + self.origin
804
+
805
+ def position_cartesian_to_relative(self, cartpos: npt.ArrayLike) -> np.ndarray:
806
+ """
807
+ Converts position vectors from absolute Cartesian coordinates to
808
+ relative box coordinates based on the box's vects and origin.
809
+
810
+ Parameters
811
+ ----------
812
+ cartpos : array-like object
813
+ (..., 3) array of Cartesian position vectors.
814
+
815
+ Returns
816
+ -------
817
+ numpy.ndarray
818
+ (..., 3) array of the relative positions corresponding
819
+ to cartpos.
820
+
821
+ Raises
822
+ ------
823
+ ValueError
824
+ If cartpos dimensions are not (..., 3).
825
+ """
826
+ # Check/convert cartpos
827
+ cartpos = np.asarray(cartpos, dtype=float)
828
+ if cartpos.shape[-1] != 3:
829
+ raise ValueError('Invalid position dimensions')
830
+
831
+ # Convert and return
832
+ return np.inner((cartpos - self.origin), self.reciprocal_vects)
833
+
834
+ def iscubic(self,
835
+ rtol: float = 1e-05,
836
+ atol: float = 1e-08) -> bool:
837
+ """
838
+ Tests if the box is consistent with a standard cubic cell:
839
+ a = b = c
840
+ alpha = beta = gamma = 90
841
+
842
+ Parameters
843
+ ----------
844
+ rtol : float, optional
845
+ Relative tolerance for testing box parameters. Default value is 1e-5.
846
+ atol : float, optional
847
+ Absolute tolerance for testing box parameters. Default value is 1e-8.
848
+
849
+ Returns
850
+ -------
851
+ bool
852
+ True if the box is a standard cubic cell, False otherwise.
853
+ """
854
+ return (np.isclose(self.a, self.b, atol=atol, rtol=rtol)
855
+ and np.isclose(self.a, self.c, atol=atol, rtol=rtol)
856
+ and np.isclose(self.alpha, 90.0, atol=atol, rtol=rtol)
857
+ and np.isclose(self.beta, 90.0, atol=atol, rtol=rtol)
858
+ and np.isclose(self.gamma, 90.0, atol=atol, rtol=rtol))
859
+
860
+ def ishexagonal(self,
861
+ rtol: float = 1e-05,
862
+ atol: float = 1e-08) -> bool:
863
+ """
864
+ Tests if the box is consistent with a standard hexagonal cell:
865
+ a = b != c
866
+ alpha = beta = 90
867
+ gamma = 120
868
+
869
+ Parameters
870
+ ----------
871
+ rtol : float, optional
872
+ Relative tolerance for testing box parameters. Default value is 1e-5.
873
+ atol : float, optional
874
+ Absolute tolerance for testing box parameters. Default value is 1e-8.
875
+
876
+ Returns
877
+ -------
878
+ bool
879
+ True if the box is a standard hexagonal cell, False otherwise.
880
+ """
881
+ return (np.isclose(self.a, self.b, atol=atol, rtol=rtol)
882
+ and np.isclose(self.alpha, 90.0, atol=atol, rtol=rtol)
883
+ and np.isclose(self.beta, 90.0, atol=atol, rtol=rtol)
884
+ and np.isclose(self.gamma, 120.0, atol=atol, rtol=rtol))
885
+
886
+ def istetragonal(self,
887
+ rtol: float = 1e-05,
888
+ atol: float = 1e-08) -> bool:
889
+ """
890
+ Tests if the box is consistent with a standard tetragonal cell:
891
+ a = b != c
892
+ alpha = beta = gamma = 90
893
+
894
+ Parameters
895
+ ----------
896
+ rtol : float, optional
897
+ Relative tolerance for testing box parameters. Default value is 1e-5.
898
+ atol : float, optional
899
+ Absolute tolerance for testing box parameters. Default value is 1e-8.
900
+
901
+ Returns
902
+ -------
903
+ bool
904
+ True if the box is a standard tetragonal cell, False otherwise.
905
+ """
906
+ return (np.isclose(self.a, self.b, atol=atol, rtol=rtol)
907
+ and not np.isclose(self.a, self.c, atol=atol, rtol=rtol)
908
+ and np.isclose(self.alpha, 90.0, atol=atol, rtol=rtol)
909
+ and np.isclose(self.beta, 90.0, atol=atol, rtol=rtol)
910
+ and np.isclose(self.gamma, 90.0, atol=atol, rtol=rtol))
911
+
912
+ def isrhombohedral(self,
913
+ rtol: float = 1e-05,
914
+ atol: float = 1e-08) -> bool:
915
+ """
916
+ Tests if the box is consistent with a standard rhombohedral cell:
917
+ a = b = c
918
+ alpha = beta = gamma != 90
919
+
920
+ Parameters
921
+ ----------
922
+ rtol : float, optional
923
+ Relative tolerance for testing box parameters. Default value is 1e-5.
924
+ atol : float, optional
925
+ Absolute tolerance for testing box parameters. Default value is 1e-8.
926
+
927
+ Returns
928
+ -------
929
+ bool
930
+ True if the box is a standard rhombohedral cell, False otherwise.
931
+ """
932
+ return (np.isclose(self.a, self.b, atol=atol, rtol=rtol)
933
+ and np.isclose(self.a, self.c, atol=atol, rtol=rtol)
934
+ and np.isclose(self.alpha, self.beta, atol=atol, rtol=rtol)
935
+ and np.isclose(self.alpha, self.gamma, atol=atol, rtol=rtol)
936
+ and not np.isclose(self.alpha, 90.0, atol=atol, rtol=rtol))
937
+
938
+ def isorthorhombic(self,
939
+ rtol: float = 1e-05,
940
+ atol: float = 1e-08) -> bool:
941
+ """
942
+ Tests if the box is consistent with a standard orthorhombic cell:
943
+ a != b != c
944
+ alpha = beta = gamma = 90
945
+
946
+ Parameters
947
+ ----------
948
+ rtol : float, optional
949
+ Relative tolerance for testing box parameters. Default value is 1e-5.
950
+ atol : float, optional
951
+ Absolute tolerance for testing box parameters. Default value is 1e-8.
952
+
953
+ Returns
954
+ -------
955
+ bool
956
+ True if the box is a standard orthorhombic cell, False otherwise.
957
+ """
958
+ return (not np.isclose(self.a, self.b, atol=atol, rtol=rtol)
959
+ and not np.isclose(self.a, self.c, atol=atol, rtol=rtol)
960
+ and np.isclose(self.alpha, 90.0, atol=atol, rtol=rtol)
961
+ and np.isclose(self.beta, 90.0, atol=atol, rtol=rtol)
962
+ and np.isclose(self.gamma, 90.0, atol=atol, rtol=rtol))
963
+
964
+ def ismonoclinic(self,
965
+ rtol: float = 1e-05,
966
+ atol: float = 1e-08) -> bool:
967
+ """
968
+ Tests if the box is consistent with a standard monoclinic cell:
969
+ a != b != c
970
+ alpha = gamma = 90
971
+ beta != 90
972
+
973
+ Parameters
974
+ ----------
975
+ rtol : float, optional
976
+ Relative tolerance for testing box parameters. Default value is 1e-5.
977
+ atol : float, optional
978
+ Absolute tolerance for testing box parameters. Default value is 1e-8.
979
+
980
+ Returns
981
+ -------
982
+ bool
983
+ True if box is a standard monoclinic cell, False otherwise.
984
+ """
985
+ return (not np.isclose(self.a, self.b, atol=atol, rtol=rtol)
986
+ and not np.isclose(self.a, self.c, atol=atol, rtol=rtol)
987
+ and np.isclose(self.alpha, 90.0, atol=atol, rtol=rtol)
988
+ and not np.isclose(self.beta, 90.0, atol=atol, rtol=rtol)
989
+ and np.isclose(self.gamma, 90.0, atol=atol, rtol=rtol))
990
+
991
+ def istriclinic(self,
992
+ rtol: float = 1e-05,
993
+ atol: float = 1e-08) -> bool:
994
+ """
995
+ Tests if the box is consistent with a standard triclinic cell:
996
+ a != b != c
997
+ alpha != 90
998
+ beta != 90
999
+ gamma != 90
1000
+
1001
+ Parameters
1002
+ ----------
1003
+ rtol : float, optional
1004
+ Relative tolerance for testing box parameters. Default value is 1e-5.
1005
+ atol : float, optional
1006
+ Absolute tolerance for testing box parameters. Default value is 1e-8.
1007
+
1008
+ Returns
1009
+ -------
1010
+ bool
1011
+ True if box is a standard triclinic cell, False otherwise.
1012
+ """
1013
+ return (not np.isclose(self.a, self.b, atol=atol, rtol=rtol)
1014
+ and not np.isclose(self.a, self.c, atol=atol, rtol=rtol)
1015
+ and not np.isclose(self.alpha, self.beta, atol=atol, rtol=rtol)
1016
+ and not np.isclose(self.alpha, self.gamma, atol=atol, rtol=rtol))
1017
+
1018
+ def identifyfamily(self,
1019
+ rtol: float = 1e-05,
1020
+ atol: float = 1e-08) -> Optional[str]:
1021
+ """
1022
+ Tests if the box is consistent with a standard representation
1023
+ of a crystal system cell.
1024
+
1025
+ Parameters
1026
+ ----------
1027
+ rtol : float, optional
1028
+ Relative tolerance for testing box parameters. Default value is 1e-5.
1029
+ atol : float, optional
1030
+ Absolute tolerance for testing box parameters. Default value is 1e-8.
1031
+
1032
+ Returns
1033
+ -------
1034
+ str or None
1035
+ 'cubic', 'hexagonal', 'tetragonal', 'rhombohedral', 'orthorhombic',
1036
+ 'monoclinic' or 'triclinic' if it matches any. None if no matches.
1037
+
1038
+ Raises
1039
+ ------
1040
+ ValueError
1041
+ If the box is not consistent with a standard cell.
1042
+ """
1043
+ if self.iscubic(rtol=rtol, atol=atol):
1044
+ return 'cubic'
1045
+ elif self.ishexagonal(rtol=rtol, atol=atol):
1046
+ return 'hexagonal'
1047
+ elif self.istetragonal(rtol=rtol, atol=atol):
1048
+ return 'tetragonal'
1049
+ elif self.isrhombohedral(rtol=rtol, atol=atol):
1050
+ return 'rhombohedral'
1051
+ elif self.isorthorhombic(rtol=rtol, atol=atol):
1052
+ return 'orthorhombic'
1053
+ elif self.ismonoclinic(rtol=rtol, atol=atol):
1054
+ return 'monoclinic'
1055
+ elif self.istriclinic(rtol=rtol, atol=atol):
1056
+ return 'triclinic'
1057
+ else:
1058
+ None
1059
+
1060
+ def d_hkl(self, plane_hkl):
1061
+ """
1062
+ Compute the interplanar spacing for a lattice plane. Note this feature
1063
+ is expected to be used for only unit cell boxes!
1064
+
1065
+ Parameters
1066
+ ----------
1067
+ plane_hkl : array-like
1068
+ The 3 index Miller or 4 index Miller-Bravais lattice plane. Values
1069
+ typically are ints, but can be floats if the cell basis is not
1070
+ primitive.
1071
+
1072
+ Returns
1073
+ -------
1074
+ d_hkl : float
1075
+ The spacing between the lattice planes of the given type.
1076
+ """
1077
+ # Check hkl values
1078
+ plane_hkl = np.asarray(plane_hkl)
1079
+ if plane_hkl.shape == (4,):
1080
+ if not self.ishexagonal():
1081
+ raise ValueError('Box is not hexagonal: cannot use 4 index Miller-Bravais plane')
1082
+ plane_hkl = miller.plane4to3(plane_hkl)
1083
+ elif plane_hkl.shape != (3,):
1084
+ raise ValueError('plane_hkl must have 3 or 4 indices')
1085
+ h = plane_hkl[0]
1086
+ k = plane_hkl[1]
1087
+ l = plane_hkl[2]
1088
+
1089
+ a = self.a
1090
+ b = self.b
1091
+ c = self.c
1092
+
1093
+ # Compute sin and cos of the angles
1094
+ sin_alpha = np.sin(self.alpha / 180 * np.pi)
1095
+ sin_beta = np.sin(self.beta / 180 * np.pi)
1096
+ sin_gamma = np.sin(self.gamma / 180 * np.pi)
1097
+ cos_alpha = np.cos(self.alpha / 180 * np.pi)
1098
+ cos_beta = np.cos(self.beta / 180 * np.pi)
1099
+ cos_gamma = np.cos(self.gamma / 180 * np.pi)
1100
+
1101
+ # Compute the triclinic 1/d_hkl^2 formula
1102
+ numerator = (
1103
+ h**2 / a**2 * sin_alpha**2 +
1104
+ k**2 / b**2 * sin_beta**2 +
1105
+ l**2 / c**2 * sin_gamma**2 +
1106
+ (2 * k * l / b * c) * (cos_beta * cos_gamma - cos_alpha) +
1107
+ (2 * h * l / a * c) * (cos_gamma * cos_alpha - cos_beta) +
1108
+ (2 * h * k / a * b) * (cos_alpha * cos_beta - cos_gamma))
1109
+ denominator = (1 - cos_alpha**2 - cos_beta**2 - cos_gamma**2 +
1110
+ 2 * cos_alpha * cos_beta * cos_gamma)
1111
+
1112
+ return (numerator / denominator)**-0.5
atomman/source/atomman/core/ElasticConstants.py ADDED
@@ -0,0 +1,1037 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ from __future__ import annotations
4
+ from copy import deepcopy
5
+ import io
6
+ from typing import Optional, Union
7
+
8
+ # http://www.numpy.org/
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+
12
+ # https://github.com/usnistgov/DataModelDict
13
+ from DataModelDict import DataModelDict as DM
14
+
15
+ # atomman imports
16
+ from ..tools import axes_check
17
+ import atomman.unitconvert as uc
18
+
19
+ class ElasticConstants(object):
20
+ """Class for storing and converting elastic constant values"""
21
+
22
+ def __init__(self, **kwargs):
23
+ """
24
+ Initilizes an ElasticConstants instance from one of the parameter
25
+ options.
26
+
27
+ Parameters
28
+ ----------
29
+ Cij : numpy.ndarray
30
+ (6, 6) array of Voigt representation of elastic stiffness.
31
+ Sij : numpy.ndarray
32
+ (6, 6) array of Voigt representation of elastic compliance.
33
+ Cij9 : numpy.ndarray
34
+ (9, 9) array representation of elastic stiffness.
35
+ Cijkl : numpy.ndarray
36
+ (3, 3, 3, 3) array representation of elastic stiffness.
37
+ Sijkl : numpy.ndarray
38
+ (3, 3, 3, 3) array representation of elastic compliance.
39
+ model : DataModelDict, string, or file-like object
40
+ Data model containing elastic constants.
41
+ C11, C12, ... C66 : float
42
+ Individual components of Cij for a standardized representation:
43
+ isotropic: C11, C12 (see below for more options)
44
+ cubic: C11, C12, C44
45
+ hexagonal: C11, C12, C13, C33, C44, C66 (2*C66=C11-C12)
46
+ tetragonal: C11, C12, C13, C16, C33, C44, C66 (C16 optional)
47
+ rhombohedral: C11, C12, C13, C14, C15, C33, C44, C66 (2*C66=C11-C12, C15 optional)
48
+ orthorhombic: C11, C12, C13, C22, C23, C33, C44, C55, C66
49
+ monoclinic: C11, C12, C13, C15, C22, C23, C25, C33, C35, C44, C46, C55, C66
50
+ triclinic: all Cij where i <= j
51
+ M, lambda, mu, E, nu, K, C11, C12, C44 : float
52
+ Individual isotropic elastic moduli (exactly two must be given).
53
+ """
54
+ # Initialize for no arguments
55
+ if len(kwargs) == 0:
56
+ self.__c_ij = np.zeros((6,6), dtype='float64')
57
+
58
+ # Initialize for matrix arguments
59
+ elif 'Cij' in kwargs:
60
+ assert len(kwargs) == 1, 'Cij cannot be specified with other keyword arguments'
61
+ self.Cij = kwargs['Cij']
62
+ elif 'Sij' in kwargs:
63
+ assert len(kwargs) == 1, 'Sij cannot be specified with other keyword arguments'
64
+ self.Sij = kwargs['Sij']
65
+ elif 'Cij9' in kwargs:
66
+ assert len(kwargs) == 1, 'Cij9 cannot be specified with other keyword arguments'
67
+ self.Cij9 = kwargs['Cij9']
68
+ elif 'Cijkl' in kwargs:
69
+ assert len(kwargs) == 1, 'Cijkl cannot be specified with other keyword arguments'
70
+ self.Cijkl = kwargs['Cijkl']
71
+ elif 'Sijkl' in kwargs:
72
+ assert len(kwargs) == 1, 'Sijkl cannot be specified with other keyword arguments'
73
+ self.Sijkl = kwargs['Sijkl']
74
+
75
+ # Initialize using data model
76
+ elif 'model' in kwargs:
77
+ self.model(**kwargs)
78
+
79
+ # Initialize for individually specified parameters
80
+ elif len(kwargs) == 2:
81
+ self.isotropic(**kwargs)
82
+ elif len(kwargs) == 3:
83
+ self.cubic(**kwargs)
84
+ elif len(kwargs) == 5:
85
+ self.hexagonal(**kwargs)
86
+ elif len(kwargs) == 6 or len(kwargs) == 7:
87
+ if 'C14' in kwargs:
88
+ self.rhombohedral(**kwargs)
89
+ else:
90
+ self.tetragonal(**kwargs)
91
+ elif len(kwargs) == 8:
92
+ self.rhombohedral(**kwargs)
93
+ elif len(kwargs) == 9:
94
+ self.orthorhombic(**kwargs)
95
+ elif len(kwargs) == 13:
96
+ self.monoclinic(**kwargs)
97
+ elif len(kwargs) == 21:
98
+ self.triclinic(**kwargs)
99
+ else:
100
+ raise TypeError('Invalid argument keywords')
101
+
102
+ def __str__(self) -> str:
103
+ """Calling string returns str(self.Cij)."""
104
+ return str(self.Cij)
105
+
106
+ @property
107
+ def Cij(self) -> np.ndarray:
108
+ """The stiffness constants in Voigt 6x6 format"""
109
+ return deepcopy(self.__c_ij)
110
+
111
+ @Cij.setter
112
+ def Cij(self, value: npt.ArrayLike):
113
+ value = np.asarray(value, dtype='float64')
114
+ assert value.shape == (6,6), 'Cij must be 6x6'
115
+
116
+ # Zero out near-zero terms
117
+ assert value.max() > 0.0, 'Cij values not valid'
118
+ value[np.isclose(value/value.max(), 0.0, atol=1e-9)] = 0.0
119
+
120
+ # Check symmetry
121
+ for i in range(6):
122
+ for j in range(i):
123
+ assert np.isclose(value[i,j], value[j,i], atol=1e-9), '6x6 matrix not symmetric!'
124
+ self.__c_ij = value
125
+
126
+ @property
127
+ def Sij(self) -> np.ndarray:
128
+ """The compliance constants in Voigt 6x6 format"""
129
+ return np.linalg.inv(self.Cij)
130
+
131
+ @Sij.setter
132
+ def Sij(self, value: npt.ArrayLike):
133
+ value = np.asarray(value, dtype='float64')
134
+ assert value.shape == (6,6), 'Sij must be 6x6'
135
+ self.Cij = np.linalg.inv(value)
136
+
137
+ @property
138
+ def Cij9(self) -> np.ndarray:
139
+ """The stiffness constants in 9x9 format"""
140
+ c = self.Cij
141
+ return np.array([[c[0,0],c[0,1],c[0,2],c[0,3],c[0,4],c[0,5],c[0,3],c[0,4],c[0,5]],
142
+ [c[1,0],c[1,1],c[1,2],c[1,3],c[1,4],c[1,5],c[1,3],c[1,4],c[1,5]],
143
+ [c[2,0],c[2,1],c[2,2],c[2,3],c[2,4],c[2,5],c[2,3],c[2,4],c[2,5]],
144
+ [c[3,0],c[3,1],c[3,2],c[3,3],c[3,4],c[3,5],c[3,3],c[3,4],c[3,5]],
145
+ [c[4,0],c[4,1],c[4,2],c[4,3],c[4,4],c[4,5],c[4,3],c[4,4],c[4,5]],
146
+ [c[5,0],c[5,1],c[5,2],c[5,3],c[5,4],c[5,5],c[5,3],c[5,4],c[5,5]],
147
+ [c[3,0],c[3,1],c[3,2],c[3,3],c[3,4],c[3,5],c[3,3],c[3,4],c[3,5]],
148
+ [c[4,0],c[4,1],c[4,2],c[4,3],c[4,4],c[4,5],c[4,3],c[4,4],c[4,5]],
149
+ [c[5,0],c[5,1],c[5,2],c[5,3],c[5,4],c[5,5],c[5,3],c[5,4],c[5,5]]])
150
+
151
+ @Cij9.setter
152
+ def Cij9(self, value: npt.ArrayLike):
153
+ value = np.asarray(value, dtype='float64')
154
+ assert value.shape == (9,9), 'Cij9 must be 9x9'
155
+
156
+ # Check symmetry
157
+ for i in range(6, 9):
158
+ for j in range(9):
159
+ assert value[i,j] == value[i-3, j]
160
+ assert value[j,i] == value[j, i-3]
161
+ self.Cij = value[:6, :6]
162
+
163
+ @property
164
+ def Cijkl(self) -> np.ndarray:
165
+ """The stiffness constants in 3x3x3x3 format"""
166
+ c = self.Cij
167
+ return np.array([[[[c[0,0],c[0,5],c[0,4]], [c[0,5],c[0,1],c[0,3]], [c[0,4],c[0,3],c[0,2]]],
168
+ [[c[5,0],c[5,5],c[5,4]], [c[5,5],c[5,1],c[5,3]], [c[5,4],c[5,3],c[5,2]]],
169
+ [[c[4,0],c[4,5],c[4,4]], [c[4,5],c[4,1],c[4,3]], [c[4,4],c[4,3],c[4,2]]]],
170
+
171
+ [[[c[5,0],c[5,5],c[5,4]], [c[5,5],c[5,1],c[5,3]], [c[5,4],c[5,3],c[5,2]]],
172
+ [[c[1,0],c[1,5],c[1,4]], [c[1,5],c[1,1],c[1,3]], [c[1,4],c[1,3],c[1,2]]],
173
+ [[c[3,0],c[3,5],c[3,4]], [c[3,5],c[3,1],c[3,3]], [c[3,4],c[3,3],c[3,2]]]],
174
+
175
+ [[[c[4,0],c[4,5],c[4,4]], [c[4,5],c[4,1],c[4,3]], [c[4,4],c[4,3],c[4,2]]],
176
+ [[c[3,0],c[3,5],c[3,4]], [c[3,5],c[3,1],c[3,3]], [c[3,4],c[3,3],c[3,2]]],
177
+ [[c[2,0],c[2,5],c[2,4]], [c[2,5],c[2,1],c[2,3]], [c[2,4],c[2,3],c[2,2]]]]])
178
+
179
+ @Cijkl.setter
180
+ def Cijkl(self, value: npt.ArrayLike):
181
+ c = np.asarray(value, dtype='float64')
182
+ assert c.shape == (3,3,3,3), 'Cijkl must be 3x3x3x3'
183
+ assert c.max() > 0.0, 'Cij values not valid'
184
+ # Check symmetry
185
+ indexes = np.array([[0,0], [1,1], [2,2], [1,2], [0,2], [0,1]], dtype=int)
186
+ for ij in range(6):
187
+ for kl in range(ij, 6):
188
+ i, j, k, l = indexes[ij,0], indexes[ij,1], indexes[kl,0], indexes[kl,1]
189
+ assert np.isclose(c[i,j,k,l], c[j,i,k,l])
190
+ assert np.isclose(c[i,j,k,l], c[j,i,l,k])
191
+ assert np.isclose(c[i,j,k,l], c[k,l,j,i])
192
+ assert np.isclose(c[i,j,k,l], c[l,k,j,i])
193
+ assert np.isclose(c[i,j,k,l], c[i,j,l,k])
194
+ assert np.isclose(c[i,j,k,l], c[k,l,i,j])
195
+ assert np.isclose(c[i,j,k,l], c[l,k,i,j])
196
+
197
+ self.Cij = np.array([[c[0,0,0,0], c[0,0,1,1], c[0,0,2,2], c[0,0,1,2], c[0,0,0,2], c[0,0,0,1]],
198
+ [c[1,1,0,0], c[1,1,1,1], c[1,1,2,2], c[1,1,1,2], c[1,1,0,2], c[1,1,0,1]],
199
+ [c[2,2,0,0], c[2,2,1,1], c[2,2,2,2], c[2,2,1,2], c[2,2,0,2], c[2,2,0,1]],
200
+ [c[1,2,0,0], c[1,2,1,1], c[1,2,2,2], c[1,2,1,2], c[1,2,0,2], c[1,2,0,1]],
201
+ [c[0,2,0,0], c[0,2,1,1], c[0,2,2,2], c[0,2,1,2], c[0,2,0,2], c[0,2,0,1]],
202
+ [c[0,1,0,0], c[0,1,1,1], c[0,1,2,2], c[0,1,1,2], c[0,1,0,2], c[0,1,0,1]]])
203
+
204
+ @property
205
+ def Sijkl(self) -> np.ndarray:
206
+ """The compliance constants in 3x3x3x3 format"""
207
+ s = self.Sij
208
+ s[3:,:] = s[3:,:]/2.
209
+ s[:,3:] = s[:,3:]/2.
210
+ return np.array([[[[s[0,0],s[0,5],s[0,4]], [s[0,5],s[0,1],s[0,3]], [s[0,4],s[0,3],s[0,2]]],
211
+ [[s[5,0],s[5,5],s[5,4]], [s[5,5],s[5,1],s[5,3]], [s[5,4],s[5,3],s[5,2]]],
212
+ [[s[4,0],s[4,5],s[4,4]], [s[4,5],s[4,1],s[4,3]], [s[4,4],s[4,3],s[4,2]]]],
213
+
214
+ [[[s[5,0],s[5,5],s[5,4]], [s[5,5],s[5,1],s[5,3]], [s[5,4],s[5,3],s[5,2]]],
215
+ [[s[1,0],s[1,5],s[1,4]], [s[1,5],s[1,1],s[1,3]], [s[1,4],s[1,3],s[1,2]]],
216
+ [[s[3,0],s[3,5],s[3,4]], [s[3,5],s[3,1],s[3,3]], [s[3,4],s[3,3],s[3,2]]]],
217
+
218
+ [[[s[4,0],s[4,5],s[4,4]], [s[4,5],s[4,1],s[4,3]], [s[4,4],s[4,3],s[4,2]]],
219
+ [[s[3,0],s[3,5],s[3,4]], [s[3,5],s[3,1],s[3,3]], [s[3,4],s[3,3],s[3,2]]],
220
+ [[s[2,0],s[2,5],s[2,4]], [s[2,5],s[2,1],s[2,3]], [s[2,4],s[2,3],s[2,2]]]]])
221
+
222
+ @Sijkl.setter
223
+ def Sijkl(self, value: npt.ArrayLike):
224
+ s = np.asarray(value, dtype='float64')
225
+ assert s.shape == (3,3,3,3), 'Sijkl must be 3x3x3x3'
226
+
227
+ # Check symmetry
228
+ indexes = np.array([[0,0], [1,1], [2,2], [1,2], [0,2], [0,1]], dtype=int)
229
+ for ij in range(6):
230
+ for kl in range(ij, 6):
231
+ i, j, k, l = indexes[ij,0], indexes[ij,1], indexes[kl,0], indexes[kl,1]
232
+ assert np.isclose(s[i,j,k,l], s[j,i,k,l])
233
+ assert np.isclose(s[i,j,k,l], s[j,i,l,k])
234
+ assert np.isclose(s[i,j,k,l], s[k,l,j,i])
235
+ assert np.isclose(s[i,j,k,l], s[l,k,j,i])
236
+ assert np.isclose(s[i,j,k,l], s[i,j,l,k])
237
+ assert np.isclose(s[i,j,k,l], s[k,l,i,j])
238
+ assert np.isclose(s[i,j,k,l], s[l,k,i,j])
239
+
240
+ self.Sij = np.array([[ s[0,0,0,0], s[0,0,1,1], s[0,0,2,2], 2.*s[0,0,1,2], 2.*s[0,0,0,2], 2.*s[0,0,0,1]],
241
+ [ s[1,1,0,0], s[1,1,1,1], s[1,1,2,2], 2.*s[1,1,1,2], 2.*s[1,1,0,2], 2.*s[1,1,0,1]],
242
+ [ s[2,2,0,0], s[2,2,1,1], s[2,2,2,2], 2.*s[2,2,1,2], 2.*s[2,2,0,2], 2.*s[2,2,0,1]],
243
+ [2.*s[1,2,0,0], 2.*s[1,2,1,1], 2.*s[1,2,2,2], 4.*s[1,2,1,2], 4.*s[1,2,0,2], 4.*s[1,2,0,1]],
244
+ [2.*s[0,2,0,0], 2.*s[0,2,1,1], 2.*s[0,2,2,2], 4.*s[0,2,1,2], 4.*s[0,2,0,2], 4.*s[0,2,0,1]],
245
+ [2.*s[0,1,0,0], 2.*s[0,1,1,1], 2.*s[0,1,2,2], 4.*s[0,1,1,2], 4.*s[0,1,0,2], 4.*s[0,1,0,1]]])
246
+
247
+ def transform(self,
248
+ axes: npt.ArrayLike,
249
+ tol: float = 1e-8) -> ElasticConstants:
250
+ """
251
+ Transforms the elastic constant matrix based on the supplied axes.
252
+
253
+ Parameters
254
+ ----------
255
+ axes : numpy.ndarray
256
+ (3, 3) array giving three right-handed orthogonal vectors to use
257
+ for transforming.
258
+ tol : float, optional
259
+ Relative tolerance to use in identifying near-zero terms.
260
+
261
+ Returns
262
+ -------
263
+ ElasticConstants
264
+ A new ElasticConstants object that has been transformed.
265
+ """
266
+ axes = np.asarray(axes, dtype='float64')
267
+ T = axes_check(axes)
268
+
269
+ Q = np.einsum('km,ln->mnkl', T, T)
270
+ C = np.einsum('ghij,ghmn,mnkl->ijkl', Q, self.Cijkl, Q)
271
+ C[abs(C / C.max()) < tol] = 0.0
272
+
273
+ return ElasticConstants(Cijkl=C)
274
+
275
+ def isotropic(self, **kwargs):
276
+ """
277
+ Set values with two independent isotropic moduli.
278
+
279
+ Parameters
280
+ ----------
281
+ C11 : float, optional
282
+ C11 component of Cij.
283
+ C12 : float, optional
284
+ C12 component of Cij.
285
+ C44 : float, optional
286
+ C44 component of Cij.
287
+ M : float, optional
288
+ P-wave modulus(Equivalent to C11).
289
+ lambda : float, optional
290
+ Lame's first parameter (Equivalent to C12).
291
+ mu : float, optional
292
+ Shear modulus (Equivalent to C44).
293
+ E : float, optional
294
+ Young's modulus
295
+ nu : float, optional
296
+ Poisson's ratio
297
+ K : float, optional
298
+ Bulk modulus
299
+ """
300
+
301
+ try:
302
+ # Handle equivalent terms
303
+ if 'M' in kwargs:
304
+ kwargs['C11'] = kwargs.pop('M')
305
+ if 'lambda' in kwargs:
306
+ kwargs['C12'] = kwargs.pop('lambda')
307
+ if 'mu' in kwargs:
308
+ kwargs['C44'] = kwargs.pop('mu')
309
+
310
+ # Check len of kwargs
311
+ assert len(kwargs) == 2
312
+
313
+ # Pop and convert terms
314
+ if 'C11' in kwargs:
315
+ c11 = kwargs.pop('C11')
316
+
317
+ if 'C12' in kwargs:
318
+ c12 = kwargs.pop('C12')
319
+ c44 = (c11 - c12) / 2
320
+
321
+ else:
322
+ if 'C44' in kwargs:
323
+ c44 = kwargs.pop('C44')
324
+
325
+ elif 'E' in kwargs:
326
+ E = kwargs.pop('E')
327
+ S = (E**2 + 9 * c11**2 - 10 * E * c11)**0.5
328
+ c44 = (3 * c11 + E - S) / 8
329
+
330
+ elif 'nu' in kwargs:
331
+ nu = kwargs.pop('nu')
332
+ c44 = c11 * (1 - 2 * nu) / (2 * (1 - nu))
333
+
334
+ elif 'K' in kwargs:
335
+ K = kwargs.pop('K')
336
+ c44 = 3 * (c11 - K) / 4
337
+
338
+ c12 = c11 - 2 * c44
339
+
340
+ else:
341
+ if 'C12' in kwargs:
342
+ c12 = kwargs.pop('C12')
343
+
344
+ if 'C44' in kwargs:
345
+ c44 = kwargs.pop('C44')
346
+
347
+ elif 'E' in kwargs:
348
+ E = kwargs.pop('E')
349
+ R = (E**2 + 9 * c12**2 + 2 * E * c12)**0.5
350
+ c44 = (E - 3 * c12 + R) / 4
351
+
352
+ elif 'nu' in kwargs:
353
+ nu = kwargs.pop('nu')
354
+ c44 = c12 * (1 - 2 * nu) / (2 * nu)
355
+
356
+ elif 'K' in kwargs:
357
+ K = kwargs.pop('K')
358
+ c44 = 3 * (K - c12) / 2
359
+
360
+ elif 'C44' in kwargs:
361
+ c44 = kwargs.pop('C44')
362
+
363
+ if 'E' in kwargs:
364
+ E = kwargs.pop('E')
365
+ c12 = c44 * (E - 2 * c44) / (3 * c44 - E)
366
+
367
+ elif 'nu' in kwargs:
368
+ nu = kwargs.pop('nu')
369
+ c12 = 2 * c44 * nu / (1 - 2 * nu)
370
+
371
+ elif 'K' in kwargs:
372
+ K = kwargs.pop('K')
373
+ c12 = K - 2 * c44 / 3
374
+
375
+ elif 'E' in kwargs:
376
+ E = kwargs.pop('E')
377
+
378
+ if 'nu' in kwargs:
379
+ nu = kwargs.pop('nu')
380
+ c12 = E * nu / ((1 + nu) * (1 - 2 * nu))
381
+ c44 = E / (2 * (1 + nu))
382
+
383
+ elif 'K' in kwargs:
384
+ K = kwargs.pop('K')
385
+ c12 = 3 * K * (3 * K - E) / (9 * K - E)
386
+ c44 = 3 * K * E / (9 * K - E)
387
+
388
+ elif 'nu' in kwargs:
389
+ nu = kwargs.pop('nu')
390
+
391
+ if 'K' in kwargs:
392
+ K = kwargs.pop('K')
393
+ c12 = 3 * K * nu / (1 + nu)
394
+ c44 = 3 * K * (1 - 2 * nu) / (2 * (1 + nu))
395
+
396
+ c11 = c12 + 2 * c44
397
+
398
+ except:
399
+ raise TypeError('isotropic style takes two unique keyword arguments of (C11=M, C12=lambda, C44=mu, E, nu, K)')
400
+
401
+ # Build Cij array
402
+ self.Cij = np.array([[c11, c12, c12, 0.0, 0.0, 0.0],
403
+ [c12, c11, c12, 0.0, 0.0, 0.0],
404
+ [c12, c12, c11, 0.0, 0.0, 0.0],
405
+ [0.0, 0.0, 0.0, c44, 0.0, 0.0],
406
+ [0.0, 0.0, 0.0, 0.0, c44, 0.0],
407
+ [0.0, 0.0, 0.0, 0.0, 0.0, c44]])
408
+
409
+ def cubic(self, **kwargs):
410
+ """
411
+ Set values with three independent cubic moduli.
412
+
413
+ Parameters
414
+ ----------
415
+ C11 : float
416
+ C11 component of Cij.
417
+ C12 : float
418
+ C12 component of Cij.
419
+ C44 : float
420
+ C44 component of Cij.
421
+ """
422
+
423
+ try:
424
+ # Check len of kwargs
425
+ assert len(kwargs) == 3
426
+
427
+ # Pop required independent terms
428
+ c11 = kwargs.pop('C11')
429
+ c12 = kwargs.pop('C12')
430
+ c44 = kwargs.pop('C44')
431
+ except:
432
+ raise TypeError('cubic style takes keyword arguments C11, C12, and C66')
433
+
434
+ # Build Cij array
435
+ self.Cij = np.array([[c11, c12, c12, 0.0, 0.0, 0.0],
436
+ [c12, c11, c12, 0.0, 0.0, 0.0],
437
+ [c12, c12, c11, 0.0, 0.0, 0.0],
438
+ [0.0, 0.0, 0.0, c44, 0.0, 0.0],
439
+ [0.0, 0.0, 0.0, 0.0, c44, 0.0],
440
+ [0.0, 0.0, 0.0, 0.0, 0.0, c44]])
441
+
442
+ def hexagonal(self, **kwargs):
443
+ """
444
+ Set values with five independent hexagonal moduli.
445
+ (2 * C66 = C11 - C12)
446
+
447
+ Parameters
448
+ ----------
449
+ C11 : float, optional
450
+ C11 component of Cij.
451
+ C12 : float, optional
452
+ C12 component of Cij.
453
+ C13 : float
454
+ C13 component of Cij.
455
+ C33 : float
456
+ C33 component of Cij.
457
+ C44 : float
458
+ C44 component of Cij.
459
+ C66 : float, optional
460
+ C66 component of Cij.
461
+ """
462
+
463
+ try:
464
+ # Check len of kwargs
465
+ assert len(kwargs) >= 5 and len(kwargs) <=6
466
+
467
+ # Pop required independent terms
468
+ c33 = kwargs.pop('C33')
469
+ c13 = kwargs.pop('C13')
470
+ c44 = kwargs.pop('C44')
471
+
472
+ # Pop required dependent terms
473
+ if 'C11' in kwargs and 'C12' in kwargs:
474
+ c11 = kwargs.pop('C11')
475
+ c12 = kwargs.pop('C12')
476
+ c66 = (c11 - c12) / 2
477
+
478
+ # Check if redundant C66 is given
479
+ if 'C66' in kwargs:
480
+ assert np.isclose(c66, kwargs['C66'])
481
+ c66 = kwargs.pop('C66')
482
+
483
+ elif 'C11' in kwargs and 'C66' in kwargs:
484
+ c11 = kwargs.pop('C11')
485
+ c66 = kwargs.pop('C66')
486
+ c12 = c11 - 2 * c66
487
+
488
+ elif 'C12' in kwargs and 'C66' in kwargs:
489
+ c12 = kwargs.pop('C12')
490
+ c66 = kwargs.pop('C66')
491
+ c11 = 2 * c66 + c12
492
+ else:
493
+ assert False
494
+ except:
495
+ raise TypeError('hexagonal style takes keyword arguments C33, C13, C44, and two of (2*C66=C11-C12)')
496
+
497
+ # Build Cij array
498
+ self.Cij = np.array([[c11, c12, c13, 0.0, 0.0, 0.0],
499
+ [c12, c11, c13, 0.0, 0.0, 0.0],
500
+ [c13, c13, c33, 0.0, 0.0, 0.0],
501
+ [0.0, 0.0, 0.0, c44, 0.0, 0.0],
502
+ [0.0, 0.0, 0.0, 0.0, c44, 0.0],
503
+ [0.0, 0.0, 0.0, 0.0, 0.0, c66]])
504
+
505
+ def rhombohedral(self, **kwargs):
506
+ """
507
+ Set values with six or seven independent rhombohedral moduli.
508
+ (2 * C66 = C11 - C12)
509
+
510
+ Parameters
511
+ ----------
512
+ C11 : float, optional
513
+ C11 component of Cij.
514
+ C12 : float, optional
515
+ C12 component of Cij.
516
+ C13 : float
517
+ C13 component of Cij.
518
+ C14 : float
519
+ C14 component of Cij.
520
+ C15 : float, optional
521
+ C15 component of Cij.
522
+ C33 : float
523
+ C33 component of Cij.
524
+ C44 : float
525
+ C44 component of Cij.
526
+ C66 : float, optional
527
+ C66 component of Cij.
528
+ """
529
+
530
+ try:
531
+ # Check len of kwargs
532
+ assert len(kwargs) >= 6 and len(kwargs) <= 8
533
+
534
+ # Pop required independent terms
535
+ c33 = kwargs.pop('C33')
536
+ c13 = kwargs.pop('C13')
537
+ c14 = kwargs.pop('C14')
538
+ c44 = kwargs.pop('C44')
539
+
540
+ # Pop required dependent terms
541
+ if 'C11' in kwargs and 'C12' in kwargs:
542
+ c11 = kwargs.pop('C11')
543
+ c12 = kwargs.pop('C12')
544
+ c66 = (c11 - c12) / 2
545
+
546
+ # Check if redundant C66 is given
547
+ if 'C66' in kwargs:
548
+ assert np.isclose(c66, kwargs['C66'])
549
+ c66 = kwargs.pop('C66')
550
+
551
+ elif 'C11' in kwargs and 'C66' in kwargs:
552
+ c11 = kwargs.pop('C11')
553
+ c66 = kwargs.pop('C66')
554
+ c12 = c11 - 2 * c66
555
+
556
+ elif 'C12' in kwargs and 'C66' in kwargs:
557
+ c12 = kwargs.pop('C12')
558
+ c66 = kwargs.pop('C66')
559
+ c11 = 2 * c66 + c12
560
+ else:
561
+ assert False
562
+
563
+ # Check for optional term
564
+ if len(kwargs) == 0:
565
+ c15 = 0.0
566
+ else:
567
+ c15 = kwargs.pop('C15')
568
+ assert len(kwargs) == 0
569
+ except:
570
+ raise TypeError('rhombohedral style takes keyword arguments C33, C13, C14, C44, at least two of (2*C66=C11-C12) and optional C15')
571
+
572
+ # Build Cij array
573
+ self.Cij = np.array([[c11, c12, c13, c14, c15, 0.0],
574
+ [c12, c11, c13,-c14,-c15, 0.0],
575
+ [c13, c13, c33, 0.0, 0.0, 0.0],
576
+ [c14,-c14, 0.0, c44, 0.0,-c15],
577
+ [c15,-c15, 0.0, 0.0, c44, c14],
578
+ [0.0, 0.0, 0.0,-c15, c14, c66]])
579
+
580
+ def tetragonal(self, **kwargs):
581
+ """
582
+ Set values with six or seven independent tetragonal moduli.
583
+
584
+ Parameters
585
+ ----------
586
+ C11 : float
587
+ C11 component of Cij.
588
+ C12 : float
589
+ C12 component of Cij.
590
+ C13 : float
591
+ C13 component of Cij.
592
+ C16 : float, optional
593
+ C16 component of Cij.
594
+ C33 : float
595
+ C33 component of Cij.
596
+ C44 : float
597
+ C44 component of Cij.
598
+ C66 : float
599
+ C66 component of Cij.
600
+ """
601
+
602
+ try:
603
+ # Check len of kwargs
604
+ assert len(kwargs) == 6 or len(kwargs) == 7
605
+
606
+ # Pop required independent terms
607
+ c11 = kwargs.pop('C11')
608
+ c33 = kwargs.pop('C33')
609
+ c12 = kwargs.pop('C12')
610
+ c13 = kwargs.pop('C13')
611
+ c44 = kwargs.pop('C44')
612
+ c66 = kwargs.pop('C66')
613
+
614
+ # Check for optional term
615
+ if len(kwargs) == 0:
616
+ c16 = 0.0
617
+ else:
618
+ c16 = kwargs.pop('C16')
619
+ assert len(kwargs) == 0
620
+ except:
621
+ raise TypeError('tetragonal style takes keyword arguments C11, C33, C12, C13, C44, C66, and optional C16')
622
+
623
+ # Build Cij array
624
+ self.Cij = np.array([[c11, c12, c13, 0.0, 0.0, c16],
625
+ [c12, c11, c13, 0.0, 0.0,-c16],
626
+ [c13, c13, c33, 0.0, 0.0, 0.0],
627
+ [0.0, 0.0, 0.0, c44, 0.0, 0.0],
628
+ [0.0, 0.0, 0.0, 0.0, c44, 0.0],
629
+ [c16,-c16, 0.0, 0.0, 0.0, c66]])
630
+
631
+ def orthorhombic(self, **kwargs):
632
+ """
633
+ Set values with nine independent orthorhombic moduli.
634
+
635
+ Parameters
636
+ ----------
637
+ C11 : float
638
+ C11 component of Cij.
639
+ C12 : float
640
+ C12 component of Cij.
641
+ C13 : float
642
+ C13 component of Cij.
643
+ C22 : float
644
+ C22 component of Cij.
645
+ C23 : float
646
+ C23 component of Cij.
647
+ C33 : float
648
+ C33 component of Cij.
649
+ C44 : float
650
+ C44 component of Cij.
651
+ C55 : float
652
+ C55 component of Cij.
653
+ C66 : float
654
+ C66 component of Cij.
655
+ """
656
+
657
+ try:
658
+ # Check len of kwargs
659
+ assert len(kwargs) == 9
660
+
661
+ # Set required independent terms
662
+ c11 = kwargs['C11']
663
+ c22 = kwargs['C22']
664
+ c33 = kwargs['C33']
665
+ c12 = kwargs['C12']
666
+ c13 = kwargs['C13']
667
+ c23 = kwargs['C23']
668
+ c44 = kwargs['C44']
669
+ c55 = kwargs['C55']
670
+ c66 = kwargs['C66']
671
+ except:
672
+ raise TypeError('orthorhombic style takes keyword arguments C11, C22, C33, C12, C13, C23, C44, C55, C66')
673
+
674
+ # Build Cij array
675
+ self.Cij = np.array([[c11, c12, c13, 0.0, 0.0, 0.0],
676
+ [c12, c22, c23, 0.0, 0.0, 0.0],
677
+ [c13, c23, c33, 0.0, 0.0, 0.0],
678
+ [0.0, 0.0, 0.0, c44, 0.0, 0.0],
679
+ [0.0, 0.0, 0.0, 0.0, c55, 0.0],
680
+ [0.0, 0.0, 0.0, 0.0, 0.0, c66]])
681
+
682
+ def monoclinic(self, **kwargs):
683
+ """
684
+ Set values with thirteen independent monoclinic moduli.
685
+
686
+ Parameters
687
+ ----------
688
+ C11 : float
689
+ C11 component of Cij.
690
+ C12 : float
691
+ C12 component of Cij.
692
+ C13 : float
693
+ C13 component of Cij.
694
+ C15 : float
695
+ C15 component of Cij.
696
+ C22 : float
697
+ C22 component of Cij.
698
+ C23 : float
699
+ C23 component of Cij.
700
+ C25 : float
701
+ C25 component of Cij.
702
+ C33 : float
703
+ C33 component of Cij.
704
+ C35 : float
705
+ C35 component of Cij.
706
+ C44 : float
707
+ C44 component of Cij.
708
+ C46 : float
709
+ C46 component of Cij.
710
+ C55 : float
711
+ C55 component of Cij.
712
+ C66 : float
713
+ C66 component of Cij.
714
+ """
715
+
716
+ try:
717
+ # Check len of kwargs
718
+ assert len(kwargs) == 13
719
+
720
+ # Set required independent terms
721
+ c11 = kwargs['C11']
722
+ c12 = kwargs['C12']
723
+ c13 = kwargs['C13']
724
+ c15 = kwargs['C15']
725
+ c22 = kwargs['C22']
726
+ c23 = kwargs['C23']
727
+ c25 = kwargs['C25']
728
+ c33 = kwargs['C33']
729
+ c35 = kwargs['C35']
730
+ c44 = kwargs['C44']
731
+ c46 = kwargs['C46']
732
+ c55 = kwargs['C55']
733
+ c66 = kwargs['C66']
734
+ except:
735
+ raise TypeError('monoclinic style takes keyword arguments C11, C12, C13, C15, C22, C23, C25, C33, C35, C44, C46, C55, C66')
736
+
737
+ # Build Cij array
738
+ self.Cij = np.array([[c11, c12, c13, 0.0, c15, 0.0],
739
+ [c12, c22, c23, 0.0, c25, 0.0],
740
+ [c13, c23, c33, 0.0, c35, 0.0],
741
+ [0.0, 0.0, 0.0, c44, 0.0, c46],
742
+ [c15, c25, c35, 0.0, c55, 0.0],
743
+ [0.0, 0.0, 0.0, c46, 0.0, c66]])
744
+
745
+ def triclinic(self, **kwargs):
746
+ """
747
+ Set values with twenty one independent triclinic moduli
748
+
749
+ Parameters
750
+ ----------
751
+ C11 : float
752
+ C11 component of Cij.
753
+ C12 : float
754
+ C12 component of Cij.
755
+ C13 : float
756
+ C13 component of Cij.
757
+ C14 : float
758
+ C14 component of Cij.
759
+ C15 : float
760
+ C15 component of Cij.
761
+ C16 : float
762
+ C16 component of Cij.
763
+ C22 : float
764
+ C22 component of Cij.
765
+ C23 : float
766
+ C23 component of Cij.
767
+ C24 : float
768
+ C24 component of Cij.
769
+ C25 : float
770
+ C25 component of Cij.
771
+ C26 : float
772
+ C26 component of Cij.
773
+ C33 : float
774
+ C33 component of Cij.
775
+ C34 : float
776
+ C34 component of Cij.
777
+ C35 : float
778
+ C35 component of Cij.
779
+ C36 : float
780
+ C36 component of Cij.
781
+ C44 : float
782
+ C44 component of Cij.
783
+ C45 : float
784
+ C45 component of Cij.
785
+ C46 : float
786
+ C46 component of Cij.
787
+ C55 : float
788
+ C55 component of Cij.
789
+ C56 : float
790
+ C56 component of Cij.
791
+ C66 : float
792
+ C66 component of Cij.
793
+ """
794
+
795
+ try:
796
+ # Check len of kwargs
797
+ assert len(kwargs) == 21
798
+
799
+ # Set required independent terms
800
+ c11 = kwargs['C11']
801
+ c12 = kwargs['C12']
802
+ c13 = kwargs['C13']
803
+ c14 = kwargs['C14']
804
+ c15 = kwargs['C15']
805
+ c16 = kwargs['C16']
806
+ c22 = kwargs['C22']
807
+ c23 = kwargs['C23']
808
+ c24 = kwargs['C24']
809
+ c25 = kwargs['C25']
810
+ c26 = kwargs['C26']
811
+ c33 = kwargs['C33']
812
+ c34 = kwargs['C34']
813
+ c35 = kwargs['C35']
814
+ c36 = kwargs['C36']
815
+ c44 = kwargs['C44']
816
+ c45 = kwargs['C45']
817
+ c46 = kwargs['C46']
818
+ c55 = kwargs['C55']
819
+ c56 = kwargs['C56']
820
+ c66 = kwargs['C66']
821
+ except:
822
+ raise TypeError('triclinic style takes keyword arguments of all Cij where i <= j')
823
+
824
+ # Build Cij array
825
+ self.Cij = np.array([[c11, c12, c13, c14, c15, c16],
826
+ [c12, c22, c23, c24, c25, c26],
827
+ [c13, c23, c33, c34, c35, c36],
828
+ [c14, c24, c34, c44, c45, c46],
829
+ [c15, c25, c35, c45, c55, c56],
830
+ [c16, c26, c36, c46, c56, c66]])
831
+
832
+ def normalized_as(self, crystal_system: str) -> ElasticConstants:
833
+ """
834
+ Returns a new ElasticConstants object where values of the current are
835
+ averaged or zeroed out according to a standard crystal system setting.
836
+ NOTE: no validation checks are made to evaluate whether such
837
+ normalizations should be done! That is left up to you (compare values
838
+ before and after normalization).
839
+
840
+ Parameters
841
+ ----------
842
+ crystal_system : str
843
+ Indicates the crystal system representation to use when building a
844
+ data model.
845
+
846
+ Returns
847
+ -------
848
+ atomman.ElasticConstants
849
+ The elastic constants normalized according to the crystal system
850
+ symmetries.
851
+ """
852
+
853
+ if crystal_system == 'triclinic':
854
+ return ElasticConstants(Cij = self.Cij)
855
+
856
+ else:
857
+ c = self.Cij
858
+ c_dict = {}
859
+
860
+ if crystal_system == 'isotropic':
861
+ c_dict['mu'] = self.shear()
862
+ c_dict['K'] = self.bulk()
863
+
864
+ elif crystal_system == 'cubic':
865
+ c_dict['C11'] = (c[0,0] + c[1,1] + c[2,2]) / 3
866
+ c_dict['C12'] = (c[0,1] + c[0,2] + c[1,2]) / 3
867
+ c_dict['C44'] = (c[3,3] + c[4,4] + c[5,5]) / 3
868
+
869
+ elif crystal_system == 'hexagonal':
870
+ c_dict['C11'] = (c[0,0] + c[1,1]) / 2
871
+ c_dict['C33'] = c[2,2]
872
+ c_dict['C12'] = (c[0,1] + (c[0,0] - 2*c[5,5])) / 2
873
+ c_dict['C13'] = (c[0,2] + c[1,2]) / 2
874
+ c_dict['C44'] = (c[3,3] + c[4,4]) / 2
875
+
876
+ elif crystal_system == 'tetragonal':
877
+ c_dict['C11'] = (c[0,0] + c[1,1]) / 2
878
+ c_dict['C33'] = c[2,2]
879
+ c_dict['C12'] = c[0,1]
880
+ c_dict['C13'] = (c[0,2] + c[1,2]) / 2
881
+ c_dict['C16'] = (c[0,5] - c[1,5]) / 2
882
+ c_dict['C44'] = (c[3,3] + c[4,4]) / 2
883
+ c_dict['C66'] = c[5,5]
884
+
885
+ elif crystal_system == 'rhombohedral':
886
+ c_dict['C11'] = (c[0,0] + c[1,1]) / 2
887
+ c_dict['C33'] = c[2,2]
888
+ c_dict['C12'] = (c[0,1] + (c[0,0] - 2*c[5,5])) / 2
889
+ c_dict['C13'] = (c[0,2] + c[1,2]) / 2
890
+ c_dict['C14'] = (c[0,3] - c[1,3]) / 2
891
+ c_dict['C15'] = (c[0,4] - c[1,4] - c[3,5]) / 3
892
+ c_dict['C44'] = (c[3,3] + c[4,4]) / 2
893
+
894
+ elif crystal_system == 'orthorhombic':
895
+ c_dict['C11'] = c[0,0]
896
+ c_dict['C22'] = c[1,1]
897
+ c_dict['C33'] = c[2,2]
898
+ c_dict['C12'] = c[0,1]
899
+ c_dict['C13'] = c[0,2]
900
+ c_dict['C23'] = c[1,2]
901
+ c_dict['C44'] = c[3,3]
902
+ c_dict['C55'] = c[4,4]
903
+ c_dict['C66'] = c[5,5]
904
+ else:
905
+ raise ValueError('Invalid crystal_system: ' + crystal_system)
906
+
907
+ return ElasticConstants(**c_dict)
908
+
909
+ def is_normal(self,
910
+ crystal_system: str,
911
+ atol: float = 1e-4,
912
+ rtol: float = 1e-4) -> bool:
913
+ """
914
+ Checks if current elastic constants agree with values normalized to
915
+ a specified crystal family (within tolerances).
916
+
917
+ Parameters
918
+ ----------
919
+ crystal_system : str
920
+ Indicates the crystal system representation to use when building a
921
+ data model.
922
+ atol : float, optional
923
+ Absolute tolerance to use. Default value is 1e-4.
924
+ rtol : float, optional
925
+ Relative tolerance to use. Default value is 1e-4.
926
+
927
+ Returns
928
+ -------
929
+ bool
930
+ True if all Cij match within the tolerances, false otherwise.
931
+ """
932
+ return np.allclose(self.Cij, self.normalized_as(crystal_system).Cij,
933
+ atol=atol, rtol=rtol)
934
+
935
+ def model(self,
936
+ model: Union[str, io.IOBase, DM, None] = None,
937
+ unit: Optional[str] = None,
938
+ crystal_system: str = 'triclinic') -> Optional[DM]:
939
+ """
940
+ Return or set DataModelDict representation of the elastic constants.
941
+
942
+ Parameters
943
+ ----------
944
+ model : DataModelDict, string, or file-like object, optional
945
+ Data model containing exactly one 'elastic-constants' branch to
946
+ read.
947
+ unit : str, optional
948
+ Units or pressure to save values in when building a data model.
949
+ Default value is None (no conversion).
950
+ crystal_system : str, optional
951
+ Indicates the crystal system representation to normalize by.
952
+ Default value is 'triclinic', i.e. no normalization.
953
+
954
+ Returns
955
+ -------
956
+ DataModelDict
957
+ If model is not given as a parameter.
958
+ """
959
+
960
+ # Set values if model given
961
+ if model is not None:
962
+
963
+ # Find elastic-constants element
964
+ model = DM(model).find('elastic-constants')
965
+
966
+ # Read in values
967
+ try:
968
+ # New format
969
+ self.Cij = uc.value_unit(model['Cij'])
970
+ except:
971
+ # Old format
972
+ c_dict = {}
973
+ for C in model['C']:
974
+ key = 'C' + C['ij'][0] + C['ij'][2]
975
+ c_dict[key] = uc.value_unit(C['stiffness'])
976
+ self.Cij = ElasticConstants(**c_dict).Cij
977
+
978
+ # Return DataModelDict if model not given
979
+ else:
980
+ normCij = self.normalized_as(crystal_system).Cij
981
+ model = DM()
982
+ model['elastic-constants'] = DM()
983
+ model['elastic-constants']['Cij'] = uc.model(normCij, unit)
984
+
985
+ return model
986
+
987
+ def bulk(self, style: str = 'Hill') -> float:
988
+ """
989
+ Returns a bulk modulus estimate.
990
+
991
+ Parameters
992
+ ----------
993
+ style : str
994
+ Indicates which style of estimate to use. Default value is 'Hill'.
995
+ - 'Hill' -- Hill estimate (average of Voigt and Reuss).
996
+ - 'Voigt' -- Voigt estimate. Uses Cij.
997
+ - 'Reuss' -- Reuss estimate. Uses Sij.
998
+ """
999
+ if style == 'Hill':
1000
+ return (self.bulk('Voigt') + self.bulk('Reuss')) / 2
1001
+
1002
+ elif style == 'Voigt':
1003
+ c = self.Cij
1004
+ return ( (c[0,0] + c[1,1] + c[2,2]) + 2*(c[0,1] + c[1,2] + c[0,2]) ) / 9
1005
+
1006
+ elif style == 'Reuss':
1007
+ s = self.Sij
1008
+ return 1 / ( (s[0,0] + s[1,1] + s[2,2]) + 2*(s[0,1] + s[1,2] + s[0,2]) )
1009
+
1010
+ else:
1011
+ raise ValueError('Unknown estimate style')
1012
+
1013
+ def shear(self, style: str = 'Hill') -> float:
1014
+ """
1015
+ Returns a shear modulus estimate.
1016
+
1017
+ Parameters
1018
+ ----------
1019
+ style : str
1020
+ Indicates which style of estimate to use. Default value is 'Hill'.
1021
+ - 'Hill' -- Hill estimate (average of Voigt and Reuss).
1022
+ - 'Voigt' -- Voigt estimate. Uses Cij.
1023
+ - 'Reuss' -- Reuss estimate. Uses Sij.
1024
+ """
1025
+ if style == 'Hill':
1026
+ return (self.shear('Voigt') + self.shear('Reuss')) / 2
1027
+
1028
+ elif style == 'Voigt':
1029
+ c = self.Cij
1030
+ return ( (c[0,0] + c[1,1] + c[2,2]) - (c[0,1] + c[1,2] + c[0,2]) + 3*(c[3,3] + c[4,4] + c[5,5]) ) / 15
1031
+
1032
+ elif style == 'Reuss':
1033
+ s = self.Sij
1034
+ return 15 / ( 4*(s[0,0] + s[1,1] + s[2,2]) - 4*(s[0,1] + s[1,2] + s[0,2]) + 3*(s[3,3] + s[4,4] + s[5,5]) )
1035
+
1036
+ else:
1037
+ raise ValueError('Unknown estimate style')
atomman/source/atomman/core/ElasticConstants2.py ADDED
@@ -0,0 +1,1017 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ from __future__ import annotations
4
+ from copy import deepcopy
5
+ import io
6
+ from typing import Optional, Union
7
+
8
+ # http://www.numpy.org/
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+
12
+ # https://github.com/usnistgov/DataModelDict
13
+ from DataModelDict import DataModelDict as DM
14
+
15
+ # atomman imports
16
+ from ..tools import axes_check
17
+ import atomman.unitconvert as uc
18
+
19
+ class ElasticConstants2(object):
20
+ """Class for storing and converting elastic constant values"""
21
+
22
+ def __init__(self, **kwargs):
23
+ """
24
+ Initializes an ElasticConstants instance from one of the parameter
25
+ options.
26
+
27
+ Parameters
28
+ ----------
29
+ Cij : numpy.ndarray
30
+ (6, 6) array of Voigt representation of elastic stiffness.
31
+ Sij : numpy.ndarray
32
+ (6, 6) array of Voigt representation of elastic compliance.
33
+ Cij9 : numpy.ndarray
34
+ (9, 9) array representation of elastic stiffness.
35
+ Cijkl : numpy.ndarray
36
+ (3, 3, 3, 3) array representation of elastic stiffness.
37
+ Sijkl : numpy.ndarray
38
+ (3, 3, 3, 3) array representation of elastic compliance.
39
+ model : DataModelDict, string, or file-like object
40
+ Data model containing elastic constants.
41
+ C11, C12, ... C66 : float
42
+ Individual components of Cij for a standardized representation:
43
+ isotropic: C11, C12, C44 (2*C44=C11-C12)
44
+ cubic: C11, C12, C44
45
+ hexagonal: C11, C12, C13, C33, C44, C66 (2*C66=C11-C12)
46
+ tetragonal: C11, C12, C13, C16, C33, C44, C66 (C16 optional)
47
+ rhombohedral: C11, C12, C13, C14, C15, C33, C44, C66 (2*C66=C11-C12, C15 optional)
48
+ orthorhombic: C11, C12, C13, C22, C23, C33, C44, C55, C66
49
+ monoclinic: C11, C12, C13, C15, C22, C23, C25, C33, C35, C44, C46, C55, C66
50
+ triclinic: all Cij where i <= j
51
+ M, Lame, mu, E, nu, K : float
52
+ Individual isotropic elastic moduli that can be used instead.
53
+ verify : bool, optional
54
+ Used with hexagonal and rhombohedral Cij sets. If True (default) then values
55
+ of the non-independent moduli C11, C12, C66 will be checked for compatibility
56
+ if all three are given.
57
+ """
58
+ # Pop verify if needed
59
+ verify = kwargs.pop('verify', True)
60
+
61
+ # Initialize for no arguments
62
+ if len(kwargs) == 0:
63
+ self.__c_ij = np.zeros((6,6), dtype='float64')
64
+
65
+ # Initialize for matrix arguments
66
+ elif 'Cij' in kwargs:
67
+ assert len(kwargs) == 1, 'Cij cannot be specified with other keyword arguments'
68
+ self.Cij = kwargs['Cij']
69
+ elif 'Sij' in kwargs:
70
+ assert len(kwargs) == 1, 'Sij cannot be specified with other keyword arguments'
71
+ self.Sij = kwargs['Sij']
72
+ elif 'Cij9' in kwargs:
73
+ assert len(kwargs) == 1, 'Cij9 cannot be specified with other keyword arguments'
74
+ self.Cij9 = kwargs['Cij9']
75
+ elif 'Cijkl' in kwargs:
76
+ assert len(kwargs) == 1, 'Cijkl cannot be specified with other keyword arguments'
77
+ self.Cijkl = kwargs['Cijkl']
78
+ elif 'Sijkl' in kwargs:
79
+ assert len(kwargs) == 1, 'Sijkl cannot be specified with other keyword arguments'
80
+ self.Sijkl = kwargs['Sijkl']
81
+
82
+ # Initialize using data model
83
+ elif 'model' in kwargs:
84
+ self.model(**kwargs)
85
+
86
+ # Initialize for individually specified parameters by standard representation
87
+ elif ('C24' in kwargs or 'C26' in kwargs or 'C34' in kwargs or
88
+ 'C36' in kwargs or 'C45' in kwargs or 'C56' in kwargs):
89
+ self.triclinic(**kwargs)
90
+ elif 'C25' in kwargs or 'C35' in kwargs or 'C46' in kwargs:
91
+ self.monoclinic(**kwargs)
92
+ elif 'C22' in kwargs or 'C23' in kwargs or 'C55' in kwargs:
93
+ self.orthorhombic(**kwargs)
94
+ elif 'C14' in kwargs:
95
+ self.rhombohedral(verify=verify, **kwargs)
96
+ elif 'C13' in kwargs or 'C33' in kwargs or 'C66' in kwargs:
97
+ if 'C16' in kwargs or ('C11' in kwargs and 'C12' in kwargs and 'C66' in kwargs):
98
+ self.tetragonal(**kwargs)
99
+ else:
100
+ self.hexagonal(verify=verify, **kwargs)
101
+ elif len(kwargs) == 3:
102
+ self.cubic(**kwargs)
103
+ elif len(kwargs) == 2:
104
+ self.isotropic(**kwargs)
105
+ else:
106
+ raise TypeError('Invalid argument keywords')
107
+
108
+ def __str__(self) -> str:
109
+ """Calling string returns str(self.Cij)."""
110
+ return str(self.Cij)
111
+
112
+ @property
113
+ def Cij(self) -> np.ndarray:
114
+ """The stiffness constants in Voigt 6x6 format"""
115
+ return deepcopy(self.__c_ij)
116
+
117
+ @Cij.setter
118
+ def Cij(self, value: npt.ArrayLike):
119
+ value = np.asarray(value, dtype='float64')
120
+ assert value.shape == (6,6), 'Cij must be 6x6'
121
+
122
+ # Zero out near-zero terms
123
+ assert value.max() > 0.0, 'Cij values not valid'
124
+ value[np.isclose(value/value.max(), 0.0, atol=1e-9)] = 0.0
125
+
126
+ # Check symmetry
127
+ for i in range(6):
128
+ for j in range(i):
129
+ assert np.isclose(value[i,j], value[j,i], atol=1e-9), '6x6 matrix not symmetric!'
130
+ self.__c_ij = value
131
+
132
+ @property
133
+ def Sij(self) -> np.ndarray:
134
+ """The compliance constants in Voigt 6x6 format"""
135
+ return np.linalg.inv(self.Cij)
136
+
137
+ @Sij.setter
138
+ def Sij(self, value: npt.ArrayLike):
139
+ value = np.asarray(value, dtype='float64')
140
+ assert value.shape == (6,6), 'Sij must be 6x6'
141
+ self.Cij = np.linalg.inv(value)
142
+
143
+ @property
144
+ def Cij9(self) -> np.ndarray:
145
+ """The stiffness constants in 9x9 format"""
146
+ c = self.Cij
147
+ return np.array([[c[0,0],c[0,1],c[0,2],c[0,3],c[0,4],c[0,5],c[0,3],c[0,4],c[0,5]],
148
+ [c[1,0],c[1,1],c[1,2],c[1,3],c[1,4],c[1,5],c[1,3],c[1,4],c[1,5]],
149
+ [c[2,0],c[2,1],c[2,2],c[2,3],c[2,4],c[2,5],c[2,3],c[2,4],c[2,5]],
150
+ [c[3,0],c[3,1],c[3,2],c[3,3],c[3,4],c[3,5],c[3,3],c[3,4],c[3,5]],
151
+ [c[4,0],c[4,1],c[4,2],c[4,3],c[4,4],c[4,5],c[4,3],c[4,4],c[4,5]],
152
+ [c[5,0],c[5,1],c[5,2],c[5,3],c[5,4],c[5,5],c[5,3],c[5,4],c[5,5]],
153
+ [c[3,0],c[3,1],c[3,2],c[3,3],c[3,4],c[3,5],c[3,3],c[3,4],c[3,5]],
154
+ [c[4,0],c[4,1],c[4,2],c[4,3],c[4,4],c[4,5],c[4,3],c[4,4],c[4,5]],
155
+ [c[5,0],c[5,1],c[5,2],c[5,3],c[5,4],c[5,5],c[5,3],c[5,4],c[5,5]]])
156
+
157
+ @Cij9.setter
158
+ def Cij9(self, value: npt.ArrayLike):
159
+ value = np.asarray(value, dtype='float64')
160
+ assert value.shape == (9,9), 'Cij9 must be 9x9'
161
+
162
+ # Check symmetry
163
+ for i in range(6, 9):
164
+ for j in range(9):
165
+ assert value[i,j] == value[i-3, j]
166
+ assert value[j,i] == value[j, i-3]
167
+ self.Cij = value[:6, :6]
168
+
169
+ @property
170
+ def Cijkl(self) -> np.ndarray:
171
+ """The stiffness constants in 3x3x3x3 format"""
172
+ c = self.Cij
173
+ return np.array([[[[c[0,0],c[0,5],c[0,4]], [c[0,5],c[0,1],c[0,3]], [c[0,4],c[0,3],c[0,2]]],
174
+ [[c[5,0],c[5,5],c[5,4]], [c[5,5],c[5,1],c[5,3]], [c[5,4],c[5,3],c[5,2]]],
175
+ [[c[4,0],c[4,5],c[4,4]], [c[4,5],c[4,1],c[4,3]], [c[4,4],c[4,3],c[4,2]]]],
176
+
177
+ [[[c[5,0],c[5,5],c[5,4]], [c[5,5],c[5,1],c[5,3]], [c[5,4],c[5,3],c[5,2]]],
178
+ [[c[1,0],c[1,5],c[1,4]], [c[1,5],c[1,1],c[1,3]], [c[1,4],c[1,3],c[1,2]]],
179
+ [[c[3,0],c[3,5],c[3,4]], [c[3,5],c[3,1],c[3,3]], [c[3,4],c[3,3],c[3,2]]]],
180
+
181
+ [[[c[4,0],c[4,5],c[4,4]], [c[4,5],c[4,1],c[4,3]], [c[4,4],c[4,3],c[4,2]]],
182
+ [[c[3,0],c[3,5],c[3,4]], [c[3,5],c[3,1],c[3,3]], [c[3,4],c[3,3],c[3,2]]],
183
+ [[c[2,0],c[2,5],c[2,4]], [c[2,5],c[2,1],c[2,3]], [c[2,4],c[2,3],c[2,2]]]]])
184
+
185
+ @Cijkl.setter
186
+ def Cijkl(self, value: npt.ArrayLike):
187
+ c = np.asarray(value, dtype='float64')
188
+ assert c.shape == (3,3,3,3), 'Cijkl must be 3x3x3x3'
189
+ assert c.max() > 0.0, 'Cij values not valid'
190
+ # Check symmetry
191
+ indexes = np.array([[0,0], [1,1], [2,2], [1,2], [0,2], [0,1]], dtype=int)
192
+ for ij in range(6):
193
+ for kl in range(ij, 6):
194
+ i, j, k, l = indexes[ij,0], indexes[ij,1], indexes[kl,0], indexes[kl,1]
195
+ assert np.isclose(c[i,j,k,l], c[j,i,k,l])
196
+ assert np.isclose(c[i,j,k,l], c[j,i,l,k])
197
+ assert np.isclose(c[i,j,k,l], c[k,l,j,i])
198
+ assert np.isclose(c[i,j,k,l], c[l,k,j,i])
199
+ assert np.isclose(c[i,j,k,l], c[i,j,l,k])
200
+ assert np.isclose(c[i,j,k,l], c[k,l,i,j])
201
+ assert np.isclose(c[i,j,k,l], c[l,k,i,j])
202
+
203
+ self.Cij = np.array([[c[0,0,0,0], c[0,0,1,1], c[0,0,2,2], c[0,0,1,2], c[0,0,0,2], c[0,0,0,1]],
204
+ [c[1,1,0,0], c[1,1,1,1], c[1,1,2,2], c[1,1,1,2], c[1,1,0,2], c[1,1,0,1]],
205
+ [c[2,2,0,0], c[2,2,1,1], c[2,2,2,2], c[2,2,1,2], c[2,2,0,2], c[2,2,0,1]],
206
+ [c[1,2,0,0], c[1,2,1,1], c[1,2,2,2], c[1,2,1,2], c[1,2,0,2], c[1,2,0,1]],
207
+ [c[0,2,0,0], c[0,2,1,1], c[0,2,2,2], c[0,2,1,2], c[0,2,0,2], c[0,2,0,1]],
208
+ [c[0,1,0,0], c[0,1,1,1], c[0,1,2,2], c[0,1,1,2], c[0,1,0,2], c[0,1,0,1]]])
209
+
210
+ @property
211
+ def Sijkl(self) -> np.ndarray:
212
+ """The compliance constants in 3x3x3x3 format"""
213
+ s = self.Sij
214
+ s[3:,:] = s[3:,:]/2.
215
+ s[:,3:] = s[:,3:]/2.
216
+ return np.array([[[[s[0,0],s[0,5],s[0,4]], [s[0,5],s[0,1],s[0,3]], [s[0,4],s[0,3],s[0,2]]],
217
+ [[s[5,0],s[5,5],s[5,4]], [s[5,5],s[5,1],s[5,3]], [s[5,4],s[5,3],s[5,2]]],
218
+ [[s[4,0],s[4,5],s[4,4]], [s[4,5],s[4,1],s[4,3]], [s[4,4],s[4,3],s[4,2]]]],
219
+
220
+ [[[s[5,0],s[5,5],s[5,4]], [s[5,5],s[5,1],s[5,3]], [s[5,4],s[5,3],s[5,2]]],
221
+ [[s[1,0],s[1,5],s[1,4]], [s[1,5],s[1,1],s[1,3]], [s[1,4],s[1,3],s[1,2]]],
222
+ [[s[3,0],s[3,5],s[3,4]], [s[3,5],s[3,1],s[3,3]], [s[3,4],s[3,3],s[3,2]]]],
223
+
224
+ [[[s[4,0],s[4,5],s[4,4]], [s[4,5],s[4,1],s[4,3]], [s[4,4],s[4,3],s[4,2]]],
225
+ [[s[3,0],s[3,5],s[3,4]], [s[3,5],s[3,1],s[3,3]], [s[3,4],s[3,3],s[3,2]]],
226
+ [[s[2,0],s[2,5],s[2,4]], [s[2,5],s[2,1],s[2,3]], [s[2,4],s[2,3],s[2,2]]]]])
227
+
228
+ @Sijkl.setter
229
+ def Sijkl(self, value: npt.ArrayLike):
230
+ s = np.asarray(value, dtype='float64')
231
+ assert s.shape == (3,3,3,3), 'Sijkl must be 3x3x3x3'
232
+
233
+ # Check symmetry
234
+ indexes = np.array([[0,0], [1,1], [2,2], [1,2], [0,2], [0,1]], dtype=int)
235
+ for ij in range(6):
236
+ for kl in range(ij, 6):
237
+ i, j, k, l = indexes[ij,0], indexes[ij,1], indexes[kl,0], indexes[kl,1]
238
+ assert np.isclose(s[i,j,k,l], s[j,i,k,l])
239
+ assert np.isclose(s[i,j,k,l], s[j,i,l,k])
240
+ assert np.isclose(s[i,j,k,l], s[k,l,j,i])
241
+ assert np.isclose(s[i,j,k,l], s[l,k,j,i])
242
+ assert np.isclose(s[i,j,k,l], s[i,j,l,k])
243
+ assert np.isclose(s[i,j,k,l], s[k,l,i,j])
244
+ assert np.isclose(s[i,j,k,l], s[l,k,i,j])
245
+
246
+ self.Sij = np.array([[ s[0,0,0,0], s[0,0,1,1], s[0,0,2,2], 2.*s[0,0,1,2], 2.*s[0,0,0,2], 2.*s[0,0,0,1]],
247
+ [ s[1,1,0,0], s[1,1,1,1], s[1,1,2,2], 2.*s[1,1,1,2], 2.*s[1,1,0,2], 2.*s[1,1,0,1]],
248
+ [ s[2,2,0,0], s[2,2,1,1], s[2,2,2,2], 2.*s[2,2,1,2], 2.*s[2,2,0,2], 2.*s[2,2,0,1]],
249
+ [2.*s[1,2,0,0], 2.*s[1,2,1,1], 2.*s[1,2,2,2], 4.*s[1,2,1,2], 4.*s[1,2,0,2], 4.*s[1,2,0,1]],
250
+ [2.*s[0,2,0,0], 2.*s[0,2,1,1], 2.*s[0,2,2,2], 4.*s[0,2,1,2], 4.*s[0,2,0,2], 4.*s[0,2,0,1]],
251
+ [2.*s[0,1,0,0], 2.*s[0,1,1,1], 2.*s[0,1,2,2], 4.*s[0,1,1,2], 4.*s[0,1,0,2], 4.*s[0,1,0,1]]])
252
+
253
+ def transform(self,
254
+ axes: npt.ArrayLike,
255
+ tol: float = 1e-8) -> ElasticConstants2:
256
+ """
257
+ Transforms the elastic constant matrix based on the supplied axes.
258
+
259
+ Parameters
260
+ ----------
261
+ axes : numpy.ndarray
262
+ (3, 3) array giving three right-handed orthogonal vectors to use
263
+ for transforming.
264
+ tol : float, optional
265
+ Relative tolerance to use in identifying near-zero terms.
266
+
267
+ Returns
268
+ -------
269
+ ElasticConstants
270
+ A new ElasticConstants object that has been transformed.
271
+ """
272
+ axes = np.asarray(axes, dtype='float64')
273
+ T = axes_check(axes)
274
+
275
+ Q = np.einsum('km,ln->mnkl', T, T)
276
+ C = np.einsum('ghij,ghmn,mnkl->ijkl', Q, self.Cijkl, Q)
277
+ C[abs(C / C.max()) < tol] = 0.0
278
+
279
+ return ElasticConstants2(Cijkl=C)
280
+
281
+ def isotropic(self, *,
282
+ C11: Optional[float] = None,
283
+ C12: Optional[float] = None,
284
+ C44: Optional[float] = None,
285
+ M: Optional[float] = None,
286
+ Lame: Optional[float] = None,
287
+ mu: Optional[float] = None,
288
+ E: Optional[float] = None,
289
+ nu: Optional[float] = None,
290
+ K: Optional[float] = None):
291
+ """
292
+ Set values with two independent isotropic moduli.
293
+
294
+ Parameters
295
+ ----------
296
+ C11 : float, optional
297
+ C11 component of Cij.
298
+ C12 : float, optional
299
+ C12 component of Cij.
300
+ C44 : float, optional
301
+ C44 component of Cij.
302
+ M : float, optional
303
+ P-wave modulus(Equivalent to C11).
304
+ Lame : float, optional
305
+ Lame's first parameter (Equivalent to C12).
306
+ mu : float, optional
307
+ Shear modulus (Equivalent to C44).
308
+ E : float, optional
309
+ Young's modulus
310
+ nu : float, optional
311
+ Poisson's ratio
312
+ K : float, optional
313
+ Bulk modulus
314
+ """
315
+ # Count moduli with values
316
+ kwargcount = sum([C11 is not None, C12 is not None, C44 is not None,
317
+ M is not None, Lame is not None, mu is not None,
318
+ E is not None, nu is not None, K is not None])
319
+ if kwargcount != 2:
320
+ raise TypeError('isotropic requires exactly two independent moduli')
321
+
322
+ # Handle equivalent terms
323
+ if M is not None:
324
+ if C11 is not None:
325
+ raise ValueError('C11 and M are not independent moduli')
326
+ C11 = M
327
+ if Lame is not None:
328
+ if C12 is not None:
329
+ raise ValueError('C12 and Lame are not independent moduli')
330
+ C12 = Lame
331
+ if mu is not None:
332
+ if C44 is not None:
333
+ raise ValueError('C44 and mu are not independent moduli')
334
+ C44 = mu
335
+
336
+ # C11 + (something) combinations
337
+ if C11 is not None:
338
+
339
+ # Get C44 from C11 and C12
340
+ if C12 is not None:
341
+ C44 = (C11 - C12) / 2
342
+
343
+ # Find C44 then get C12
344
+ else:
345
+ if E is not None:
346
+ S = (E**2 + 9 * C11**2 - 10 * E * C11)**0.5
347
+ C44 = (3 * C11 + E - S) / 8
348
+ elif nu is not None:
349
+ C44 = C11 * (1 - 2 * nu) / (2 * (1 - nu))
350
+ elif K is not None:
351
+ C44 = 3 * (C11 - K) / 4
352
+ # else: C44 is not None
353
+
354
+ C12 = C11 - 2 * C44
355
+
356
+ # Combinations without C11: find C12 and C44 then get C11
357
+ else:
358
+ # C12 + (something) combinations
359
+ if C12 is not None:
360
+ if E is not None:
361
+ R = (E**2 + 9 * C12**2 + 2 * E * C12)**0.5
362
+ C44 = (E - 3 * C12 + R) / 4
363
+ elif nu is not None:
364
+ C44 = C12 * (1 - 2 * nu) / (2 * nu)
365
+ elif K is not None:
366
+ C44 = 3 * (K - C12) / 2
367
+ # else: C44 is not None
368
+
369
+ # C44 + (something) combinations
370
+ elif C44 is not None:
371
+ if E is not None:
372
+ C12 = C44 * (E - 2 * C44) / (3 * C44 - E)
373
+ elif nu is not None:
374
+ C12 = 2 * C44 * nu / (1 - 2 * nu)
375
+ else: # K is not None
376
+ C12 = K - 2 * C44 / 3
377
+
378
+ # Others + (something) combinations
379
+ elif E is not None:
380
+ if nu is not None:
381
+ C12 = E * nu / ((1 + nu) * (1 - 2 * nu))
382
+ C44 = E / (2 * (1 + nu))
383
+ else: # K is not None
384
+ C12 = 3 * K * (3 * K - E) / (9 * K - E)
385
+ C44 = 3 * K * E / (9 * K - E)
386
+ elif nu is not None:
387
+ #if K is not None:
388
+ C12 = 3 * K * nu / (1 + nu)
389
+ C44 = 3 * K * (1 - 2 * nu) / (2 * (1 + nu))
390
+
391
+ C11 = C12 + 2 * C44
392
+
393
+ # Build Cij array
394
+ self.Cij = np.array([[C11, C12, C12, 0.0, 0.0, 0.0],
395
+ [C12, C11, C12, 0.0, 0.0, 0.0],
396
+ [C12, C12, C11, 0.0, 0.0, 0.0],
397
+ [0.0, 0.0, 0.0, C44, 0.0, 0.0],
398
+ [0.0, 0.0, 0.0, 0.0, C44, 0.0],
399
+ [0.0, 0.0, 0.0, 0.0, 0.0, C44]])
400
+
401
+ def cubic(self, *,
402
+ C11: Optional[float] = None,
403
+ C12: Optional[float] = None,
404
+ C44: Optional[float] = None):
405
+ """
406
+ Set values with three independent cubic moduli.
407
+
408
+ Parameters
409
+ ----------
410
+ C11 : float
411
+ C11 component of Cij.
412
+ C12 : float
413
+ C12 component of Cij.
414
+ C44 : float
415
+ C44 component of Cij.
416
+ """
417
+ if C11 is None or C12 is None or C44 is None:
418
+ raise TypeError('cubic style requires C11, C12, and C66')
419
+
420
+ # Build Cij array
421
+ self.Cij = np.array([[C11, C12, C12, 0.0, 0.0, 0.0],
422
+ [C12, C11, C12, 0.0, 0.0, 0.0],
423
+ [C12, C12, C11, 0.0, 0.0, 0.0],
424
+ [0.0, 0.0, 0.0, C44, 0.0, 0.0],
425
+ [0.0, 0.0, 0.0, 0.0, C44, 0.0],
426
+ [0.0, 0.0, 0.0, 0.0, 0.0, C44]])
427
+
428
+ def hexagonal(self, *,
429
+ C11: Optional[float] = None,
430
+ C12: Optional[float] = None,
431
+ C13: Optional[float] = None,
432
+ C33: Optional[float] = None,
433
+ C44: Optional[float] = None,
434
+ C66: Optional[float] = None,
435
+ verify: bool = True):
436
+ """
437
+ Set values with five independent hexagonal moduli.
438
+ (2 * C66 = C11 - C12)
439
+
440
+ Parameters
441
+ ----------
442
+ C11 : float, optional
443
+ C11 component of Cij.
444
+ C12 : float, optional
445
+ C12 component of Cij.
446
+ C13 : float
447
+ C13 component of Cij.
448
+ C33 : float
449
+ C33 component of Cij.
450
+ C44 : float
451
+ C44 component of Cij.
452
+ C66 : float, optional
453
+ C66 component of Cij.
454
+ verify : bool, optional
455
+ If True (default), the values of the non-independent moduli C11,
456
+ C12, and C66 will be checked for compatibility if all are given.
457
+ """
458
+ if C13 is None or C33 is None or C44 is None:
459
+ raise TypeError('hexagonal style requires C13, C33, and C44')
460
+
461
+ # Calculate missing C11, C12 or C66
462
+ if C11 is None:
463
+ if C12 is None or C66 is None:
464
+ raise TypeError('hexagonal style requires at least two of C11, C12, and C66')
465
+ C11 = 2 * C66 + C12
466
+ elif C12 is None:
467
+ if C66 is None:
468
+ raise TypeError('hexagonal style requires at least two of C11, C12, and C66')
469
+ C12 = C11 - 2 * C66
470
+ elif C66 is None:
471
+ C66 = (C11 - C12) / 2
472
+
473
+ # Verify 2 * C66 = C11 - C12
474
+ elif verify:
475
+ if not np.isclose(2 * C66, C11 - C12):
476
+ raise ValueError('dependent values not compatible: C11-C12 != 2*C66')
477
+
478
+ # Build Cij array
479
+ self.Cij = np.array([[C11, C12, C13, 0.0, 0.0, 0.0],
480
+ [C12, C11, C13, 0.0, 0.0, 0.0],
481
+ [C13, C13, C33, 0.0, 0.0, 0.0],
482
+ [0.0, 0.0, 0.0, C44, 0.0, 0.0],
483
+ [0.0, 0.0, 0.0, 0.0, C44, 0.0],
484
+ [0.0, 0.0, 0.0, 0.0, 0.0, C66]])
485
+
486
+ def rhombohedral(self, *,
487
+ C11: Optional[float] = None,
488
+ C12: Optional[float] = None,
489
+ C13: Optional[float] = None,
490
+ C14: Optional[float] = None,
491
+ C15: float = 0.0,
492
+ C33: Optional[float] = None,
493
+ C44: Optional[float] = None,
494
+ C66: Optional[float] = None,
495
+ verify: bool = True):
496
+ """
497
+ Set values with six or seven independent rhombohedral moduli.
498
+ (2 * C66 = C11 - C12)
499
+
500
+ Parameters
501
+ ----------
502
+ C11 : float, optional
503
+ C11 component of Cij.
504
+ C12 : float, optional
505
+ C12 component of Cij.
506
+ C13 : float
507
+ C13 component of Cij.
508
+ C14 : float
509
+ C14 component of Cij.
510
+ C15 : float, optional
511
+ C15 component of Cij.
512
+ C33 : float
513
+ C33 component of Cij.
514
+ C44 : float
515
+ C44 component of Cij.
516
+ C66 : float, optional
517
+ C66 component of Cij.
518
+ verify : bool, optional
519
+ If True, the values of the non-independent moduli C11, C12, C66
520
+ will be checked for compatibility.
521
+ """
522
+ if C13 is None or C14 is None or C33 is None or C44 is None:
523
+ raise TypeError('rhombohedral style requires C13, C14, C33, and C44')
524
+
525
+ # Calculate missing C11, C12 or C66
526
+ if C11 is None:
527
+ if C12 is None or C66 is None:
528
+ raise TypeError('rhombohedral style requires at least two of C11, C12, and C66')
529
+ C11 = 2 * C66 + C12
530
+ elif C12 is None:
531
+ if C66 is None:
532
+ raise TypeError('rhombohedral style requires at least two of C11, C12, and C66')
533
+ C12 = C11 - 2 * C66
534
+ elif C66 is None:
535
+ C66 = (C11 - C12) / 2
536
+
537
+ # Verify 2 * C66 = C11 - C12
538
+ elif verify:
539
+ if not np.isclose(2 * C66, C11 - C12):
540
+ raise ValueError('dependent values not compatible: C11-C12 != 2*C66')
541
+
542
+ # Build Cij array
543
+ self.Cij = np.array([[C11, C12, C13, C14, C15, 0.0],
544
+ [C12, C11, C13,-C14,-C15, 0.0],
545
+ [C13, C13, C33, 0.0, 0.0, 0.0],
546
+ [C14,-C14, 0.0, C44, 0.0,-C15],
547
+ [C15,-C15, 0.0, 0.0, C44, C14],
548
+ [0.0, 0.0, 0.0,-C15, C14, C66]])
549
+
550
+ def tetragonal(self, *,
551
+ C11: Optional[float] = None,
552
+ C12: Optional[float] = None,
553
+ C13: Optional[float] = None,
554
+ C16: float = 0.0,
555
+ C33: Optional[float] = None,
556
+ C44: Optional[float] = None,
557
+ C66: Optional[float] = None):
558
+ """
559
+ Set values with six or seven independent tetragonal moduli.
560
+
561
+ Parameters
562
+ ----------
563
+ C11 : float
564
+ C11 component of Cij.
565
+ C12 : float
566
+ C12 component of Cij.
567
+ C13 : float
568
+ C13 component of Cij.
569
+ C16 : float, optional
570
+ C16 component of Cij.
571
+ C33 : float
572
+ C33 component of Cij.
573
+ C44 : float
574
+ C44 component of Cij.
575
+ C66 : float
576
+ C66 component of Cij.
577
+ """
578
+ if (C11 is None or C12 is None or C13 is None or
579
+ C33 is None or C44 is None or C66 is None):
580
+ raise TypeError('tetragonal style requires C11, C12, C13, C33, C44, and C66')
581
+
582
+ # Build Cij array
583
+ self.Cij = np.array([[C11, C12, C13, 0.0, 0.0, C16],
584
+ [C12, C11, C13, 0.0, 0.0,-C16],
585
+ [C13, C13, C33, 0.0, 0.0, 0.0],
586
+ [0.0, 0.0, 0.0, C44, 0.0, 0.0],
587
+ [0.0, 0.0, 0.0, 0.0, C44, 0.0],
588
+ [C16,-C16, 0.0, 0.0, 0.0, C66]])
589
+
590
+ def orthorhombic(self, *,
591
+ C11: Optional[float] = None,
592
+ C12: Optional[float] = None,
593
+ C13: Optional[float] = None,
594
+ C22: Optional[float] = None,
595
+ C23: Optional[float] = None,
596
+ C33: Optional[float] = None,
597
+ C44: Optional[float] = None,
598
+ C55: Optional[float] = None,
599
+ C66: Optional[float] = None):
600
+ """
601
+ Set values with nine independent orthorhombic moduli.
602
+
603
+ Parameters
604
+ ----------
605
+ C11 : float
606
+ C11 component of Cij.
607
+ C12 : float
608
+ C12 component of Cij.
609
+ C13 : float
610
+ C13 component of Cij.
611
+ C22 : float
612
+ C22 component of Cij.
613
+ C23 : float
614
+ C23 component of Cij.
615
+ C33 : float
616
+ C33 component of Cij.
617
+ C44 : float
618
+ C44 component of Cij.
619
+ C55 : float
620
+ C55 component of Cij.
621
+ C66 : float
622
+ C66 component of Cij.
623
+ """
624
+ if (C11 is None or C12 is None or C13 is None or
625
+ C22 is None or C23 is None or C33 is None or
626
+ C44 is None or C55 is None or C66 is None):
627
+ raise TypeError('orthorhombic style requires C11, C12, C13, C22, C23, C33, C44, C55, and C66')
628
+
629
+ # Build Cij array
630
+ self.Cij = np.array([[C11, C12, C13, 0.0, 0.0, 0.0],
631
+ [C12, C22, C23, 0.0, 0.0, 0.0],
632
+ [C13, C23, C33, 0.0, 0.0, 0.0],
633
+ [0.0, 0.0, 0.0, C44, 0.0, 0.0],
634
+ [0.0, 0.0, 0.0, 0.0, C55, 0.0],
635
+ [0.0, 0.0, 0.0, 0.0, 0.0, C66]])
636
+
637
+ def monoclinic(self, *,
638
+ C11: Optional[float] = None,
639
+ C12: Optional[float] = None,
640
+ C13: Optional[float] = None,
641
+ C15: Optional[float] = None,
642
+ C22: Optional[float] = None,
643
+ C23: Optional[float] = None,
644
+ C25: Optional[float] = None,
645
+ C33: Optional[float] = None,
646
+ C35: Optional[float] = None,
647
+ C44: Optional[float] = None,
648
+ C46: Optional[float] = None,
649
+ C55: Optional[float] = None,
650
+ C66: Optional[float] = None):
651
+ """
652
+ Set values with thirteen independent monoclinic moduli.
653
+
654
+ Parameters
655
+ ----------
656
+ C11 : float
657
+ C11 component of Cij.
658
+ C12 : float
659
+ C12 component of Cij.
660
+ C13 : float
661
+ C13 component of Cij.
662
+ C15 : float
663
+ C15 component of Cij.
664
+ C22 : float
665
+ C22 component of Cij.
666
+ C23 : float
667
+ C23 component of Cij.
668
+ C25 : float
669
+ C25 component of Cij.
670
+ C33 : float
671
+ C33 component of Cij.
672
+ C35 : float
673
+ C35 component of Cij.
674
+ C44 : float
675
+ C44 component of Cij.
676
+ C46 : float
677
+ C46 component of Cij.
678
+ C55 : float
679
+ C55 component of Cij.
680
+ C66 : float
681
+ C66 component of Cij.
682
+ """
683
+ if (C11 is None or C12 is None or C13 is None or C15 is None or
684
+ C22 is None or C23 is None or C25 is None or
685
+ C33 is None or C35 is None or C44 is None or
686
+ C46 is None or C55 is None or C66 is None):
687
+ raise TypeError('monoclinic style requires C11, C12, C13, C15, C22, C23, C25, C33, C35, C44, C46, C55, and C66')
688
+
689
+ # Build Cij array
690
+ self.Cij = np.array([[C11, C12, C13, 0.0, C15, 0.0],
691
+ [C12, C22, C23, 0.0, C25, 0.0],
692
+ [C13, C23, C33, 0.0, C35, 0.0],
693
+ [0.0, 0.0, 0.0, C44, 0.0, C46],
694
+ [C15, C25, C35, 0.0, C55, 0.0],
695
+ [0.0, 0.0, 0.0, C46, 0.0, C66]])
696
+
697
+ def triclinic(self, *,
698
+ C11: Optional[float] = None,
699
+ C12: Optional[float] = None,
700
+ C13: Optional[float] = None,
701
+ C14: Optional[float] = None,
702
+ C15: Optional[float] = None,
703
+ C16: Optional[float] = None,
704
+ C22: Optional[float] = None,
705
+ C23: Optional[float] = None,
706
+ C24: Optional[float] = None,
707
+ C25: Optional[float] = None,
708
+ C26: Optional[float] = None,
709
+ C33: Optional[float] = None,
710
+ C34: Optional[float] = None,
711
+ C35: Optional[float] = None,
712
+ C36: Optional[float] = None,
713
+ C44: Optional[float] = None,
714
+ C45: Optional[float] = None,
715
+ C46: Optional[float] = None,
716
+ C55: Optional[float] = None,
717
+ C56: Optional[float] = None,
718
+ C66: Optional[float] = None):
719
+ """
720
+ Set values with twenty one independent triclinic moduli
721
+
722
+ Parameters
723
+ ----------
724
+ C11 : float
725
+ C11 component of Cij.
726
+ C12 : float
727
+ C12 component of Cij.
728
+ C13 : float
729
+ C13 component of Cij.
730
+ C14 : float
731
+ C14 component of Cij.
732
+ C15 : float
733
+ C15 component of Cij.
734
+ C16 : float
735
+ C16 component of Cij.
736
+ C22 : float
737
+ C22 component of Cij.
738
+ C23 : float
739
+ C23 component of Cij.
740
+ C24 : float
741
+ C24 component of Cij.
742
+ C25 : float
743
+ C25 component of Cij.
744
+ C26 : float
745
+ C26 component of Cij.
746
+ C33 : float
747
+ C33 component of Cij.
748
+ C34 : float
749
+ C34 component of Cij.
750
+ C35 : float
751
+ C35 component of Cij.
752
+ C36 : float
753
+ C36 component of Cij.
754
+ C44 : float
755
+ C44 component of Cij.
756
+ C45 : float
757
+ C45 component of Cij.
758
+ C46 : float
759
+ C46 component of Cij.
760
+ C55 : float
761
+ C55 component of Cij.
762
+ C56 : float
763
+ C56 component of Cij.
764
+ C66 : float
765
+ C66 component of Cij.
766
+ """
767
+ if (C11 is None or C12 is None or C13 is None or
768
+ C14 is None or C15 is None or C16 is None or
769
+ C22 is None or C23 is None or C24 is None or
770
+ C25 is None or C26 is None or C33 is None or
771
+ C34 is None or C35 is None or C36 is None or
772
+ C44 is None or C45 is None or C46 is None or
773
+ C55 is None or C56 is None or C66 is None):
774
+ raise TypeError('triclinic style requires all 6x6 Cij where i <= j')
775
+
776
+ # Build Cij array
777
+ self.Cij = np.array([[C11, C12, C13, C14, C15, C16],
778
+ [C12, C22, C23, C24, C25, C26],
779
+ [C13, C23, C33, C34, C35, C36],
780
+ [C14, C24, C34, C44, C45, C46],
781
+ [C15, C25, C35, C45, C55, C56],
782
+ [C16, C26, C36, C46, C56, C66]])
783
+
784
+ def normalized_as(self,
785
+ crystal_system: str,
786
+ return_dict: bool = False
787
+ ) -> Union[dict, ElasticConstants2]:
788
+ """
789
+ Returns a new ElasticConstants object where values of the current are
790
+ averaged or zeroed out according to a standard crystal system setting.
791
+ NOTE: no validation checks are made to evaluate whether such
792
+ normalizations should be done! That is left up to you (compare values
793
+ before and after normalization).
794
+
795
+ Parameters
796
+ ----------
797
+ crystal_system : str
798
+ Indicates the crystal system representation to use when building a
799
+ data model.
800
+ return_dict: bool, optional
801
+ If False (default), a new ElasticConstants object will be returned.
802
+ If set to True, a dict containing only the unique lattice constants
803
+ for the crystal_system will be returned.
804
+
805
+ Returns
806
+ -------
807
+ atomman.ElasticConstants
808
+ The elastic constants normalized according to the crystal system
809
+ symmetries. Returned if return_dict is False.
810
+ dict
811
+ A dict containing only the unique elastic constants for the given
812
+ crystal_system. For 'isotropic', mu and K are used. For all
813
+ others, the unique Cij values will be used
814
+ """
815
+
816
+ c = self.Cij
817
+ c_dict = {}
818
+
819
+ if crystal_system == 'isotropic':
820
+ c_dict['mu'] = self.shear()
821
+ c_dict['K'] = self.bulk()
822
+
823
+ elif crystal_system == 'cubic':
824
+ c_dict['C11'] = (c[0,0] + c[1,1] + c[2,2]) / 3
825
+ c_dict['C12'] = (c[0,1] + c[0,2] + c[1,2]) / 3
826
+ c_dict['C44'] = (c[3,3] + c[4,4] + c[5,5]) / 3
827
+
828
+ elif crystal_system == 'hexagonal':
829
+ c_dict['C11'] = (c[0,0] + c[1,1]) / 2
830
+ c_dict['C33'] = c[2,2]
831
+ c_dict['C12'] = (c[0,1] + (c[0,0] - 2*c[5,5])) / 2
832
+ c_dict['C13'] = (c[0,2] + c[1,2]) / 2
833
+ c_dict['C44'] = (c[3,3] + c[4,4]) / 2
834
+
835
+ elif crystal_system == 'tetragonal':
836
+ c_dict['C11'] = (c[0,0] + c[1,1]) / 2
837
+ c_dict['C33'] = c[2,2]
838
+ c_dict['C12'] = c[0,1]
839
+ c_dict['C13'] = (c[0,2] + c[1,2]) / 2
840
+ c_dict['C16'] = (c[0,5] - c[1,5]) / 2
841
+ c_dict['C44'] = (c[3,3] + c[4,4]) / 2
842
+ c_dict['C66'] = c[5,5]
843
+
844
+ elif crystal_system == 'rhombohedral':
845
+ c_dict['C11'] = (c[0,0] + c[1,1]) / 2
846
+ c_dict['C33'] = c[2,2]
847
+ c_dict['C12'] = (c[0,1] + (c[0,0] - 2*c[5,5])) / 2
848
+ c_dict['C13'] = (c[0,2] + c[1,2]) / 2
849
+ c_dict['C14'] = (c[0,3] - c[1,3]) / 2
850
+ c_dict['C15'] = (c[0,4] - c[1,4] - c[3,5]) / 3
851
+ c_dict['C44'] = (c[3,3] + c[4,4]) / 2
852
+
853
+ elif crystal_system == 'orthorhombic':
854
+ c_dict['C11'] = c[0,0]
855
+ c_dict['C22'] = c[1,1]
856
+ c_dict['C33'] = c[2,2]
857
+ c_dict['C12'] = c[0,1]
858
+ c_dict['C13'] = c[0,2]
859
+ c_dict['C23'] = c[1,2]
860
+ c_dict['C44'] = c[3,3]
861
+ c_dict['C55'] = c[4,4]
862
+ c_dict['C66'] = c[5,5]
863
+
864
+ elif crystal_system == 'monoclinic':
865
+ c_dict['C11'] = c[0,0]
866
+ c_dict['C22'] = c[1,1]
867
+ c_dict['C33'] = c[2,2]
868
+ c_dict['C12'] = c[0,1]
869
+ c_dict['C13'] = c[0,2]
870
+ c_dict['C15'] = c[0,4]
871
+ c_dict['C23'] = c[1,2]
872
+ c_dict['C25'] = c[1,4]
873
+ c_dict['C35'] = c[2,4]
874
+ c_dict['C46'] = c[3,5]
875
+ c_dict['C44'] = c[3,3]
876
+ c_dict['C55'] = c[4,4]
877
+ c_dict['C66'] = c[5,5]
878
+
879
+ elif crystal_system == 'triclinic':
880
+ c_dict['Cij'] = c
881
+
882
+ else:
883
+ raise ValueError('Invalid crystal_system: ' + crystal_system)
884
+
885
+ if return_dict:
886
+ return c_dict
887
+ return ElasticConstants2(**c_dict)
888
+
889
+ def is_normal(self,
890
+ crystal_system: str,
891
+ atol: float = 1e-4,
892
+ rtol: float = 1e-4) -> bool:
893
+ """
894
+ Checks if current elastic constants agree with values normalized to
895
+ a specified crystal family (within tolerances).
896
+
897
+ Parameters
898
+ ----------
899
+ crystal_system : str
900
+ Indicates the crystal system representation to use when building a
901
+ data model.
902
+ atol : float, optional
903
+ Absolute tolerance to use. Default value is 1e-4.
904
+ rtol : float, optional
905
+ Relative tolerance to use. Default value is 1e-4.
906
+
907
+ Returns
908
+ -------
909
+ bool
910
+ True if all Cij match within the tolerances, false otherwise.
911
+ """
912
+ return np.allclose(self.Cij, self.normalized_as(crystal_system).Cij,
913
+ atol=atol, rtol=rtol)
914
+
915
+ def model(self,
916
+ model: Union[str, io.IOBase, DM, None] = None,
917
+ unit: Optional[str] = None,
918
+ crystal_system: str = 'triclinic') -> Optional[DM]:
919
+ """
920
+ Return or set DataModelDict representation of the elastic constants.
921
+
922
+ Parameters
923
+ ----------
924
+ model : DataModelDict, string, or file-like object, optional
925
+ Data model containing exactly one 'elastic-constants' branch to
926
+ read.
927
+ unit : str, optional
928
+ Units or pressure to save values in when building a data model.
929
+ Default value is None (no conversion).
930
+ crystal_system : str, optional
931
+ Indicates the crystal system representation to normalize by.
932
+ Default value is 'triclinic', i.e. no normalization.
933
+
934
+ Returns
935
+ -------
936
+ DataModelDict
937
+ If model is not given as a parameter.
938
+ """
939
+
940
+ # Set values if model given
941
+ if model is not None:
942
+
943
+ # Find elastic-constants element
944
+ model = DM(model).find('elastic-constants')
945
+
946
+ # Read in values
947
+ if 'Cij' in model:
948
+ # New format
949
+ self.Cij = uc.value_unit(model['Cij'])
950
+ else:
951
+ # Old format
952
+ c_dict = {}
953
+ for C in model['C']:
954
+ key = 'C' + C['ij'][0] + C['ij'][2]
955
+ c_dict[key] = uc.value_unit(C['stiffness'])
956
+ self.Cij = ElasticConstants2(**c_dict).Cij
957
+
958
+ # Return DataModelDict if model not given
959
+ else:
960
+ normCij = self.normalized_as(crystal_system).Cij
961
+ model = DM()
962
+ model['elastic-constants'] = DM()
963
+ model['elastic-constants']['Cij'] = uc.model(normCij, unit)
964
+
965
+ return model
966
+
967
+ def bulk(self, style: str = 'Hill') -> float:
968
+ """
969
+ Returns a bulk modulus estimate.
970
+
971
+ Parameters
972
+ ----------
973
+ style : str
974
+ Indicates which style of estimate to use. Default value is 'Hill'.
975
+ - 'Hill' -- Hill estimate (average of Voigt and Reuss).
976
+ - 'Voigt' -- Voigt estimate. Uses Cij.
977
+ - 'Reuss' -- Reuss estimate. Uses Sij.
978
+ """
979
+ if style == 'Hill':
980
+ return (self.bulk('Voigt') + self.bulk('Reuss')) / 2
981
+
982
+ elif style == 'Voigt':
983
+ c = self.Cij
984
+ return ( (c[0,0] + c[1,1] + c[2,2]) + 2*(c[0,1] + c[1,2] + c[0,2]) ) / 9
985
+
986
+ elif style == 'Reuss':
987
+ s = self.Sij
988
+ return 1 / ( (s[0,0] + s[1,1] + s[2,2]) + 2*(s[0,1] + s[1,2] + s[0,2]) )
989
+
990
+ else:
991
+ raise ValueError('Unknown estimate style')
992
+
993
+ def shear(self, style: str = 'Hill') -> float:
994
+ """
995
+ Returns a shear modulus estimate.
996
+
997
+ Parameters
998
+ ----------
999
+ style : str
1000
+ Indicates which style of estimate to use. Default value is 'Hill'.
1001
+ - 'Hill' -- Hill estimate (average of Voigt and Reuss).
1002
+ - 'Voigt' -- Voigt estimate. Uses Cij.
1003
+ - 'Reuss' -- Reuss estimate. Uses Sij.
1004
+ """
1005
+ if style == 'Hill':
1006
+ return (self.shear('Voigt') + self.shear('Reuss')) / 2
1007
+
1008
+ elif style == 'Voigt':
1009
+ c = self.Cij
1010
+ return ( (c[0,0] + c[1,1] + c[2,2]) - (c[0,1] + c[1,2] + c[0,2]) + 3*(c[3,3] + c[4,4] + c[5,5]) ) / 15
1011
+
1012
+ elif style == 'Reuss':
1013
+ s = self.Sij
1014
+ return 15 / ( 4*(s[0,0] + s[1,1] + s[2,2]) - 4*(s[0,1] + s[1,2] + s[0,2]) + 3*(s[3,3] + s[4,4] + s[5,5]) )
1015
+
1016
+ else:
1017
+ raise ValueError('Unknown estimate style')
atomman/source/atomman/core/NeighborList.py ADDED
@@ -0,0 +1,263 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+
3
+ # Standard Python imports
4
+ import io
5
+ from typing import Union, Optional
6
+
7
+ # http://www.numpy.org/
8
+ import numpy as np
9
+
10
+ # atomman imports
11
+ from .nlist import nlist
12
+ from ..tools import uber_open_rmode
13
+
14
+ class NeighborList(object):
15
+ """Class that finds and stores the neighbor atoms for a system."""
16
+
17
+ def __init__(self, **kwargs):
18
+ """
19
+ Class initializer. Calls NeighborList.load() if
20
+ 'model' is given, otherwise calls NeighborList.build().
21
+
22
+ Parameters
23
+ ----------
24
+ system : atomman.System, optional
25
+ The system to calculate the neighbor list for. Must be given if
26
+ model is not given.
27
+ cutoff : float, optional
28
+ Radial cutoff distance for identifying neighbors. Must be given if
29
+ model is not given.
30
+ model : str or file-like object, optional
31
+ Gives the file path or content to load. If given, no other
32
+ parameters are allowed.
33
+ initialsize : int, optional
34
+ The number of neighbor positions to initially assign to each atom.
35
+ Default value is 20.
36
+ deltasize : int, optional
37
+ Specifies the number of extra neighbor positions to allow each atom
38
+ when the number of neighbors exceeds the underlying array size.
39
+ Default value is 10.
40
+ nlist : array-like object, optional
41
+ An array consisting of int values, where each row is associated with
42
+ an atom, the first value in each row is that atom's coordination, and
43
+ all subsequent values (up to the coordination number) are indices of
44
+ the neighboring atoms.
45
+ """
46
+ # Load from model
47
+ if 'model' in kwargs:
48
+ model = kwargs.pop('model')
49
+ self.load(model, **kwargs)
50
+
51
+ # Convert from existing nlist array
52
+ elif 'nlist' in kwargs:
53
+ nlist = kwargs.pop('nlist')
54
+ assert len(kwargs) == 0
55
+ self.__nlist = nlist
56
+ self.__coord = nlist[:, 0]
57
+ self.__neighbors = nlist[:, 1:]
58
+
59
+ # Build new neighbor list
60
+ else:
61
+ system = kwargs.pop('system')
62
+ cutoff = kwargs.pop('cutoff')
63
+ self.build(system, cutoff, **kwargs)
64
+
65
+
66
+ @classmethod
67
+ def freud(cls,
68
+ system,
69
+ cutoff: Optional[float] = None,
70
+ coord: Optional[int] = None,
71
+ return_bonds: bool = False):
72
+ """
73
+ Use the AABBQuery from the freud Python package to build the neighbor
74
+ list. This has better performance than the native atomman method and
75
+ allows for searching by number of neighbors, but requires additional
76
+ package installs.
77
+
78
+ Parameters
79
+ ----------
80
+ system : atomman.System
81
+ The system to calculate the neighbor list for.
82
+ cutoff : float, optional
83
+ Radial cutoff distance for identifying neighbors. Either cutoff
84
+ or coord are required.
85
+ coord : int, optional
86
+ Number of neighbors to find for each atom. Either cutoff
87
+ or coord are required.
88
+ return_bonds : bool, optional
89
+ If True, then the method will also return the list of bonds
90
+ found by the AABBQuery. Useful if you also want the dmag vects
91
+ for all neighbor pairs.
92
+ """
93
+ try:
94
+ import freud
95
+ except ModuleNotFoundError as e:
96
+ raise ModuleNotFoundError('package freud must be installed for this method') from e
97
+
98
+ # Build freud query terms dict
99
+ if cutoff is not None:
100
+ if coord is not None:
101
+ raise ValueError('cutoff and coord cannot both be given')
102
+ query_terms = dict(
103
+ mode = 'ball',
104
+ r_max = cutoff,
105
+ exclude_ii = True)
106
+
107
+ elif coord is not None:
108
+ query_terms = dict(
109
+ mode = 'nearest',
110
+ num_neighbors = coord,
111
+ exclude_ii = True)
112
+
113
+ else:
114
+ raise ValueError('either cutoff or coord must be given')
115
+
116
+ # Convert atomman system into freud box and points
117
+ box, points = system.dump('freud')
118
+
119
+ # Build query object and perform the query
120
+ aq = freud.locality.AABBQuery(box, points)
121
+ bonds = aq.query(points, query_terms)
122
+ coord = 15
123
+ # Find max coord
124
+ if coord is None:
125
+ coord = 0 # coord is max coord across all atoms
126
+ last_i = -1
127
+ c = 0 # c is current coord count for atom i
128
+ for bond in bonds:
129
+ i, j, dmag = bond
130
+ if i != last_i:
131
+ if c > coord:
132
+ coord = c
133
+ last_i = i
134
+ c = 1
135
+ else:
136
+ c += 1
137
+
138
+ # Build nlist
139
+ nlist = np.empty((system.natoms, coord+1), dtype=np.int64)
140
+ nlist[:, 0] = 0
141
+
142
+ for bond in bonds:
143
+ i, j, dmag = bond
144
+
145
+ # Increase atom's coordination
146
+ nlist[i, 0] += 1
147
+
148
+ # Add j to the nlist row
149
+ nlist[i, nlist[i, 0]] = j
150
+
151
+ # Initialize neighborlist object
152
+ neighbors = cls(nlist=nlist)
153
+
154
+ if return_bonds:
155
+ return neighbors, bonds
156
+ else:
157
+ return neighbors
158
+
159
+
160
+ @property
161
+ def coord(self) -> np.ndarray:
162
+ """numpy.ndarray: The atomic coordination numbers"""
163
+ return self.__coord
164
+
165
+ @property
166
+ def nlist(self) -> np.ndarray:
167
+ """numpy.ndarray: The underlying numpy array of coord + neighbor ids"""
168
+ return self.__nlist
169
+
170
+ def __len__(self) -> int:
171
+ """len returns the number of atoms"""
172
+ return len(self.__coord)
173
+
174
+ def __getitem__(self, key):
175
+ """Get returns the list of neighbors for the specified atom."""
176
+ return self.__neighbors[key, :self.coord[key]]
177
+
178
+ def build(self,
179
+ system,
180
+ cutoff: float,
181
+ initialsize: int = 20,
182
+ deltasize: int = 10):
183
+ """
184
+ Builds the neighbor list for a system.
185
+
186
+ Parameters
187
+ ----------
188
+ system : atomman.System
189
+ The system to calculate the neighbor list for.
190
+ cutoff : float
191
+ Radial cutoff distance for identifying neighbors.
192
+ initialsize : int, optional
193
+ The number of neighbor positions to initially assign to each atom.
194
+ Default value is 20.
195
+ deltasize : int, optional
196
+ Specifies the number of extra neighbor positions to allow each atom
197
+ when the number of neighbors exceeds the underlying array size.
198
+ Default value is 10.
199
+ """
200
+ # Call nlist
201
+ self.__nlist = nlist(system, cutoff, initialsize=initialsize,
202
+ deltasize=deltasize)
203
+
204
+ # Split coord and neighbors
205
+ self.__coord = self.__nlist[:, 0]
206
+ self.__neighbors = self.__nlist[:, 1:]
207
+
208
+ def load(self, model: Union[str, io.IOBase]):
209
+ """
210
+ Read in a neighbor list from a file.
211
+
212
+ Parameters
213
+ ----------
214
+ model : str or file-like object
215
+ Gives the file path or content to load.
216
+ """
217
+ # First pass determines number of atoms and max number of neighbors
218
+ nterms = 0
219
+ natoms = 0
220
+ with uber_open_rmode(model) as fin:
221
+ for line in fin:
222
+ line = line.decode('UTF-8')
223
+ terms = line.split()
224
+ n_n = len(terms)
225
+ if terms[0][0] != '#' and n_n > 0:
226
+ natoms += 1
227
+ if n_n > nterms:
228
+ nterms = n_n
229
+
230
+ self.__nlist = np.empty((natoms, nterms+1), dtype=int)
231
+ self.__coord = self.__nlist[:, 0]
232
+ self.__neighbors = self.__nlist[:, 1:]
233
+
234
+ # Second pass gets values
235
+ self.__coord[:] = 0
236
+ fin.seek(0)
237
+ for line in fin:
238
+ line = line.decode('UTF-8')
239
+ terms = line.split()
240
+ if len(terms) > 0 and terms[0][0] != '#':
241
+ i = int(terms[0])
242
+ self.__coord[i] = len(terms) - 1
243
+ for j in range(1, len(terms)):
244
+ self.__neighbors[i, j-1] = terms[j]
245
+
246
+ def dump(self, fname: str):
247
+ """
248
+ Saves the neighbor list to a file.
249
+
250
+ Parameters
251
+ ----------
252
+ fname : str
253
+ The file name to save the content to.
254
+ """
255
+ with open(fname, 'w') as fp:
256
+ fp.write('# Neighbor list:\n')
257
+ fp.write('# The first column gives an atom index.\n')
258
+ fp.write('# The rest of the columns are the indexes of the identified neighbors.\n')
259
+ for i in range(len(self)):
260
+ fp.write('%i' % i)
261
+ for j in self[i]:
262
+ fp.write(' %i' % j)
263
+ fp.write('\n')
atomman/source/atomman/core/System.py ADDED
@@ -0,0 +1,1267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ from __future__ import annotations
4
+ import io
5
+ from collections import OrderedDict
6
+ from copy import deepcopy
7
+ import warnings
8
+ from typing import Any, Optional, Union, Tuple
9
+
10
+ # http://www.numpy.org/
11
+ import numpy as np
12
+ import numpy.typing as npt
13
+
14
+ # https://pandas.pydata.org/
15
+ import pandas as pd
16
+
17
+ # https://github.com/usnistgov/DataModelDict
18
+ from DataModelDict import DataModelDict as DM
19
+
20
+ # atomman imports
21
+ import atomman.unitconvert as uc
22
+ from . import Atoms, Box, dvect, dmag, NeighborList
23
+ from ..lammps import normalize as lmp_normalize
24
+ from ..tools import indexstr, miller, ishexagonal, aslist
25
+ from .. import dump
26
+
27
+ class System(object):
28
+ """
29
+ A representation of an atomic system. This combines Atoms with Box and
30
+ adds methods and attributes involving both.
31
+ """
32
+
33
+ class _AtomsIndexer(object):
34
+ """Internal class for setitem / getitem acting on atoms"""
35
+ def __init__(self, host: System):
36
+ self.__host = host
37
+
38
+ def __getitem__(self,
39
+ index: Union[int, list, slice]) -> System:
40
+ """Index getting of Atoms that operates on System."""
41
+ host = self.__host
42
+ return System(atoms=host.atoms[index], box=host.box, pbc=host.pbc,
43
+ symbols=host.symbols)
44
+
45
+ def __setitem__(self,
46
+ index: Union[int, list, slice],
47
+ value: Any):
48
+ """Index setting of Atoms that operates on System."""
49
+ host = self.__host
50
+ if isinstance(value, Atoms):
51
+ host.atoms[index] = value
52
+ elif isinstance(value, System):
53
+ try:
54
+ assert np.allclose(host.box.vects, value.box.vects)
55
+ assert np.allclose(host.box.origin, value.box.origin)
56
+ except:
57
+ warnings.warn('Atom assignment between two Systems with different boxes', UserWarning)
58
+ host.atoms[index] = value.atoms
59
+ else:
60
+ raise ValueError('Can only set using Atoms or System objects')
61
+
62
+ def __init__(self,
63
+ atoms: Optional[Atoms] = None,
64
+ box: Optional[Box] = None,
65
+ pbc: Optional[Tuple[bool, bool, bool]] = None,
66
+ scale: bool = False,
67
+ symbols: Union[str, list, None] = None,
68
+ masses: Union[float, list, None] = None,
69
+ model: Union[str, io.IOBase, DM, None] = None,
70
+ safecopy: bool = False):
71
+ """
72
+ Initialize a System by joining an am.Atoms and am.Box instance.
73
+
74
+ Parameters
75
+ ----------
76
+ atoms : atomman.Atoms, optional
77
+ The underlying Atoms object to build system around.
78
+ box : atomman.Box, optional
79
+ The underlying box object to build system around.
80
+ pbc : tuple or list of bool, optional
81
+ Indicates which of the dimensions related to the three box vectors
82
+ are periodic. Default value is (True, True, True).
83
+ scale : bool, optional
84
+ If True, atoms.pos will be scaled relative to the box. Default
85
+ value is False.
86
+ symbols : tuple, optional
87
+ A list of the element symbols for each atom atype. If len(symbols)
88
+ is less than natypes, then missing values will be set to None.
89
+ Default sets list with all None values.
90
+ masses : tuple, optional
91
+ A list of the masses for each atom atype. If len(symbols) is less
92
+ than natypes, then missing values will be set to None. Default
93
+ sets list with all None values.
94
+ model : str or DataModelDict, optional
95
+ File path or content of a JSON/XML data model containing all
96
+ system information. Cannot be given with atoms, box or scale.
97
+ safecopy : bool, optional
98
+ Flag indicating if values are to be copied before setting. For
99
+ values given as objects, direct setting (False, default) may result
100
+ in the System pointing to the original object. Using safecopy=True
101
+ deep copies the objects before setting to avoid this. Note that
102
+ safecopy=True may be considerably slower for large numbers of atoms
103
+ and/or properties.
104
+
105
+ Returns
106
+ =======
107
+ System
108
+ The System object.
109
+ """
110
+ # Check for model
111
+ if model is not None:
112
+ try:
113
+ assert atoms is None
114
+ assert box is None
115
+ assert scale is False
116
+ except:
117
+ raise ValueError('model cannot be given with atoms, box or scale parameters')
118
+
119
+ # Load data model
120
+ model = DM(model).find('atomic-system')
121
+
122
+ # Extract values from model
123
+ box = Box(model=model)
124
+ atoms = Atoms(model=model)
125
+ if pbc is None:
126
+ pbc = model['periodic-boundary-condition']
127
+ if symbols is None:
128
+ symbols = tuple(model.aslist('atom-type-symbol'))
129
+ if masses is None:
130
+ masses = tuple(model.aslist('atom-type-mass'))
131
+
132
+ # Interpret given/missing parameters
133
+ else:
134
+ # Set default atoms or deepcopy
135
+ if atoms is None:
136
+ atoms = Atoms()
137
+ elif safecopy:
138
+ atoms = deepcopy(atoms)
139
+
140
+ # Set default box or deepcopy
141
+ if box is None:
142
+ box = Box()
143
+ elif safecopy:
144
+ box = deepcopy(box)
145
+
146
+ # Set default pbc
147
+ if pbc is None:
148
+ pbc = (True, True, True)
149
+
150
+ # Set default masses
151
+ if masses is None:
152
+ masses = []
153
+ else:
154
+ masses = aslist(masses)
155
+
156
+ # Set default symbols
157
+ if symbols is None:
158
+ symbols = [None for i in range(len(masses))]
159
+ else:
160
+ symbols = aslist(symbols)
161
+
162
+ # Check data types
163
+ if not isinstance(atoms, Atoms):
164
+ raise TypeError('Invalid atoms type')
165
+ if not isinstance(box, Box):
166
+ raise TypeError('Invalid box type')
167
+ if not isinstance(scale, bool):
168
+ raise TypeError('Invalid scale type')
169
+
170
+ # Set properties
171
+ self.__atoms = atoms
172
+ self.__box = box
173
+ self.pbc = pbc
174
+ self.__transformation = np.identity(3)
175
+
176
+ self.symbols = symbols
177
+ self.masses = masses
178
+
179
+ # Scale pos if needed
180
+ if scale is True:
181
+ self.atoms_prop('pos', value=self.atoms.pos, scale=True)
182
+
183
+ # Scale model properties if needed
184
+ if model is not None:
185
+ for prop in model['atoms'].aslist('property'):
186
+ if prop['data'].get('unit', None) == 'scaled':
187
+ self.atoms.view[prop['name']] = self.box.position_relative_to_cartesian(self.atoms.view[prop['name']])
188
+
189
+ # Set atoms indexer
190
+ self.__atoms_ix = System._AtomsIndexer(self)
191
+
192
+ def __str__(self) -> str:
193
+ """str : The string representation of a system."""
194
+ return '\n'.join([str(self.box),
195
+ 'natoms = ' + str(self.natoms),
196
+ 'natypes = ' + str(self.natypes),
197
+ 'symbols = ' + str(self.symbols),
198
+ 'pbc = ' + str(self.pbc),
199
+ str(self.atoms)])
200
+
201
+ def __len__(self) -> int:
202
+ return self.natoms
203
+
204
+ @property
205
+ def atoms(self) -> Atoms:
206
+ """atomman.Atoms : underlying Atoms object."""
207
+ return self.__atoms
208
+
209
+ @property
210
+ def natoms(self) -> int:
211
+ """int : The number of atoms in the Atoms class."""
212
+ return self.__atoms.natoms
213
+
214
+ @property
215
+ def atypes(self) -> tuple:
216
+ """tuple : List of int atom types."""
217
+ return tuple(range(1, self.natypes+1))
218
+
219
+ @property
220
+ def natypes(self) -> int:
221
+ """int : The number of atom types/symbols."""
222
+
223
+ # Check if more symbols than atypes
224
+ try:
225
+ nsymbols = len(self.symbols)
226
+ except:
227
+ nsymbols = 0
228
+ if nsymbols > self.__atoms.natypes:
229
+ return len(self.symbols)
230
+ else:
231
+ return self.__atoms.natypes
232
+
233
+ @property
234
+ def box(self) -> Box:
235
+ """atommman.Box : underlying Box object."""
236
+ return self.__box
237
+
238
+ @property
239
+ def pbc(self) -> np.ndarray:
240
+ """numpy.ndarray of bool : The periodic boundary condition settings."""
241
+ return self.__pbc
242
+
243
+ @pbc.setter
244
+ def pbc(self, value: npt.ArrayLike):
245
+ pbc = np.asarray(value, dtype=bool)
246
+ assert pbc.shape == (3,), 'invalid pbc entry'
247
+ self.__pbc = pbc
248
+
249
+ @property
250
+ def atoms_ix(self) -> _AtomsIndexer:
251
+ """Indexer for index slicing of Systems by per-atom properties."""
252
+ return self.__atoms_ix
253
+
254
+ @property
255
+ def symbols(self) -> tuple:
256
+ """tuple : The element model symbols associated with each atype."""
257
+
258
+ # Fill in missing values
259
+ if len(self.__symbols) < self.__atoms.natypes:
260
+ self.symbols = self.__symbols
261
+ return self.__symbols
262
+
263
+ @symbols.setter
264
+ def symbols(self, value: Union[str, list]):
265
+
266
+ # Make value list if needed
267
+ value = aslist(value)
268
+
269
+ # Fill in missing values
270
+ if len(value) < self.__atoms.natypes:
271
+ newvalue = [None for x in range(self.__atoms.natypes)]
272
+ for i in range(len(value)):
273
+ newvalue[i] = value[i]
274
+ value = newvalue
275
+
276
+ self.__symbols = tuple(value)
277
+
278
+ @property
279
+ def masses(self) -> tuple:
280
+ """tuple : The masses associated with each atype if given."""
281
+
282
+ # Fill in missing values
283
+ if len(self.__masses) < self.natypes:
284
+ self.masses = self.__masses
285
+ return self.__masses
286
+
287
+ @masses.setter
288
+ def masses(self, value: Union[float, list]):
289
+
290
+ # Make value list if needed
291
+ value = aslist(value)
292
+
293
+ # Convert non-None values to float
294
+ for i in range(len(value)):
295
+ if value[i] is not None:
296
+ value[i] = float(value[i])
297
+
298
+ # Fill in missing values
299
+ if len(value) < self.natypes:
300
+ newvalue = [None for x in range(self.natypes)]
301
+ for i in range(len(value)):
302
+ newvalue[i] = value[i]
303
+ value = newvalue
304
+ elif len(value) > self.natypes:
305
+ raise ValueError('More masses than atom types given. Either change atype values or symbols first.')
306
+
307
+ self.__masses = tuple(value)
308
+
309
+ @property
310
+ def composition(self) -> Optional[str]:
311
+ """
312
+ The system's reduced and sorted symbols composition.
313
+
314
+ Returns
315
+ -------
316
+ str or None
317
+ The system's reduced and sorted symbols composition.
318
+ Will return None if any symbols are missing
319
+ """
320
+
321
+ # Build total count of each symbol
322
+ sym_dict = {}
323
+ for i in range(self.natypes):
324
+ count = np.sum(self.atoms.atype == i+1)
325
+ if count > 0:
326
+ symbol = self.symbols[i]
327
+
328
+ if symbol is None:
329
+ return None
330
+
331
+ if symbol in sym_dict:
332
+ sym_dict[symbol] += count
333
+ else:
334
+ sym_dict[symbol] = count
335
+
336
+ # Find greatest common denominator
337
+ gcd = np.gcd.reduce(list(sym_dict.values())) # pylint: disable=no-member
338
+
339
+ # Sort symbols and reduce counts
340
+ composition =''
341
+ for symbol in sorted(sym_dict):
342
+ count = sym_dict[symbol] // gcd
343
+ if sym_dict[symbol] > 0:
344
+ composition += symbol
345
+ if count != 1:
346
+ composition += str(count)
347
+
348
+ return composition
349
+
350
+ def atoms_prop(self,
351
+ key: Optional[str] = None,
352
+ index: Union[int, list, slice, None] = None,
353
+ value: Optional[Any] = None,
354
+ a_id: Optional[int] = None,
355
+ scale: bool = False
356
+ ) -> Union[list, Atoms, np.ndarray, None]:
357
+ """
358
+ Extends Atoms.prop() by adding a scale argument.
359
+
360
+ Parameters
361
+ ----------
362
+ key : str, optional
363
+ Per-atom property name.
364
+ index : int, list, slice, optional
365
+ Index of atoms.
366
+ value : any, optional
367
+ Property values to assign.
368
+ a_id : int, optional
369
+ Integer atom index. Left in for backwards compatibility.
370
+ scale : bool, optional
371
+ Flag indicating if values should be scaled/unscaled. If True:
372
+ - Values being retrieved are scaled from absolute Cartesian to box relative vectors.
373
+ - Values being set are unscaled from box relative to absolute Cartesian vectors.
374
+ Default value is False.
375
+
376
+ Returns
377
+ -------
378
+ list
379
+ If no parameters given, returns a list of all assigned property
380
+ keys.
381
+ atomman.Atoms
382
+ If index or a_id is given without value or key, returns a new
383
+ Atoms instance for the specified atom indices.
384
+ numpy.ndarray
385
+ If key (and index/a_id) is given without value, returns a copy of
386
+ the data associated with that property key.
387
+ """
388
+
389
+ # Check that scale is bool
390
+ if not isinstance(scale, bool):
391
+ raise TypeError('Invalid scale type')
392
+
393
+ # Call atoms.prop() for scale is False
394
+ if scale is False:
395
+ if value is None:
396
+ return self.atoms.prop(key=key, index=index, a_id=a_id)
397
+ else:
398
+ self.atoms.prop(key=key, index=index, value=value, a_id=a_id)
399
+
400
+ # Handle property scaling if scale is True
401
+ else:
402
+ # Handle a_id
403
+ if a_id is not None:
404
+ if index is not None:
405
+ raise ValueError('a_id and index cannot both be given')
406
+ index = a_id
407
+
408
+ # Get values if value is None
409
+ if value is None:
410
+ # If no key, return copy of atoms (all or a slice) with scaled pos
411
+ if key is None:
412
+ if index is None:
413
+ newatoms = deepcopy(self.atoms)
414
+ else:
415
+ newatoms = deepcopy(self.atoms[index])
416
+ newatoms.pos = self.box.position_cartesian_to_relative(newatoms.pos)
417
+ return newatoms
418
+
419
+ # If key is given, return scaled property values
420
+ else:
421
+ if index is None:
422
+ value = self.atoms.view[key]
423
+ else:
424
+ value = self.atoms.view[key][index]
425
+ return self.box.position_cartesian_to_relative(value)
426
+
427
+ # Set values if value is given
428
+ else:
429
+ # If no key, unscale pos of Atoms value and set to atoms (or a slice)
430
+ if key is None:
431
+ if not isinstance(value, Atoms):
432
+ raise TypeError('If key is None, value must be instance of atomman.Atoms')
433
+
434
+ value.pos = self.box.position_relative_to_cartesian(value.pos)
435
+ if index is None:
436
+ self.atoms[:] = value
437
+ else:
438
+ self.atoms[index] = value
439
+
440
+ # If key is given, unscale value and set to property
441
+ else:
442
+ value = self.box.position_relative_to_cartesian(value)
443
+ if index is None:
444
+ self.atoms.view[key] = value
445
+ else:
446
+ self.atoms.view[key][index] = value
447
+
448
+ def atoms_df(self,
449
+ scale: bool = False) -> pd.DataFrame:
450
+ """
451
+ Extends Atoms.df() by adding a scale argument.
452
+
453
+ Parameters
454
+ ----------
455
+ scale : bool or list, optional
456
+ Indicates if/which per-atom properties are to be scaled to box
457
+ relative values. If False (default), no properties will be scaled.
458
+ If True, pos will be scaled. Multiple properties can be scaled by
459
+ providing a list of the property names to scale.
460
+
461
+ Returns
462
+ -------
463
+ pandas.DataFrame
464
+ Tabulated DataFrame version of the atomic data.
465
+ """
466
+ # Handle scale values
467
+ if scale is True:
468
+ scale = ['pos']
469
+ elif scale is False:
470
+ scale = []
471
+ elif not isinstance(scale, list):
472
+ scale = [scale]
473
+
474
+ # Initialize new dictionary of values
475
+ values = OrderedDict()
476
+ for key in self.atoms.view.keys():
477
+
478
+ value = self.atoms.view[key]
479
+ if key in scale:
480
+ value = self.box.position_cartesian_to_relative(value)
481
+
482
+ # Flatten multidimensional arrays
483
+ for index, istr in indexstr(self.atoms.view[key].shape[1:]):
484
+ newkey = key + istr
485
+
486
+ # Copy values over
487
+ if index == ():
488
+ values[newkey] = value
489
+ else:
490
+ values[newkey] = value[(Ellipsis, ) + index]
491
+
492
+ # Return DataFrame
493
+ return pd.DataFrame(values)
494
+
495
+ def atoms_extend(self,
496
+ value: Union[Atoms, int],
497
+ scale: bool = False,
498
+ symbols: Optional[list] = None,
499
+ safecopy: bool = False) -> System:
500
+ """
501
+ Extends Atoms.extend() to the System level by adding scale and symbols
502
+ arguments.
503
+
504
+ Parameters
505
+ ----------
506
+ value : atomman.Atoms or int
507
+ An int value will result in the atoms object being extended by
508
+ that number of atoms, with all per-atom properties having default
509
+ values (atype = 1, everything else = 0). For an Atoms value, the
510
+ current atoms list will be extended by the correct number of atoms
511
+ and all per-atom properties in value will be copied over. Any
512
+ properties defined in one Atoms object and not the other will be
513
+ set to default values.
514
+ scale : bool, optional
515
+ Flag indicating if position values in a supplied Atoms value are to
516
+ be taken as absolute Cartesian (False, default) or in scaled box
517
+ relative units (True).
518
+ symbols : tuple, list or None, optional
519
+ Allows for the system's symbols list to be updated. If not given,
520
+ will use the current object's symbols.
521
+ safecopy : bool, optional
522
+ Flag indicating if values are to be copied before setting. If
523
+ False (default), underlying objects may be shared between the new
524
+ system and the current system and input parameters. If True, atoms
525
+ and box will be deepcopied before setting.
526
+ Note that safecopy=True may be considerably slower for large
527
+ numbers of atoms and/or properties.
528
+
529
+ Returns
530
+ -------
531
+ atomman.System
532
+ A new System object with Atoms extended to contain all atoms and
533
+ properties of the current object plus the additional atoms. The
534
+ current System object's box and pbc (and symbols if not specified)
535
+ will be copied over.
536
+ """
537
+ # Copy value if safecopy
538
+ if safecopy:
539
+ value = deepcopy(value)
540
+
541
+ # scale only makes sense for Atoms values
542
+ if scale is True and not isinstance(value, Atoms):
543
+ raise ValueError('scale can only be True for Atoms values')
544
+
545
+ # Handle symbols parameter
546
+ if symbols is None:
547
+ symbols = self.symbols
548
+
549
+ # Handle box
550
+ if safecopy:
551
+ box = deepcopy(self.box)
552
+ else:
553
+ box = self.box
554
+
555
+ # Call atoms.extend to generate new atoms
556
+ atoms = self.atoms.extend(value)
557
+
558
+ # Unscale pos from Atoms value if needed
559
+ if scale:
560
+ atoms.pos[value.natoms:] = self.box.position_relative_to_cartesian(value.pos)
561
+
562
+ # Generate and return new System
563
+ return System(atoms=atoms, box=box, pbc=self.pbc, symbols=symbols)
564
+
565
+ def box_set(self, **kwargs):
566
+ """
567
+ Extends box.set() with a scale argument.
568
+
569
+ Parameters
570
+ ----------
571
+ scale : bool
572
+ If True, the scaled (box-relative) positions remain unchanged.
573
+ Default value is False (absolute positions unchanged).
574
+ other kwargs : any
575
+ Any other kwargs allowed by box.set().
576
+ """
577
+
578
+ # Pop scale
579
+ scale = kwargs.pop('scale', False)
580
+ if not isinstance(scale, bool):
581
+ raise TypeError('Invalid scale type')
582
+
583
+ # Hold scaled positions constant
584
+ if scale is True:
585
+ spos = self.atoms_prop('pos', scale=True)
586
+ self.box.set(**kwargs)
587
+ self.atoms_prop('pos', value=spos, scale=True)
588
+
589
+ # Call box.set without scaling
590
+ else:
591
+ self.box.set(**kwargs)
592
+
593
+ def scale(self, value: npt.ArrayLike) -> np.ndarray:
594
+ """
595
+ Scales 3D vectors from absolute Cartesian coordinates to relative box
596
+ coordinates.
597
+
598
+ Parameters
599
+ ----------
600
+ value : array-like object
601
+ Absolute Cartesian coordinates to scale.
602
+
603
+ Returns
604
+ -------
605
+ numpy.ndarray
606
+ The relative box coordinates associated with the given values.
607
+ """
608
+ warnmsg = "System.scale() will likely be depreciated in the next big version update "
609
+ warnmsg += "as the method name is not informative. It is being replaced by the "
610
+ warnmsg += "equivalent Box.position_cartesian_to_relative() method."
611
+ warnings.warn(warnmsg, PendingDeprecationWarning)
612
+
613
+ # Retrieve parameters
614
+ #value = np.asarray(value, dtype=float)
615
+ #vects = self.box.vects
616
+ #inverse = np.linalg.inv(vects)
617
+ #origin = self.box.origin
618
+
619
+ # Convert
620
+ #return (value - origin).dot(inverse)
621
+
622
+ return self.box.position_cartesian_to_relative(value)
623
+
624
+ def unscale(self, value: npt.ArrayLike) -> np.ndarray:
625
+ """
626
+ Unscales 3D vectors from relative box coordinates to absolute
627
+ Cartesian coordinates.
628
+
629
+ Parameters
630
+ ----------
631
+ value : numpy.ndarray
632
+ Relative box coordinates to unscale.
633
+
634
+ Returns
635
+ -------
636
+ numpy.ndarray
637
+ Absolute Cartesian coordinates associated with the given values.
638
+ """
639
+ warnmsg = "System.unscale() will likely be depreciated in the next big version update "
640
+ warnmsg += "as the method name is not informative. It is being replaced by the "
641
+ warnmsg += "equivalent Box.position_relative_to_cartesian() method."
642
+ warnings.warn(warnmsg, PendingDeprecationWarning)
643
+
644
+ # Retrieve parameters
645
+ #value = np.asarray(value, dtype=float)
646
+ #vects = self.box.vects
647
+ #origin = self.box.origin
648
+
649
+ # Convert
650
+ #return value.dot(vects) + origin
651
+
652
+ return self.box.position_relative_to_cartesian(value)
653
+
654
+ def wrap(self,
655
+ return_imageflags: bool = False) -> Optional[np.ndarray]:
656
+ """
657
+ Wrap atoms around periodic boundaries and extend non-periodic
658
+ boundaries such that all atoms are within the box.
659
+
660
+ Parameters
661
+ ----------
662
+ return_imageflags : bool, optional
663
+ If True, an array of which image the atom was originally in is
664
+ returned.
665
+
666
+ Returns
667
+ -------
668
+ numpy.NDarray of int
669
+ The imageflags array - only returned if return_imageflags = True.
670
+ """
671
+
672
+ # mins and maxs are box dimensions relative to box vectors, i.e 0 to 1
673
+ mins = np.array([0.0, 0.0, 0.0])
674
+ maxs = np.array([1.0, 1.0, 1.0])
675
+
676
+ # Retrieve scaled pos
677
+ spos = self.atoms_prop('pos', scale=True)
678
+
679
+ # Initialize wrapflags
680
+ imageflags = np.zeros_like(spos, dtype=int)
681
+
682
+ # Loop over three pbc directions
683
+ for i in range(3):
684
+
685
+ # Count wraps across periodic boundaries
686
+ if self.pbc[i]:
687
+ imageflags[:, i] = np.floor(spos[:, i]) # pylint: disable=unsupported-assignment-operation
688
+
689
+ # Shift min and max to encompass atoms across non-periodic bounds
690
+ else:
691
+ min = spos[:, i].min()
692
+ max = spos[:, i].max()
693
+ if min <= mins[i]:
694
+ mins[i] = min - 0.001
695
+ if max >= maxs[i]:
696
+ maxs[i] = max + 0.001
697
+
698
+ # Wrap atoms across periodic boundaries
699
+ spos -= imageflags
700
+
701
+ # Unscale spos and save to pos
702
+ self.atoms_prop('pos', value=spos, scale=True)
703
+
704
+ # Modify box vectors and origin by new min and max
705
+ origin = self.box.origin + mins.dot(self.box.vects)
706
+ avect = self.box.avect * (maxs[0] - mins[0])
707
+ bvect = self.box.bvect * (maxs[1] - mins[1])
708
+ cvect = self.box.cvect * (maxs[2] - mins[2])
709
+ self.box_set(avect=avect, bvect=bvect, cvect=cvect, origin=origin)
710
+
711
+ if return_imageflags:
712
+ return imageflags
713
+
714
+ def dvect(self,
715
+ pos_0: Union[int, list, slice, npt.ArrayLike],
716
+ pos_1: Union[int, list, slice, npt.ArrayLike]
717
+ ) -> np.ndarray:
718
+ """
719
+ Computes the shortest vector between pos_0 and pos_1 using box
720
+ dimensions and accounting for periodic boundaries.
721
+
722
+ Parameters
723
+ ----------
724
+ pos_0 : index or array-like object
725
+ Absolute Cartesian vector position(s) to use as reference point(s).
726
+ If the value can be used as an index, then self.atoms.pos[pos_0]
727
+ is taken.
728
+ pos_1 : index or array-like object
729
+ Absolute Cartesian vector position(s) to find relative to pos_0.
730
+ If the value can be used as an index, then self.atoms.pos[pos_1]
731
+ is taken.
732
+
733
+ Returns
734
+ -------
735
+ numpy.ndarray
736
+ The shortest vectors from each pos_0 to pos_1 positions.
737
+ """
738
+ # Test if pos_0 and pos_1 can be used as numpy array indices
739
+ try:
740
+ pos_0 = self.atoms.pos[pos_0]
741
+ except:
742
+ pos_0 = np.asarray(pos_0)
743
+ try:
744
+ pos_1 = self.atoms.pos[pos_1]
745
+ except:
746
+ pos_1 = np.asarray(pos_1)
747
+
748
+ # Call dvect using self's box and pbc
749
+ vects = dvect(pos_0, pos_1, self.box, self.pbc)
750
+ if len(vects) == 1:
751
+ return vects[0]
752
+ else:
753
+ return vects
754
+
755
+ def dmag(self,
756
+ pos_0: Union[int, list, slice, npt.ArrayLike],
757
+ pos_1: Union[int, list, slice, npt.ArrayLike]
758
+ ) -> np.ndarray:
759
+ """
760
+ Computes the shortest distance between pos_0 and pos_1 using box
761
+ dimensions and accounting for periodic boundaries.
762
+
763
+ Parameters
764
+ ----------
765
+ pos_0 : index or array-like object
766
+ Absolute Cartesian vector position(s) to use as reference point(s).
767
+ If the value can be used as an index, then self.atoms.pos[pos_0]
768
+ is taken.
769
+ pos_1 : index or array-like object
770
+ Absolute Cartesian vector position(s) to find relative to pos_0.
771
+ If the value can be used as an index, then self.atoms.pos[pos_1]
772
+ is taken.
773
+
774
+ Returns
775
+ -------
776
+ numpy.ndarray
777
+ The shortest vector magnitude from each pos_0 to pos_1 positions.
778
+ """
779
+ # Test if pos_0 and pos_1 can be used as numpy array indices
780
+ try:
781
+ pos_0 = self.atoms.pos[pos_0]
782
+ except:
783
+ pos_0 = np.asarray(pos_0)
784
+ try:
785
+ pos_1 = self.atoms.pos[pos_1]
786
+ except:
787
+ pos_1 = np.asarray(pos_1)
788
+
789
+ # Call dvect using self's box and pbc
790
+ vects = dmag(pos_0, pos_1, self.box, self.pbc)
791
+ if len(vects) == 1:
792
+ return vects[0]
793
+ else:
794
+ return vects
795
+
796
+ def neighborlist(self, **kwargs) -> NeighborList:
797
+ """
798
+ Builds a neighbor list for the system. The resulting NeighborList
799
+ object is saved to the object as attribute 'neighbors'.
800
+
801
+ Parameters
802
+ ----------
803
+ cutoff : float, optional
804
+ Radial cutoff distance for identifying neighbors. Must be given if
805
+ model is not given.
806
+ model : str or file-like object, optional
807
+ Gives the file path or content to load. If given, no other
808
+ parameters are allowed.
809
+ initialsize : int, optional
810
+ The number of neighbor positions to initially assign to each atom.
811
+ Default value is 20.
812
+ deltasize : int, optional
813
+ Specifies the number of extra neighbor positions to allow each atom
814
+ when the number of neighbors exceeds the underlying array size.
815
+ Default value is 10.
816
+
817
+ Returns
818
+ -------
819
+ atomman.NeighborList
820
+ The compiled list of neighbors.
821
+ """
822
+ if 'system' in kwargs:
823
+ raise KeyError("Parameter 'system' not allowed")
824
+ else:
825
+ kwargs['system'] = self
826
+ return NeighborList(**kwargs)
827
+
828
+ def r0(self,
829
+ neighbors: Optional[NeighborList] = None) -> float:
830
+ """
831
+ Identifies the shortest interatomic spacing between atoms in the
832
+ system by comparing the shortest periodic box vector with the
833
+ smallest dmags of all neighbor atoms.
834
+
835
+ Parameters
836
+ ----------
837
+ neighbors : NeighborList, optional
838
+ A pre-computed NeighborList for the system. If not given, a new
839
+ NeighborList will be computed using a cutoff distance based on the
840
+ smallest dmag between atom 0 and the rest of the system's atoms.
841
+
842
+ Returns
843
+ -------
844
+ float
845
+ The shortest interatomic spacing identified.
846
+ """
847
+
848
+ # Find shortest periodic lattice parameter
849
+ try:
850
+ box_r0 = np.linalg.norm(self.box.vects, axis=1)[self.pbc].min()
851
+ except:
852
+ box_r0 = None
853
+
854
+ # Find shortest interatomic vector
855
+ if self.natoms > 1:
856
+
857
+ # Compute neighbors if needed
858
+ if neighbors is None:
859
+
860
+ # Use r0 for atom 0 as cutoff
861
+ cutoff = self.dmag(0, range(1, self.natoms)).min() * 1.01
862
+
863
+ # Identify all neighbors
864
+ neighbors = self.neighborlist(cutoff=cutoff)
865
+
866
+ # Find smallest r0 across all neighbor sets
867
+ atom_r0 = np.inf
868
+ for i in range(self.natoms):
869
+ j = neighbors[i]
870
+ if len(j) > 0:
871
+ new_r0 = self.dmag(i, j).min()
872
+ if new_r0 < atom_r0:
873
+ atom_r0 = new_r0
874
+
875
+ if atom_r0 == np.inf:
876
+ atom_r0 = None
877
+ else:
878
+ atom_r0 = None
879
+
880
+ if box_r0 is not None and atom_r0 is not None:
881
+ if atom_r0 < box_r0:
882
+ return atom_r0
883
+ else:
884
+ return box_r0
885
+ elif box_r0 is not None:
886
+ return box_r0
887
+ elif atom_r0 is not None:
888
+ return atom_r0
889
+ else:
890
+ raise ValueError('No atoms to compare or periodic boundaries found!')
891
+
892
+ def supersize(self,
893
+ a_size: Union[int, Tuple[int, int]],
894
+ b_size: Union[int, Tuple[int, int]],
895
+ c_size: Union[int, Tuple[int, int]]) -> System:
896
+ """
897
+ Creates a larger system from a given system by replicating it along the
898
+ system's box vectors.
899
+
900
+ The multiplier values \\*_size are taken to be integer tuples (m, n) where
901
+ m <= 0 and n >= 0. The system multiplication works such that if n = -m,
902
+ then the seed system's origin will be at the center of the new system.
903
+ If only one integer is given, then it is assigned to m or n depending on
904
+ its sign, and the other value is taken to be 0.
905
+
906
+ Parameters
907
+ ----------
908
+ a_size : int or tuple of int
909
+ Single int or two integers specifying replication along the avect
910
+ direction.
911
+ b_size -- int or tuple of int
912
+ Single int or two integers specifying replication along the bvect
913
+ direction.
914
+ c_size -- int or tuple of int
915
+ Single int or two integers specifying replication along the cvect
916
+ direction.
917
+
918
+ Returns
919
+ -------
920
+ atomman.System
921
+ A new system created by replicating the given seed system according to
922
+ the \\*_size parameters.
923
+
924
+ """
925
+ # Extract parameters
926
+ sizes = [a_size, b_size, c_size]
927
+ mults = np.array([0, 0, 0], dtype=int)
928
+ vects = self.box.vects
929
+ origin = self.box.origin
930
+ spos = self.atoms_prop('pos', scale=True)
931
+
932
+ # Check the *_size values
933
+ for i in range(3):
934
+
935
+ # Change single int to tuple of two int
936
+ if isinstance(sizes[i], (int, np.integer)):
937
+ if sizes[i] > 0:
938
+ sizes[i] = (0, sizes[i])
939
+ elif sizes[i] < 0:
940
+ sizes[i] = (sizes[i], 0)
941
+
942
+ elif isinstance(sizes[i], tuple):
943
+ try:
944
+ assert len(sizes[i]) == 2, str(len(sizes[i]))
945
+ assert isinstance(sizes[i][0], (int, np.integer)), str(sizes[i][0])
946
+ assert sizes[i][0] <= 0, str(sizes[i][0])
947
+ assert isinstance(sizes[i][1], (int, np.integer)), str(sizes[i][1])
948
+ assert sizes[i][1] >= 0, str(sizes[i][1])
949
+ except:
950
+ raise TypeError('Invalid system multipliers')
951
+ else:
952
+ raise TypeError('Invalid system multipliers')
953
+
954
+ # Calculate full multipliers
955
+ mults[i] = sizes[i][1] - sizes[i][0]
956
+ if mults[i] == 0:
957
+ raise ValueError('Cannot multiply system dimension by zero')
958
+
959
+ # Scale box and first set of positions accordingly
960
+ spos[:,i] /= mults[i]
961
+ origin += vects[i] * sizes[i][0]
962
+ vects[i] *= mults[i]
963
+
964
+ # Initialize new Box and Atoms
965
+ box = Box(vects=vects, origin=origin)
966
+ natoms = self.natoms * mults[0] * mults[1] * mults[2]
967
+ atoms = Atoms(natoms=natoms)
968
+
969
+ # Copy over all property values (except pos) using numpy broadcasting
970
+ for key in self.atoms_prop():
971
+ if key == 'pos':
972
+ continue
973
+
974
+ # Get old array
975
+ old = self.atoms.view[key]
976
+
977
+ # Create new array and broadcast old to it
978
+ new = np.empty((mults[0] * mults[1] * mults[2],) + old.shape, dtype = old.dtype)
979
+ new[:] = old
980
+
981
+ # Reshape new and save to atoms
982
+ new_shape = new.shape
983
+ new_shape = (new_shape[0] * new_shape[1],) + new_shape[2:] # pylint: disable=unsubscriptable-object
984
+ atoms.view[key] = np.array(new.reshape(new_shape))
985
+
986
+ # Expand spos using broadcasting
987
+ new_spos = np.empty((mults[0] * mults[1] * mults[2],) + spos.shape)
988
+ new_spos[:] = spos
989
+
990
+ # Reshape spos
991
+ new_shape = new_spos.shape
992
+ new_shape = (new_shape[0]*new_shape[1],) + new_shape[2:] # pylint: disable=unsubscriptable-object
993
+ new_spos = new_spos.reshape(new_shape)
994
+
995
+ # Use broadcasting to create arrays to add to spos
996
+ test = np.empty(mults[0] * self.natoms)
997
+ test.shape = (self.natoms, mults[0])
998
+ test[:] = np.arange(mults[0])
999
+ x = test.T.flatten()
1000
+
1001
+ test = np.empty(mults[1] * len(x))
1002
+ test.shape = (len(x), mults[1])
1003
+ test[:] = np.arange(mults[1])
1004
+ y = test.T.flatten()
1005
+ test.shape = (mults[1], len(x))
1006
+ test[:] = x
1007
+ x = test.flatten()
1008
+
1009
+ test = np.empty(mults[2] * len(x))
1010
+ test.shape = (len(x), mults[2])
1011
+ test[:] = np.arange(mults[2])
1012
+ z = test.T.flatten()
1013
+ test.shape = (mults[2], len(x))
1014
+ test[:] = x
1015
+ x = test.flatten()
1016
+ test[:] = y
1017
+ y = test.flatten()
1018
+
1019
+ # xyz is displacement values to add to spos
1020
+ xyz = (np.hstack((x[:, np.newaxis], y[:, np.newaxis], z[:, np.newaxis]))
1021
+ * np.array([1 / mults[0], 1 / mults[1], 1 / mults[2]]))
1022
+
1023
+ # Save pos values, return new System
1024
+ atoms.view['pos'] = new_spos + xyz
1025
+
1026
+ return System(box=box, atoms=atoms, scale=True, symbols=self.symbols)
1027
+
1028
+ def rotate(self,
1029
+ uvws: npt.ArrayLike,
1030
+ tol: Union[float, list, None] = None,
1031
+ return_transform: bool = False) -> System:
1032
+ """
1033
+ Transforms a System representing a periodic crystal cell from a standard
1034
+ orientation to a specified orientation. Note: if hexagonal indices are
1035
+ given, the vectors will be reduced to the smallest uvw integer
1036
+ representation.
1037
+
1038
+ Parameters
1039
+ ----------
1040
+ uvws : array-like object
1041
+ A (3, 3) array of the Miller crystal vectors or a (3, 4) array of
1042
+ Miller-Bravais hexagonal crystal vectors to use in transforming the
1043
+ system. Values must be integers.
1044
+ tol : list or float, optional
1045
+ Tolerance parameter used in determining which atoms are inside the
1046
+ box. Multiple values can be given as the identification may
1047
+ occasionally fail for a given ucell and tol. Default behavior will
1048
+ try tol values ranging from 1e-4 to 1e-8.
1049
+ return_transform : bool, optional
1050
+ Indicates if the transformation matrix associated with the
1051
+ rotation is returned. Default value is False.
1052
+
1053
+ Returns
1054
+ -------
1055
+ atomman.System
1056
+ A new fully periodic system rotated and transformed according to the
1057
+ uvws crystal vectors.
1058
+ transform : np.ndarray
1059
+ The transformation matrix associated with the rotation.
1060
+ Returned if return_transform is True.
1061
+ """
1062
+
1063
+ if tol is None:
1064
+ tol = [1e-4, 1e-5, 1e-6, 1e-7]
1065
+ else:
1066
+ tol = aslist(tol)
1067
+
1068
+ uvws = np.asarray(uvws)
1069
+
1070
+ # Convert uvws from Miller-Bravais to Miller indices if needed
1071
+ if uvws.shape == (3, 4):
1072
+ if self.box.ishexagonal():
1073
+ uvws = miller.vector4to3(uvws)
1074
+ else:
1075
+ raise ValueError('hexagonal indices only work on hexagonal systems')
1076
+
1077
+ # Check uvws shape and values
1078
+ if uvws.shape != (3, 3):
1079
+ raise ValueError('Invalid uvws crystal indices shape')
1080
+
1081
+ int_uvws = np.asarray(np.rint(uvws), dtype='int64')
1082
+ if np.allclose(uvws, int_uvws):
1083
+ uvws = int_uvws
1084
+ else:
1085
+ raise ValueError('Rotation uvws must be integer values')
1086
+
1087
+ # No rotation shortcut
1088
+ if np.all(uvws == np.eye(3, dtype='int64')):
1089
+ newsystem = deepcopy(self)
1090
+
1091
+ else:
1092
+ # Get natoms and volume of system
1093
+ natoms = self.natoms
1094
+ volume = self.box.volume
1095
+
1096
+ # Convert uvws to Cartesian units and compute new volume and natoms
1097
+ newvects = miller.vector_crystal_to_cartesian(uvws, box=self.box)
1098
+ newvolume = np.abs(newvects[0].dot(np.cross(newvects[1], newvects[2])))
1099
+ newnatoms = int(round(newvolume / volume) * natoms)
1100
+
1101
+ # Check new values
1102
+ if newnatoms == 0:
1103
+ raise ValueError('New box has no atoms/volume: vectors are parallel or planar')
1104
+
1105
+ # Identify box corners of new system wrt uvws
1106
+ corners = np.empty((8,3), dtype='int64')
1107
+ corners[0] = np.zeros(3)
1108
+ corners[1] = uvws[0]
1109
+ corners[2] = uvws[1]
1110
+ corners[3] = uvws[2]
1111
+ corners[4] = uvws[0] + uvws[1]
1112
+ corners[5] = uvws[0] + uvws[2]
1113
+ corners[6] = uvws[1] + uvws[2]
1114
+ corners[7] = uvws[0] + uvws[1] + uvws[2]
1115
+
1116
+ # Create a supercell of system that contains all box corners
1117
+ a_mults = (corners[:,0].min()-1, corners[:,0].max()+1)
1118
+ b_mults = (corners[:,1].min()-1, corners[:,1].max()+1)
1119
+ c_mults = (corners[:,2].min()-1, corners[:,2].max()+1)
1120
+ system2 = self.supersize(a_mults, b_mults, c_mults)
1121
+
1122
+ # Change system.box.vects to newvects
1123
+ system2.box_set(vects=newvects, scale=False)
1124
+
1125
+ search_success = False
1126
+ for atol in tol:
1127
+
1128
+ spos = system2.atoms_prop('pos', scale=True)
1129
+
1130
+ # Round atom positions near box boundaries to the boundaries
1131
+ spos[np.isclose(spos, 0.0, atol=atol)] = 0.0
1132
+ spos[np.isclose(spos, 1.0, atol=atol)] = 1.0
1133
+
1134
+ # Identify all atoms whose positions are 0 <= x < 1
1135
+ aindex = np.where(((spos[:, 0] >= 0.0) & (spos[:, 0] < 1.0)
1136
+ & (spos[:, 1] >= 0.0) & (spos[:, 1] < 1.0)
1137
+ & (spos[:, 2] >= 0.0) & (spos[:, 2] < 1.0)))
1138
+
1139
+ # Check if number of atoms identified matches the expected number
1140
+ if len(aindex[0]) == newnatoms:
1141
+ search_success = True
1142
+ break
1143
+
1144
+ if not search_success:
1145
+ raise ValueError(f'Filtering failed: {newnatoms} atoms expected, {len(aindex[0])} found')
1146
+
1147
+ # Make newsystem by cutting out all atoms in system2 outside boundaries
1148
+ newsystem = System(atoms=system2.atoms[aindex], box=system2.box, symbols=self.symbols)
1149
+
1150
+ # Return normalized system
1151
+ return newsystem.normalize(return_transform=return_transform)
1152
+
1153
+ def normalize(self,
1154
+ style: str = 'lammps',
1155
+ return_transform: bool = False
1156
+ ) -> Union[System, Tuple[System, np.ndarray]]:
1157
+ """
1158
+ Normalizes a system's box vectors and atom positions to be compatible
1159
+ with simulation codes.
1160
+
1161
+ Parameters
1162
+ ----------
1163
+ style : str, optional
1164
+ Indicates the normalization style to use. Default (and only
1165
+ current option) is 'lammps'.
1166
+ return_transform : bool, optional
1167
+ Indicates if the transformation matrix associated with the
1168
+ normalization is returned. Default value is False.
1169
+ Returns
1170
+ -------
1171
+ newsystem : atomman.System
1172
+ A new system that has been normalized.
1173
+ transform : np.ndarray
1174
+ The transformation matrix associated with the normalization.
1175
+ Returned if return_transform is True.
1176
+ """
1177
+ if style == 'lammps':
1178
+ return lmp_normalize(self, return_transform=return_transform)
1179
+ else:
1180
+ raise ValueError("Unknown style (only 'lammps' is currently supported)")
1181
+
1182
+ def dump(self, style: str, **kwargs) -> Optional[Any]:
1183
+ """
1184
+ Convert a System to another format.
1185
+
1186
+ Parameters
1187
+ ----------
1188
+ style : str
1189
+ Indicates the format of the content to dump the atomman.System as.
1190
+ kwargs
1191
+ Any extra keyword arguments to pass to the underlying dump methods.
1192
+
1193
+ Returns
1194
+ -------
1195
+ str, object or tuple
1196
+ Any content returned by the underlying dump methods.
1197
+ """
1198
+ return dump(style, self, **kwargs)
1199
+
1200
+ def model(self,
1201
+ box_unit: Optional[str] = None,
1202
+ prop_name: Optional[list] = None,
1203
+ unit: Optional[list] = None,
1204
+ prop_unit: Optional[dict] = None) -> DM:
1205
+ """
1206
+ Generates a data model for the System object.
1207
+
1208
+ Parameters
1209
+ ----------
1210
+ box_unit : str, optional
1211
+ Length unit to use for the box. Default value is 'angstrom'.
1212
+ prop_name : list, optional
1213
+ The Atoms properties to include. If neither prop_name nor prop_unit
1214
+ are given, all system properties will be included.
1215
+ unit : list, optional
1216
+ Lists the units for each prop_name as stored in the table. For a
1217
+ value of None, no conversion will be performed for that property. For
1218
+ a value of 'scaled', the corresponding table values will be taken in
1219
+ box-scaled units. If neither unit nor prop_units given, pos will be
1220
+ given in Angstroms and all other values will not be converted.
1221
+ prop_unit : dict, optional
1222
+ dictionary where the keys are the property keys to include, and
1223
+ the values are units to use. If neither unit nor prop_units given,
1224
+ pos will be given in Angstroms and all other values will not be
1225
+ converted.
1226
+
1227
+ Returns
1228
+ -------
1229
+ DataModelDict.DataModelDict
1230
+ A JSON/XML data model for the current System object.
1231
+ """
1232
+
1233
+ # Initialize DataModelDict
1234
+ model = DM()
1235
+ model['atomic-system'] = DM()
1236
+
1237
+ # Add box
1238
+ model['atomic-system']['box'] = self.box.model(length_unit=box_unit)['box']
1239
+
1240
+ # Add pbc
1241
+ model['atomic-system']['periodic-boundary-condition'] = self.pbc.tolist()
1242
+
1243
+ # Add symbols
1244
+ for symbol in self.symbols:
1245
+ model['atomic-system'].append('atom-type-symbol', symbol)
1246
+
1247
+ # Add masses
1248
+ addmasses = False
1249
+ for mass in self.masses:
1250
+ if mass is not None:
1251
+ addmasses = True
1252
+ break
1253
+ if addmasses:
1254
+ for mass in self.masses:
1255
+ model['atomic-system'].append('atom-type-mass', mass)
1256
+
1257
+ # Add atoms
1258
+ model['atomic-system']['atoms'] = amodel = self.atoms.model(prop_name=prop_name,
1259
+ unit=unit,
1260
+ prop_unit=prop_unit)['atoms']
1261
+
1262
+ # Scale properties if needed
1263
+ for prop in amodel.aslist('property'):
1264
+ if prop['data'].get('unit', None) == 'scaled':
1265
+ prop['data'] = uc.model(self.box.position_cartesian_to_relative(uc.value_unit(prop['data'])), units='scaled')
1266
+
1267
+ return model
atomman/source/atomman/core/__init__.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ from .dvect import dvect
3
+ from .dmag import dmag
4
+ from .nlist import nlist
5
+ from .NeighborList import NeighborList
6
+ from .Atoms import Atoms
7
+ from .Box import Box
8
+ from .ElasticConstants import ElasticConstants
9
+ from .ElasticConstants2 import ElasticConstants2
10
+ from .System import System
11
+ from .displacement import displacement
12
+
13
+ __all__ = ['displacement', 'dvect', 'dmag', 'nlist', 'Atoms', 'Box',
14
+ 'ElasticConstants', 'ElasticConstants2', 'NeighborList', 'System']
atomman/source/atomman/core/displacement.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+
3
+ # http://www.numpy.org/
4
+ import numpy as np
5
+
6
+ # atomman imports
7
+ from . import dvect
8
+
9
+ def displacement(system_0, system_1, box_reference: str = 'final') -> np.ndarray:
10
+ """
11
+ Compute the displacement vectors between all matching atoms for two systems.
12
+
13
+ Parameters
14
+ ----------
15
+ system_0 : atomman.System
16
+ The initial system to calculate displacements from.
17
+ system_1 : atomman.System
18
+ The final system to calculate displacements to.
19
+ box_reference : str or None
20
+ Specifies which system's boundary conditions to use. 'initial' uses
21
+ system_0's box and pbc. 'final' uses system_1's box and pbc (Default)
22
+ None computes the straight difference between the positions without
23
+ accounting for periodic boundaries.
24
+
25
+ Returns
26
+ -------
27
+ numpy.ndarray
28
+ The displacement vectors for all atoms.
29
+
30
+ Raises
31
+ ------
32
+ ValueError
33
+ If the systems have different numbers of atoms or for invalid
34
+ box_reference values.
35
+ """
36
+ if system_0.natoms != system_1.natoms:
37
+ raise ValueError('systems have different number of atoms')
38
+
39
+ if box_reference == 'final':
40
+ disp = dvect(system_0.atoms.pos, system_1.atoms.pos, system_1.box, system_1.pbc)
41
+ elif box_reference == 'initial':
42
+ disp = dvect(system_0.atoms.pos, system_1.atoms.pos, system_0.box, system_0.pbc)
43
+ elif box_reference is None:
44
+ disp = system_1.atoms.pos - system_0.atoms.pos
45
+ else:
46
+ raise ValueError("box_reference must be 'final', 'initial', or None")
47
+
48
+ return disp
49
+
atomman/source/atomman/core/dmag.pxd ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # coding: utf-8
2
+ # cython: language_level=3
3
+ cdef dmag2_c(const double[:,:], const double[:,:], const double[:,:], const bint, const bint, const bint)
atomman/source/atomman/core/dmag.pyx ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # cython: language_level=3
3
+ # Standard Python libraries
4
+ from copy import deepcopy
5
+
6
+ # http://cython.org/
7
+ import cython
8
+
9
+ # http://www.numpy.org/
10
+ import numpy as np
11
+
12
+ @cython.boundscheck(False)
13
+ @cython.wraparound(False)
14
+ def dmag(pos_0, pos_1, box, pbc):
15
+ """
16
+ Computes the shortest distance between pos_0 and pos_1 using box
17
+ dimensions and accounting for periodic boundaries.
18
+
19
+ Parameters
20
+ ----------
21
+ pos_0 : numpy.ndarray, list, or tuple
22
+ Absolute Cartesian vector position(s) to use as reference point(s).
23
+ pos_1 : numpy.ndarray, list, or tuple
24
+ Absolute Cartesian vector position(s) to find relative to pos_0.
25
+ box : atomman.Box
26
+ Defines the system/box dimensions
27
+ pbc : list, tuple, or numpy.ndarray of bool.
28
+ Three Boolean values indicating which of the three box vectors are
29
+ periodic (True means periodic).
30
+
31
+ Returns
32
+ -------
33
+ numpy.ndarray
34
+ The shortest vector magnitude from each pos_0 to pos_1 positions.
35
+ """
36
+
37
+ # Convert pos_0 to numpy array with proper dimensions
38
+ pos_0 = np.asarray(pos_0, dtype=np.float64)
39
+ if pos_0.ndim == 0:
40
+ raise TypeError('Invalid pos_0')
41
+ if pos_0.ndim == 1:
42
+ pos_0 = pos_0[np.newaxis, :]
43
+
44
+ # Convert pos_1 to numpy array with proper dimensions
45
+ pos_1 = np.asarray(pos_1, dtype=np.float64)
46
+ if pos_1.ndim == 0:
47
+ raise TypeError('Invalid pos_1')
48
+ if pos_1.ndim == 1:
49
+ pos_1 = pos_1[np.newaxis, :]
50
+
51
+ # Broadcast to compatible lengths
52
+ if len(pos_0) == 1:
53
+ pos_0 = np.broadcast_to(pos_0, pos_1.shape)
54
+ elif len(pos_1) == 1:
55
+ pos_1 = np.broadcast_to(pos_1, pos_0.shape)
56
+ elif len(pos_0) != len(pos_1):
57
+ raise ValueError('Incompatible pos lengths')
58
+
59
+ # Extract box vectors
60
+ bvects = box.vects
61
+
62
+ # Call the cython function
63
+ return dmag2_c(pos_0, pos_1, bvects, pbc[0], pbc[1], pbc[2])**0.5
64
+
65
+ @cython.boundscheck(False)
66
+ @cython.wraparound(False)
67
+ cdef dmag2_c(const double[:,:] pos_0,
68
+ const double[:,:] pos_1,
69
+ const double[:,:] bvects,
70
+ const bint pbc_x,
71
+ const bint pbc_y,
72
+ const bint pbc_z):
73
+ """
74
+ Computes the shortest distance between pos_0 and pos_1 using box
75
+ dimensions and accounting for periodic boundaries.
76
+
77
+ Parameters
78
+ ----------
79
+ pos_0 : cython.memoryview
80
+ Absolute Cartesian vector position(s) to use as reference point(s).
81
+ pos_1 : cython.memoryview
82
+ Absolute Cartesian vector position(s) to find relative to pos_0.
83
+ bvects : cython.memoryview
84
+ 3x3 array defining the system/box dimensions.
85
+ pbc_x : bint
86
+ Flag indicating to make x periodic.
87
+ pbc_y : bint
88
+ Flag indicating to make y periodic.
89
+ pbc_z : bint
90
+ Flag indicating to make z periodic.
91
+
92
+ Returns
93
+ -------
94
+ cython.memoryview
95
+ The shortest vector magnitude squared from each pos_0 to pos_1 positions.
96
+ """
97
+
98
+ # Define parameters
99
+ cdef Py_ssize_t ni = pos_0.shape[0]
100
+ cdef Py_ssize_t nj = 3
101
+ cdef Py_ssize_t i, j, x, y, z, xl, xh, yl, yh, zl, zh
102
+ cdef double mag2_test
103
+ cdef double[:] d = np.empty(3, dtype=np.float64)
104
+
105
+ # Define output array and its view
106
+ mag2_d = np.empty(ni, dtype=np.float64)
107
+ cdef double [:] mag2_dv = mag2_d
108
+
109
+ # Create iterators based on pbc
110
+ if pbc_x:
111
+ xl, xh = -1, 2
112
+ else:
113
+ xl, xh = 0, 1
114
+ if pbc_y:
115
+ yl, yh = -1, 2
116
+ else:
117
+ yl, yh = 0, 1
118
+ if pbc_z:
119
+ zl, zh = -1, 2
120
+ else:
121
+ zl, zh = 0, 1
122
+
123
+ # Loop over all pos
124
+ for i in range(ni):
125
+
126
+ # Compute pos_1 - pos_0
127
+ for j in range(nj):
128
+ d[j] = pos_1[i,j] - pos_0[i,j]
129
+ mag2_dv[i] = d[0] * d[0] + d[1] * d[1] + d[2] * d[2]
130
+
131
+ # Loop over all periodic boundary conditions
132
+ for x in range(xl, xh):
133
+ for y in range(yl, yh):
134
+ for z in range(zl, zh):
135
+ if x == 0 and y == 0 and z == 0:
136
+ continue
137
+
138
+ # Compute pos_1 - pos_0 + boundary image shifts
139
+ for j in range(nj):
140
+ d[j] = (pos_1[i,j] - pos_0[i,j]
141
+ + x * bvects[0,j]
142
+ + y * bvects[1,j]
143
+ + z * bvects[2,j])
144
+
145
+ # Replace d if new vector is smaller
146
+ mag2_test = d[0] * d[0] + d[1] * d[1] + d[2] * d[2]
147
+ if mag2_test < mag2_dv[i]:
148
+ mag2_dv[i] = mag2_test
149
+
150
+ return mag2_d
atomman/source/atomman/core/dvect.pxd ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # coding: utf-8
2
+ # cython: language_level=3
3
+ cdef dvect_c(const double[:,:], const double[:,:], const double[:,:], const bint, const bint, const bint)
atomman/source/atomman/core/dvect.pyx ADDED
@@ -0,0 +1,153 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # cython: language_level=3
3
+ # Standard Python libraries
4
+ from __future__ import (absolute_import, print_function,
5
+ division, unicode_literals)
6
+ from copy import deepcopy
7
+
8
+ # http://cython.org/
9
+ import cython
10
+
11
+ # http://www.numpy.org/
12
+ import numpy as np
13
+
14
+ @cython.boundscheck(False)
15
+ @cython.wraparound(False)
16
+ def dvect(pos_0, pos_1, box, pbc):
17
+ """
18
+ Computes the shortest vector between pos_0 and pos_1 using box
19
+ dimensions and accounting for periodic boundaries.
20
+
21
+ Parameters
22
+ ----------
23
+ pos_0 : numpy.ndarray, list, or tuple
24
+ Absolute Cartesian vector position(s) to use as reference point(s).
25
+ pos_1 : numpy.ndarray, list, or tuple
26
+ Absolute Cartesian vector position(s) to find relative to pos_0.
27
+ box : atomman.Box
28
+ Defines the system/box dimensions
29
+ pbc : list, tuple, or numpy.ndarray of bool.
30
+ Three Boolean values indicating which of the three box vectors are
31
+ periodic (True means periodic).
32
+
33
+ Returns
34
+ -------
35
+ numpy.ndarray
36
+ The shortest vectors from each pos_0 to pos_1 positions.
37
+ """
38
+
39
+ # Convert pos_0 to numpy array with proper dimensions
40
+ pos_0 = np.asarray(pos_0, dtype=np.float64)
41
+ if pos_0.ndim == 0:
42
+ raise TypeError('Invalid pos_0')
43
+ if pos_0.ndim == 1:
44
+ pos_0 = pos_0[np.newaxis, :]
45
+
46
+ # Convert pos_1 to numpy array with proper dimensions
47
+ pos_1 = np.asarray(pos_1, dtype=np.float64)
48
+ if pos_1.ndim == 0:
49
+ raise TypeError('Invalid pos_1')
50
+ if pos_1.ndim == 1:
51
+ pos_1 = pos_1[np.newaxis, :]
52
+
53
+ # Broadcast to compatible lengths
54
+ if len(pos_0) == 1:
55
+ pos_0 = np.broadcast_to(pos_0, pos_1.shape)
56
+ elif len(pos_1) == 1:
57
+ pos_1 = np.broadcast_to(pos_1, pos_0.shape)
58
+ elif len(pos_0) != len(pos_1):
59
+ raise ValueError('Incompatible pos lengths')
60
+
61
+ # Extract box vectors
62
+ bvects = box.vects
63
+
64
+ # Call the cython function
65
+ return dvect_c(pos_0, pos_1, bvects, pbc[0], pbc[1], pbc[2])
66
+
67
+ @cython.boundscheck(False)
68
+ @cython.wraparound(False)
69
+ cdef dvect_c(const double[:,:] pos_0,
70
+ const double[:,:] pos_1,
71
+ const double[:,:] bvects,
72
+ const bint pbc_x,
73
+ const bint pbc_y,
74
+ const bint pbc_z):
75
+ """
76
+ Computes the shortest distance between pos_0 and pos_1 using box
77
+ dimensions and accounting for periodic boundaries.
78
+
79
+ Parameters
80
+ ----------
81
+ pos_0 : cython.memoryview
82
+ Absolute Cartesian vector position(s) to use as reference point(s).
83
+ pos_1 : cython.memoryview
84
+ Absolute Cartesian vector position(s) to find relative to pos_0.
85
+ bvects : cython.memoryview
86
+ 3x3 array defining the system/box dimensions.
87
+ pbc_x : bint
88
+ Flag indicating to make x periodic.
89
+ pbc_y : bint
90
+ Flag indicating to make y periodic.
91
+ pbc_z : bint
92
+ Flag indicating to make z periodic.
93
+
94
+ Returns
95
+ -------
96
+ cython.memoryview
97
+ The shortest vectors from each pos_0 to pos_1 positions.
98
+ """
99
+
100
+ # Define parameters
101
+ cdef Py_ssize_t ni = pos_0.shape[0]
102
+ cdef int nj = 3
103
+ cdef int i, j, x, y, z, xl, xh, yl, yh, zl, zh
104
+ cdef double[:] test = np.empty(3)
105
+ cdef double mag_test, mag_d
106
+
107
+ # Define output array and its view
108
+ d = np.empty_like(pos_0, np.float64)
109
+ cdef double[:,:] dv = d
110
+
111
+ # Create iterators based on pbc
112
+ if pbc_x:
113
+ xl, xh = -1, 2
114
+ else:
115
+ xl, xh = 0, 1
116
+ if pbc_y:
117
+ yl, yh = -1, 2
118
+ else:
119
+ yl, yh = 0, 1
120
+ if pbc_z:
121
+ zl, zh = -1, 2
122
+ else:
123
+ zl, zh = 0, 1
124
+
125
+ # Loop over all pos
126
+ for i in range(ni):
127
+
128
+ # Compute pos_1 - pos_0
129
+ for j in range(nj):
130
+ dv[i,j] = pos_1[i,j] - pos_0[i,j]
131
+
132
+ # Loop over all periodic boundary conditions
133
+ for x in range(xl, xh):
134
+ for y in range(yl, yh):
135
+ for z in range(zl, zh):
136
+ if x == 0 and y == 0 and z == 0:
137
+ continue
138
+
139
+ # Compute pos_1 - pos_0 + boundary image shifts
140
+ for j in range(nj):
141
+ test[j] = (pos_1[i,j] - pos_0[i,j]
142
+ + x * bvects[0,j]
143
+ + y * bvects[1,j]
144
+ + z * bvects[2,j])
145
+
146
+ # Replace d if new vector is smaller
147
+ mag_test = test[0] * test[0] + test[1] * test[1] + test[2] * test[2]
148
+ mag_d = dv[i,0] * dv[i,0] + dv[i,1] * dv[i,1] + dv[i,2] * dv[i,2]
149
+ if mag_test < mag_d:
150
+ for j in range(nj):
151
+ dv[i,j] = test[j]
152
+
153
+ return d
atomman/source/atomman/core/nlist.pyx ADDED
@@ -0,0 +1,334 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # cython: language_level=3
3
+
4
+ # http://cython.org/
5
+ import cython
6
+
7
+ # http://www.numpy.org/
8
+ import numpy as np
9
+
10
+ # atomman imports
11
+ from .dmag cimport dmag2_c
12
+
13
+ @cython.boundscheck(False)
14
+ @cython.wraparound(False)
15
+ def nlist(system, double cutoff, Py_ssize_t initialsize=20, Py_ssize_t deltasize=10):
16
+ """
17
+ Calculates a neighbor list for all atoms in a System taking periodic
18
+ boundaries into account.
19
+
20
+ Parameters
21
+ ----------
22
+ system : atomman.System
23
+ The system to calculate the neighbor list for.
24
+ cutoff : float
25
+ Radial cutoff distance for identifying neighbors.
26
+ initialsize : int, optional
27
+ The number of neighbor positions to initially assign to each atom.
28
+ Default value is 20.
29
+ deltasize : int, optional
30
+ Specifies the number of extra neighbor positions to allow each atom
31
+ when the number of neighbors exceeds the underlying array size.
32
+ Default value is 10.
33
+
34
+ Returns
35
+ -------
36
+ numpy.ndarray of int
37
+ Array listing number of neighbors and neighbor ids for each atom in
38
+ System. First term in each row is the atom's coordination number, c.
39
+ The next c values are the atom's neighbor ids.
40
+ """
41
+
42
+ # Define variables based on input parameters
43
+ pos = system.atoms.pos
44
+ cdef const double[:,:] posv = pos
45
+ cdef const double[:,:] vects = system.box.vects
46
+ cdef const double[:] origin = system.box.origin
47
+ cdef bint pbc_a = system.pbc[0]
48
+ cdef bint pbc_b = system.pbc[1]
49
+ cdef bint pbc_c = system.pbc[2]
50
+ cdef Py_ssize_t maxneighbors = initialsize
51
+ cdef Py_ssize_t maxatomsperbin = 40
52
+ cdef Py_ssize_t natoms = posv.shape[0]
53
+ cdef double cutoff2 = cutoff*cutoff
54
+
55
+ # Define basic iteration index variables
56
+ cdef Py_ssize_t i, j, k, l
57
+
58
+ # Define superbox identification parameters
59
+ cdef Py_ssize_t x, y, z
60
+ cdef double corner
61
+ cdef double[:] supermin=np.empty(3)
62
+ cdef double[:] supermax=np.empty(3)
63
+
64
+ # Define bins
65
+ cdef double binsize
66
+ cdef double[:] xbins, ybins, zbins
67
+ cdef Py_ssize_t numxbins, numybins, numzbins
68
+
69
+ # Define bin indexers and associated atom indexes
70
+ #cdef long long [:] xindex, yindex, zindex
71
+ #cdef long long [:,:] xyzindex
72
+ cdef long long [:] atomindex
73
+ cdef long long [:,:] newxyzindex
74
+ cdef long long [:] newatomindex
75
+
76
+ # Define ghost atom info
77
+ cdef Py_ssize_t xl, xh, yl, yh, zl, zh
78
+ cdef double[:,:] ghostpos = np.empty((0, 3))
79
+ cdef double[:,:] newghostpos
80
+ newpos = np.empty(pos.shape)
81
+ cdef double[:,:] newposv = newpos
82
+ cdef long long[:] ghostindex = np.empty(0, dtype=np.int64)
83
+ cdef long long[:] newindex = np.empty(posv.shape[0], dtype=np.int64)
84
+ cdef long long[:] newghostindex
85
+ cdef long long [:,:] xyzghostindex
86
+
87
+ # Define final bins
88
+ cdef Py_ssize_t maxc, c, n
89
+ cdef Py_ssize_t dc, dx, dy, dz
90
+ cdef long long[:, :, :, :] xyzbins, newbins
91
+
92
+ # Define lists of atoms to compare
93
+ cdef Py_ssize_t uindex, vindex, u, v, w
94
+ cdef long long [:] shortlist, longlist, superlonglist
95
+ cdef bint end
96
+
97
+ # Define positions and distances between them
98
+ cdef double[:,:] upos, vpos
99
+ cdef double[:] dmag2
100
+
101
+ # Define neighbors list and assignment terms
102
+ cdef Py_ssize_t uj, vj
103
+ cdef long long[:, :] neighbors = np.empty((natoms, maxneighbors+1), dtype=np.int64)
104
+ for i in range(natoms):
105
+ neighbors[i, 0] = 0
106
+ cdef long long[:, :] newneighbors
107
+ cdef bint isnew
108
+
109
+ # Determine orthogonal superbox that fully encompasses the system
110
+ for j in range(3):
111
+ supermin[j] = origin[j]
112
+ supermax[j] = origin[j]
113
+ for z in range(0, 2):
114
+ for y in range(0, 2):
115
+ for x in range(0, 2):
116
+ for j in range(3):
117
+ corner = origin[j] + x * vects[0, j] + y * vects[1, j] + z * vects[2, j]
118
+ if corner < supermin[j]:
119
+ supermin[j] = corner
120
+ if corner > supermax[j]:
121
+ supermax[j] = corner
122
+ for j in range(3):
123
+ supermin[j] -= 1.01 * cutoff
124
+ supermax[j] += 1.01 * cutoff
125
+
126
+ # Construct bins
127
+ binsize = cutoff
128
+ xbins = np.arange(supermin[0], supermax[0] + binsize, binsize)
129
+ ybins = np.arange(supermin[1], supermax[1] + binsize, binsize)
130
+ zbins = np.arange(supermin[2], supermax[2] + binsize, binsize)
131
+ numxbins = len(xbins)
132
+ numybins = len(ybins)
133
+ numzbins = len(zbins)
134
+
135
+ # Build xyz box index for each atom
136
+ xindex = np.digitize(pos[:, 0], xbins) - 1
137
+ yindex = np.digitize(pos[:, 1], ybins) - 1
138
+ zindex = np.digitize(pos[:, 2], zbins) - 1
139
+ xyzindex = np.hstack((xindex[:, np.newaxis], yindex[:, np.newaxis], zindex[:, np.newaxis]))
140
+
141
+ # Relate atom's id to xyz_index
142
+ atomindex = np.arange(natoms, dtype=np.int64)
143
+
144
+ # Identify all bins with real atoms
145
+ realbins = unique_rows2(xyzindex)
146
+
147
+ # Create iterators based on pbc
148
+ if pbc_a:
149
+ xl, xh = -1, 2
150
+ else:
151
+ xl, xh = 0, 1
152
+ if pbc_b:
153
+ yl, yh = -1, 2
154
+ else:
155
+ yl, yh = 0, 1
156
+ if pbc_c:
157
+ zl, zh = -1, 2
158
+ else:
159
+ zl, zh = 0, 1
160
+
161
+ # Construct list of ghost atoms in the superbox
162
+ for x in range(xl, xh):
163
+ for y in range(yl, yh):
164
+ for z in range(zl, zh):
165
+ if x == 0 and y == 0 and z == 0:
166
+ pass
167
+ else:
168
+ k=0
169
+ for i in range(posv.shape[0]):
170
+ for j in range(3):
171
+ newposv[i, j] = x * vects[0, j] + y * vects[1, j] + z * vects[2, j] + posv[i, j]
172
+
173
+ if ( newposv[i, 0] > supermin[0] and newposv[i, 0] < supermax[0]
174
+ and newposv[i, 1] > supermin[1] and newposv[i, 1] < supermax[1]
175
+ and newposv[i, 2] > supermin[2] and newposv[i, 2] < supermax[2]):
176
+
177
+ newindex[k] = i
178
+ k += 1
179
+
180
+ ghostpos = np.vstack((ghostpos, newpos[newindex[:k]]))
181
+ ghostindex = np.hstack((ghostindex, newindex[:k]))
182
+
183
+ # Append xyzindex and atomindex lists with ghost atoms
184
+ if len(ghostpos) > 0:
185
+ xindex = np.digitize(ghostpos[:, 0], xbins) - 1
186
+ yindex = np.digitize(ghostpos[:, 1], ybins) - 1
187
+ zindex = np.digitize(ghostpos[:, 2], zbins) - 1
188
+ xyzghostindex = np.hstack((xindex[:, np.newaxis],
189
+ yindex[:, np.newaxis],
190
+ zindex[:, np.newaxis]))
191
+ xyzindex = np.vstack((xyzindex, xyzghostindex))
192
+ atomindex = np.hstack((atomindex, ghostindex))
193
+
194
+ # Assign atoms and ghost atoms to xyz bins
195
+ maxc = 0
196
+ xyzbins = np.zeros((numxbins, numybins, numzbins, maxatomsperbin + 1), dtype=np.int64)
197
+ for n in range(atomindex.shape[0]):
198
+ x, y, z = xyzindex[n]
199
+ c = xyzbins[x, y, z, 0] + 1
200
+
201
+ # Increase size if needed
202
+ if c == maxatomsperbin:
203
+ newbins = np.zeros((numxbins, numybins, numzbins, maxatomsperbin + 11), dtype=np.int64)
204
+ for i in range(xyzbins.shape[0]):
205
+ for j in range(xyzbins.shape[1]):
206
+ for k in range(xyzbins.shape[2]):
207
+ for l in range(maxatomsperbin + 1):
208
+ newbins[i, j, k, l] = xyzbins[i, j, k, l]
209
+ xyzbins = newbins
210
+ maxatomsperbin += 10
211
+
212
+ if c > maxc:
213
+ maxc = c
214
+ xyzbins[x, y, z, 0] = c
215
+ xyzbins[x, y, z, c] = atomindex[n]
216
+
217
+ superlonglist = np.empty(14 * maxc, dtype=np.int64)
218
+
219
+ # Iterate over all bins with real atoms
220
+ for i in range(len(realbins)):
221
+ x, y, z = realbins[i]
222
+ c = xyzbins[x, y, z, 0]
223
+ shortlist = np.empty(c, dtype=np.int64)
224
+
225
+ # shortlist = all atoms in current bin
226
+ for j in range(c):
227
+ shortlist[j] = xyzbins[x, y, z, j+1]
228
+ superlonglist[j] = shortlist[j]
229
+
230
+ # Add all atoms in half of the nearby bins to longlist
231
+ end = False
232
+ for dz in range(-1, 2):
233
+ for dy in range(-1, 2):
234
+ for dx in range(-1, 2):
235
+
236
+ # Stop when reach center bin
237
+ if dx == 0 and dy == 0 and dz == 0:
238
+ end = True
239
+ break
240
+
241
+ # Skip non-existant neighbor bins
242
+ if (x + dx < 0 or x + dx == numxbins or
243
+ y + dy < 0 or y + dy == numybins or
244
+ z + dz < 0 or z + dz == numzbins):
245
+ continue
246
+
247
+ dc = xyzbins[x + dx, y + dy, z + dz, 0]
248
+ for j in range(dc):
249
+ superlonglist[c+j] = xyzbins[x + dx, y + dy, z + dz, j+1]
250
+ c += dc
251
+ if end:
252
+ break
253
+ if end:
254
+ break
255
+
256
+ longlist = superlonglist[:c]
257
+
258
+ # Compare all atoms in shortlist to longlist.
259
+ for u in range(shortlist.shape[0]):
260
+ uindex = shortlist[u]
261
+
262
+ upos = np.empty((longlist.shape[0]-u-1, 3))
263
+ vpos = np.empty((longlist.shape[0]-u-1, 3))
264
+
265
+ for w, v in enumerate(range(u+1, longlist.shape[0])):
266
+ for j in range(3):
267
+ vindex = longlist[v]
268
+ upos[w, j] = posv[uindex, j]
269
+ vpos[w, j] = posv[vindex, j]
270
+
271
+ # Compute distances
272
+ dmag2 = dmag2_c(upos, vpos, vects, pbc_a, pbc_b, pbc_c)
273
+
274
+ # Assign neighbors if within cutoff
275
+ for w, v in enumerate(range(u+1, longlist.shape[0])):
276
+ if dmag2[w] < cutoff2:
277
+ vindex = longlist[v]
278
+
279
+ if uindex != vindex:
280
+ isnew = True
281
+ uj = -1
282
+ vj = -1
283
+
284
+ # Find uj position to insert vindex
285
+ for j in range(1, neighbors[uindex, 0] + 1):
286
+ # Check uindex's neighbors for vindex
287
+ if neighbors[uindex, j] == vindex:
288
+ isnew = False
289
+ break
290
+ elif neighbors[uindex, j] > vindex:
291
+ uj = j
292
+ break
293
+ if uj == -1:
294
+ uj = neighbors[uindex, 0] + 1
295
+
296
+ if isnew:
297
+ # Find vj position to insert uindex
298
+ for j in range(1, neighbors[vindex, 0] + 1):
299
+ if neighbors[vindex, j] > uindex:
300
+ vj = j
301
+ break
302
+ if vj == -1:
303
+ vj = neighbors[vindex, 0] + 1
304
+
305
+ # Increase coordination
306
+ neighbors[uindex, 0] += 1
307
+ neighbors[vindex, 0] += 1
308
+
309
+ # Extend system size if needed
310
+ if neighbors[uindex, 0] > maxneighbors or neighbors[vindex, 0] > maxneighbors:
311
+ newneighbors = np.empty((natoms, maxneighbors + deltasize + 1), dtype=np.int64)
312
+ for j in range(neighbors.shape[0]):
313
+ for k in range(maxneighbors + 1):
314
+ newneighbors[j, k] = neighbors[j, k]
315
+ neighbors = newneighbors
316
+ maxneighbors += deltasize
317
+
318
+ # Shift neighbor indices with higher values
319
+ for j in range(neighbors[uindex, 0], uj - 1, -1):
320
+ neighbors[uindex, j] = neighbors[uindex, j - 1]
321
+ for j in range(neighbors[vindex, 0], vj - 1, -1):
322
+ neighbors[vindex, j] = neighbors[vindex, j - 1]
323
+
324
+ # Assign neighbors to each other
325
+ neighbors[uindex, uj] = vindex
326
+ neighbors[vindex, vj] = uindex
327
+
328
+ return np.asarray(neighbors)
329
+
330
+ @cython.boundscheck(False)
331
+ @cython.wraparound(False)
332
+ def unique_rows2(a):
333
+ """Takes two-dimensional array a and returns only the unique rows."""
334
+ return np.unique(a.view(np.dtype((np.void, a.dtype.itemsize*a.shape[1])))).view(a.dtype).reshape(-1, a.shape[1])
atomman/source/atomman/defect/Boundary.py ADDED
@@ -0,0 +1,803 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python imports
3
+ from typing import Optional, Tuple
4
+ from math import ceil
5
+
6
+ # http://www.numpy.org/
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+
10
+ from scipy.spatial.transform import Rotation
11
+
12
+ # Local imports
13
+ from .. import System
14
+ from ..tools import vect_angle, iaslist, miller
15
+
16
+
17
+ class Boundary():
18
+ """
19
+ Class for generating systems to investigate grain/phase boundaries
20
+ """
21
+ def __init__(self,
22
+ ucell1: System,
23
+ ucell2: System,
24
+ uvws1: npt.ArrayLike,
25
+ uvws2: npt.ArrayLike,
26
+ conventional_setting1: str = 'p',
27
+ conventional_setting2: str = 'p',
28
+ cutboxvector: str = 'c',
29
+ zerostrain: bool = False,
30
+ maxmult: int = 10,
31
+ tol: float = 1e-8):
32
+ """
33
+ Class initializer. This is generic to allow for either a grain or
34
+ phase boundary to be specified.
35
+
36
+ Parameters
37
+ ----------
38
+ ucell1 : atomman.System
39
+ The reference unit cell to use for the first grain.
40
+ ucell2 : atomman.System
41
+ The reference unit cell to use for the second grain.
42
+ uvws1 : array-like object
43
+ The three Miller(-Bravais) crystal vectors of ucell1 to use for
44
+ orienting the first grain such that each crystal vector will be
45
+ aligned with one of the box vectors of the final configuration.
46
+ uvws2 : array-like object
47
+ The three Miller(-Bravais) crystal vectors of ucell2 to use for
48
+ orienting the second grain such that each crystal vector will be
49
+ aligned with one of the box vectors of the final configuration.
50
+ conventional_setting1 : str, optional
51
+ Specifies which conventional lattice setting that ucell1 is in.
52
+ The default value of 'p' takes ucell to be primitive, in which case
53
+ the uvws1 values must be integers. This must be specified
54
+ in order to access non-integer lattice vectors for uvws1.
55
+ conventional_setting2 : str, optional
56
+ Specifies which conventional lattice setting that ucell1 is in.
57
+ The default value of 'p' takes ucell to be primitive, in which case
58
+ the uvws1 values must be integers. This must be specified
59
+ in order to access non-integer lattice vectors for uvws1.
60
+ cutboxvector : str, optional
61
+ Indicates which of the three box vectors that the boundary will
62
+ be placed along. Default value is 'c'.
63
+ zerostrain : bool, optional
64
+ Setting this as True will check to see if the orientations of the
65
+ two grains are fully compatible without introducing strain. This
66
+ is important for grain boundary energy comparisons. Default value
67
+ is False (no check performed).
68
+ maxmult : int, optional
69
+ The max integer multiplier to use for the zero strain check if
70
+ zerostrain is True.
71
+ """
72
+ # Set default values
73
+ self.__mults1 = np.zeros(3)
74
+ self.__mults2 = np.zeros(3)
75
+
76
+ # Save given settings
77
+ self.__ucell1 = ucell1
78
+ self.__ucell2 = ucell2
79
+ self.__uvws1 = uvws1
80
+ self.__uvws2 = uvws2
81
+ self.__cutboxvector = cutboxvector
82
+
83
+ # Generate compatible primitive unit cells and associated transformation matrixes
84
+ ucell_prim1, transform_c1_to_p1 = ucell1.dump('conventional_to_primitive',
85
+ setting=conventional_setting1,
86
+ return_transform=True, atol=tol)
87
+ ucell_prim2, transform_c2_to_p2 = ucell2.dump('conventional_to_primitive',
88
+ setting=conventional_setting2,
89
+ return_transform=True, atol=tol)
90
+
91
+ # Handle uvws inputs
92
+ uvws1, uvws_prim1 = interpret_rotation_uvws(uvws1, conventional_setting=conventional_setting1)
93
+ uvws2, uvws_prim2 = interpret_rotation_uvws(uvws2, conventional_setting=conventional_setting2)
94
+
95
+ # Save primitive cell settings
96
+ self.__ucell_prim1 = ucell_prim1
97
+ self.__ucell_prim2 = ucell_prim2
98
+ self.__transform_c1_to_p1 = Rotation.from_matrix(transform_c1_to_p1)
99
+ self.__transform_c2_to_p2 = Rotation.from_matrix(transform_c2_to_p2)
100
+ self.__uvws_prim1 = uvws_prim1
101
+ self.__uvws_prim2 = uvws_prim2
102
+
103
+ # Create rotated cells
104
+ rcell1, transform_p1_to_r1 = ucell_prim1.rotate(uvws_prim1, return_transform=True)
105
+ rcell2, transform_p2_to_r2 = ucell_prim2.rotate(uvws_prim2, return_transform=True)
106
+ clean_wrap(rcell1)
107
+ clean_wrap(rcell1)
108
+ self.__rcell1 = rcell1
109
+ self.__rcell2 = rcell2
110
+ self.__transform_p1_to_r1 = Rotation.from_matrix(transform_p1_to_r1)
111
+ self.__transform_p2_to_r2 = Rotation.from_matrix(transform_p2_to_r2)
112
+
113
+ # Check boxvectors
114
+ if cutboxvector == 'a':
115
+ if rcell1.box.bvect[0] != 0.0 or rcell1.box.cvect[0] != 0.0:
116
+ raise ValueError("box bvect and cvect cannot have x component for cutboxvector='a'")
117
+ if rcell2.box.bvect[0] != 0.0 or rcell2.box.cvect[0] != 0.0:
118
+ raise ValueError("box bvect and cvect cannot have x component for cutboxvector='a'")
119
+ self.__cutindex = 0
120
+
121
+ elif cutboxvector == 'b':
122
+ if rcell1.box.avect[1] != 0.0 or rcell1.box.cvect[1] != 0.0:
123
+ raise ValueError("box avect and cvect cannot have y component for cutboxvector='b'")
124
+ if rcell2.box.avect[1] != 0.0 or rcell2.box.cvect[1] != 0.0:
125
+ raise ValueError("box avect and cvect cannot have y component for cutboxvector='b'")
126
+ self.__cutindex = 1
127
+
128
+ elif cutboxvector == 'c':
129
+ if rcell1.box.avect[2] != 0.0 or rcell1.box.bvect[2] != 0.0:
130
+ raise ValueError("box avect and bvect cannot have z component for cutboxvector='c'")
131
+ if rcell2.box.avect[2] != 0.0 or rcell2.box.bvect[2] != 0.0:
132
+ raise ValueError("box avect and bvect cannot have z component for cutboxvector='c'")
133
+ self.__cutindex = 2
134
+
135
+ for i in range(3):
136
+ if i == self.cutindex:
137
+ continue
138
+ if not np.isclose(vect_angle(rcell1.box.vects[i], rcell2.box.vects[i]), 0.0):
139
+ raise ValueError('in-plane vectors of the two grains must be parallel')
140
+
141
+ if zerostrain:
142
+ strain = self.identifymults(maxmult)[2]
143
+ if not np.allclose(strain, np.zeros(3)):
144
+ raise ValueError('no zero strain configuration found')
145
+
146
+ @property
147
+ def uvws1(self) -> np.ndarray:
148
+ """numpy.NDArray: The three crystal vectors used to orient the first grain expressed as Miller(-Bravais) vectors of ucell1"""
149
+ return self.__uvws1
150
+
151
+ @property
152
+ def uvws2(self) -> np.ndarray:
153
+ """numpy.NDArray: The three crystal vectors used to orient the second grain expressed as Miller(-Bravais) vectors of ucell2"""
154
+ return self.__uvws2
155
+
156
+ @property
157
+ def uvws_prim1(self) -> np.ndarray:
158
+ """numpy.NDArray: The three crystal vectors used to orient the first grain expressed as Miller vectors of ucell_prim1"""
159
+ return self.__uvws_prim1
160
+
161
+ @property
162
+ def uvws_prim2(self) -> np.ndarray:
163
+ """numpy.NDArray: The three crystal vectors used to orient the second grain expressed as Miller vectors of ucell_prim2"""
164
+ return self.__uvws_prim2
165
+
166
+ @property
167
+ def ucell1(self) -> System:
168
+ """atomman.System: The conventional reference unit cell for the first grain."""
169
+ return self.__ucell1
170
+
171
+ @property
172
+ def ucell2(self) -> System:
173
+ """atomman.System: The conventional reference unit cell for the second grain."""
174
+ return self.__ucell2
175
+
176
+ @property
177
+ def ucell_prim1(self) -> System:
178
+ """atomman.System: The primitive reference unit cell for the first grain."""
179
+ return self.__ucell_prim1
180
+
181
+ @property
182
+ def ucell_prim2(self) -> System:
183
+ """atomman.System: The primitive reference unit cell for the second grain."""
184
+ return self.__ucell_prim2
185
+
186
+ @property
187
+ def rcell1(self) -> System:
188
+ """atomman.System: The rotated cell for the first grain."""
189
+ return self.__rcell1
190
+
191
+ @property
192
+ def rcell2(self) -> System:
193
+ """atomman.System: The rotated cell for the second grain."""
194
+ return self.__rcell2
195
+
196
+ @property
197
+ def transform_c1_to_p1(self) -> Rotation:
198
+ """
199
+ scipy.spatial.transform.Rotation: The Cartesian rotation associated with ucell1 to ucell_prim1
200
+ """
201
+ return self.__transform_c1_to_p1
202
+
203
+ @property
204
+ def transform_c2_to_p2(self) -> Rotation:
205
+ """
206
+ scipy.spatial.transform.Rotation: The Cartesian rotation associated with ucell2 to ucell_prim2
207
+ """
208
+ return self.__transform_c2_to_p2
209
+
210
+ @property
211
+ def transform_p1_to_r1(self) -> Rotation:
212
+ """
213
+ scipy.spatial.transform.Rotation: The Cartesian rotation associated with ucell_prim1 to rcell1
214
+ """
215
+ return self.__transform_p1_to_r1
216
+
217
+ @property
218
+ def transform_p2_to_r2(self) -> Rotation:
219
+ """
220
+ scipy.spatial.transform.Rotation: The Cartesian rotation associated with ucell_prim2 to rcell2
221
+ """
222
+ return self.__transform_p2_to_r2
223
+
224
+ @property
225
+ def cutboxvector(self) -> str:
226
+ """str: The box vector that the grain boundary is positioned along"""
227
+ return self.__cutboxvector
228
+
229
+ @property
230
+ def cutindex(self) -> int:
231
+ """int: The integer index associated with the cutboxvector setting"""
232
+ return self.__cutindex
233
+
234
+ @property
235
+ def mults1(self) -> np.ndarray:
236
+ """numpy.NDArray: Three int size multipliers for the top grain."""
237
+ return self.__mults1
238
+
239
+ @mults1.setter
240
+ def mults1(self, value: npt.ArrayLike):
241
+ value = np.asarray(value, dtype=int)
242
+ assert value.shape == (3,)
243
+ self.__mults1 = value
244
+
245
+ @property
246
+ def mults2(self) -> np.ndarray:
247
+ """numpy.NDArray: Three int size multipliers for the bottom grain."""
248
+ return self.__mults2
249
+
250
+ @mults2.setter
251
+ def mults2(self, value: npt.ArrayLike):
252
+ value = np.asarray(value, dtype=int)
253
+ assert value.shape == (3,)
254
+ self.__mults2 = value
255
+
256
+ def identifymults(self,
257
+ maxmult: int = 10,
258
+ minwidth: float = 0.0,
259
+ setvalues: bool = False
260
+ ) -> Tuple[np.ndarray, np.ndarray, np.ndarray]:
261
+ """
262
+ Compares the in-plane vectors of rcell1 and rcell2 to determine optimum
263
+ system multipliers to minimize strain.
264
+
265
+ Parameters
266
+ ----------
267
+ maxmult : int, optional
268
+ The maximum size multiplier to use in searching for low strain
269
+ states for the in-plane box vectors. Both grains will be searched
270
+ up to this max, so only one grain's multiplier is guaranteed to be
271
+ maxmult or less. Default value is 10.
272
+ minwidth: float, optional
273
+ A minimum width that is used to determine the size multipliers for
274
+ the out-of-plane box vectors of the two grains. This width limit is
275
+ independently applied to both grains (not the sum) and is taken in
276
+ the direction perpendicular to the grain boundary. Default value
277
+ is 0.0, which sets the multipliers to +-1.
278
+ setvalues: bool, optional
279
+ If True, the identified mults1 and mults2 values will be saved to
280
+ the corresponding class attributes. Default value is False.
281
+
282
+ Returns
283
+ -------
284
+ mults1 : numpy.ndarray
285
+ The suggested size multipliers to use with the top grain.
286
+ mults2 : numpy.ndarray
287
+ The suggested size multipliers to use with the bottom grain.
288
+ strains : numpy.ndarray
289
+ The estimated strains relative to the top grain that will result
290
+ from the two grains positioned together with the suggested
291
+ mults1 and mults2. Three values are given, one for each direction,
292
+ with the out-of-plane strain always being reported as zero.
293
+ """
294
+ strains = np.zeros(3)
295
+ mults1 = np.ones(3, dtype=int)
296
+ mults2 = np.ones(3, dtype=int)
297
+
298
+ for i in range(3):
299
+ if i == self.cutindex:
300
+ continue
301
+
302
+ length1 = np.linalg.norm(self.rcell1.box.vects[i])
303
+ length2 = np.linalg.norm(self.rcell2.box.vects[i])
304
+
305
+ # If lengths are equal, both mults are 1 and strains are zero
306
+ if length1 == length2:
307
+ continue
308
+
309
+ # Search for minimum strain configurations
310
+ mult1 = 0
311
+ mult2 = 0
312
+ strain = 999999
313
+ for multa in range(1, maxmult+1):
314
+
315
+ # Check multiples of length1
316
+ length = length1 * multa
317
+
318
+ # Compute strain and compare to current known minimum
319
+ multb = round(length / length2)
320
+ teststrain = (length - multb * length2) / length
321
+ if abs(teststrain) < abs(strain) and multb > 0:
322
+ mult1 = multa
323
+ mult2 = multb
324
+ strain = teststrain
325
+
326
+ if strain == 0.0:
327
+ break
328
+
329
+ # Check multiples of length2
330
+ length = length2 * multa
331
+
332
+ # Compute strain and compare to current known minimum
333
+ multb = round(length / length1)
334
+ teststrain = (multb * length1 - length) / (multb * length1)
335
+
336
+ if abs(teststrain) < abs(strain) and multb > 0:
337
+ mult1 = multb
338
+ mult2 = multa
339
+ strain = teststrain
340
+
341
+ if strain == 0.0:
342
+ break
343
+
344
+ strains[i] = strain
345
+ mults1[i] = mult1
346
+ mults2[i] = mult2
347
+
348
+ # Set out-of-plane multipliers based on minwidth
349
+ if minwidth == 0.0:
350
+ mults1[self.cutindex] = 1
351
+ mults2[self.cutindex] = -1
352
+ else:
353
+ width1 = self.rcell1.box.vects[self.cutindex, self.cutindex]
354
+ mults1[self.cutindex] = ceil(minwidth / width1)
355
+ width2 = self.rcell1.box.vects[self.cutindex, self.cutindex]
356
+ mults2[self.cutindex] = - ceil(minwidth / width2)
357
+
358
+ if setvalues:
359
+ self.mults1 = mults1
360
+ self.mults2 = mults2
361
+
362
+ return mults1, mults2, strains
363
+
364
+ def boundary(self,
365
+ mults1: Optional[npt.ArrayLike] = None,
366
+ mults2: Optional[npt.ArrayLike] = None,
367
+ maxmult: Optional[int] = None,
368
+ minwidth: Optional[float] = None,
369
+ freesurface: bool = False,
370
+ straintype: str = 'top',
371
+ shift1: float = 0.0,
372
+ shift2: float = 0.0,
373
+ deleter: float = 0.1,
374
+ deletefrom: str = 'top'):
375
+ """
376
+ Generates a phase/grain boundary configuration.
377
+
378
+ Parameters
379
+ ----------
380
+ mults1 : array-like object, optional
381
+ Three int size multipliers to use with the top grain. If given,
382
+ mults2 is required and maxmults and minwidth cannot be given. The
383
+ mults1 class attribute will be updated to this value by the method.
384
+ If mults1 is not given, then either maxmults must be given or the
385
+ mults1, mults2 class attributes must be set prior to calling.
386
+ mults2 : array-like object, optional
387
+ Three int size multipliers to use with the bottom grain. If given,
388
+ mults1 is required and maxmults and minwidth cannot be given. The
389
+ mults2 class attribute will be updated to this value by the method.
390
+ If mults2 is not given, then either maxmults must be given or the
391
+ mults1, mults2 class attributes must be set prior to calling.
392
+ maxmult : int, optional
393
+ The maximum size multiplier to use in searching for low strain
394
+ states for the in-plane box vectors. Both grains will be searched
395
+ up to this max, so only one grain's multiplier is guaranteed to be
396
+ maxmult or less. If given, mults1 and mults2 cannot be given. If
397
+ not given, then mults1 and mults2 must either be given or
398
+ previously set as class attributes.
399
+ minwidth: float, optional
400
+ A minimum width that is used to determine the size multipliers for
401
+ the out-of-plane box vectors of the two grains. This width limit is
402
+ independently applied to both grains (not the sum) and is taken in
403
+ the direction perpendicular to the grain boundary. Only allowed if
404
+ maxmult is given. If not given with maxmult, then will default to
405
+ 0.0.
406
+ freesurface: bool, optional
407
+ Indicates if the system is non-periodic along the cutboxvector and
408
+ therefore contains a free surface (value of True) or if all box
409
+ vectors are periodic and the system contains two boundaries (value
410
+ of False). Default value is False.
411
+ straintype: str, optional
412
+ Indicates how the lattice mismatch strain is applied. 'top' and
413
+ 'bottom' will strain only the associated grains while 'both' will
414
+ divide the strain between both grains.
415
+ shift1: float, optional
416
+ A rigid body shift to apply along one of the two in-plane box
417
+ vectors. This is taken relative to the rcell's a vector or b
418
+ vector if cutboxvector='a', so it should range between 0 and 1.
419
+ Default value is 0.0.
420
+ shift2: float, optional
421
+ A rigid body shift to apply along one of the two in-plane box
422
+ vectors. This is taken relative to the rcell's c vector or b
423
+ vector if cutboxvector='c', so it should range between 0 and 1.
424
+ Default value is 0.0.
425
+ deleter: float, optional
426
+ Any atoms in the separate grains closer than this distance will
427
+ have one atom of the pair deleted to prevent overlapping atoms.
428
+ Varying this value can possibly result in a lower energy
429
+ configuration. Default value is 0.1.
430
+ deletefrom: str, optional
431
+ Indicates which grain 'top' or 'bottom' the atoms found with
432
+ deletewidth will be deleted from. Typically doesn't matter
433
+ for symmetric tilt grain boundaries but does matter for
434
+ asymmetric grain boundaries or phase boundaries.
435
+
436
+ Returns
437
+ -------
438
+ atomman.System
439
+ The grain/phase boundary atomic configuration
440
+ natoms1 : int
441
+ The number of atoms in the top grain. This can be used to determine
442
+ which atoms are in which grain as the first natoms1 atoms in the
443
+ returned system will be in the top grain, and the rest in the bottom
444
+ grain.
445
+ """
446
+ if mults1 is not None or mults2 is not None:
447
+ assert mults1 is not None and mults2 is not None, (
448
+ 'mults1 and mults2 must be given together')
449
+ assert maxmult is None and minwidth is None, (
450
+ 'maxmult and minwidth cannot be given with mults1, mults2')
451
+ self.mults1 = mults1
452
+ self.mults2 = mults2
453
+
454
+ elif maxmult is not None:
455
+ if minwidth is None:
456
+ minwidth = 0.0
457
+ self.identifymults(maxmult=maxmult, minwidth=minwidth, setvalues=True)
458
+
459
+ elif minwidth is not None:
460
+ raise ValueError('maxmult must be given with minwidth')
461
+
462
+ # Create systems for each grain
463
+ assert not np.any(np.isclose(self.mults1, np.zeros(3))), 'mults cannot be zero'
464
+ assert not np.any(np.isclose(self.mults2, np.zeros(3))), 'mults cannot be zero'
465
+ assert self.mults1[self.cutindex] > 0, 'out-of-plane mult for top grain must be positive'
466
+ assert self.mults2[self.cutindex] < 0, 'out-of-plane mult for bottom grain must be negative'
467
+ system1 = self.rcell1.supersize(*self.mults1)
468
+ system2 = self.rcell2.supersize(*self.mults2)
469
+
470
+ # Build system
471
+ self.applystrain(system1, system2, straintype)
472
+ system = self.mergesystems(system1, system2, freesurface)
473
+ self.applyshift(system, shift1, shift2, system1.natoms)
474
+ newsystem, natoms1 = self.deleteoverlaps(system, deleter, deletefrom, system1.natoms)
475
+
476
+ return newsystem, natoms1
477
+
478
+ def applystrain(self, system1, system2, straintype):
479
+ """
480
+ Adjust the in-plane box vectors to apply the necessary strain for lattice
481
+ compatibility.
482
+ """
483
+ if straintype == 'top':
484
+ # Strain in-plane vects of top grain to match bottom grain vects
485
+ vects = system2.box.vects
486
+ vects[self.cutindex] = system1.box.vects[self.cutindex]
487
+ system1.box_set(vects=vects, origin=system1.box.origin, scale=True)
488
+
489
+ elif straintype == 'bottom':
490
+ # Strain in-plane vects of bottom grain to match top grain vects
491
+ vects = system1.box.vects
492
+ vects[self.cutindex] = system2.box.vects[self.cutindex]
493
+ system2.box_set(vects=vects, origin=system2.box.origin, scale=True)
494
+
495
+ elif straintype == 'both':
496
+ # Average vects (works as in-plane vects are parallel)
497
+ vects = (system1.box.vects + system2.box.vects) / 2
498
+
499
+ # Change vects while retaining out-of-plane vects and origins.
500
+ vects[self.cutindex] = system1.box.vects[self.cutindex]
501
+ system1.box_set(vects=vects, origin=system1.box.origin, scale=True)
502
+ vects[self.cutindex] = system2.box.vects[self.cutindex]
503
+ system2.box_set(vects=vects, origin=system2.box.origin, scale=True)
504
+
505
+ else:
506
+ raise ValueError('Unknown straintype value: allowed values are top, bottom, or both')
507
+
508
+ def mergesystems(self, system1, system2, freesurface):
509
+ """
510
+ """
511
+ # Combine atoms
512
+ system = system1.atoms_extend(system2.atoms)
513
+
514
+ # Increase box size
515
+ vects = system.box.vects
516
+ vects[self.cutindex] += system2.box.vects[self.cutindex]
517
+ system.box_set(vects=vects, origin=system2.box.origin)
518
+
519
+ if freesurface:
520
+ system.pbc[self.cutindex] = False
521
+
522
+ return system
523
+
524
+ def applyshift(self, system, shift1, shift2, natoms1):
525
+ """
526
+ """
527
+ if self.cutboxvector == 'a':
528
+ shift = shift1 * system.box.bvect + shift2 * system.box.cvect
529
+ elif self.cutboxvector == 'b':
530
+ shift = shift1 * system.box.avect + shift2 * system.box.cvect
531
+ elif self.cutboxvector == 'c':
532
+ shift = shift1 * system.box.avect + shift2 * system.box.bvect
533
+
534
+ system.atoms.pos[:natoms1] += shift
535
+ system.wrap()
536
+
537
+ def deleteoverlaps(self, system, deleter, deletefrom, natoms1):
538
+ """
539
+ """
540
+ if deleter == 0.0:
541
+ return system, natoms1
542
+
543
+ # Build neighborlist
544
+ cutoff = 1.2
545
+ if deleter > cutoff:
546
+ cutoff = deleter + 0.2
547
+ nlist = system.neighborlist(cutoff=cutoff)
548
+
549
+ dup = set()
550
+ for i in range(natoms1):
551
+ if nlist.coord[i] == 0:
552
+ continue
553
+
554
+ dmag = system.dmag(i, nlist[i])
555
+ if nlist.coord[i] == 1:
556
+ if dmag < deleter:
557
+ if deletefrom == 'top':
558
+ dup.add(i)
559
+ elif deletefrom == 'bottom':
560
+ dup.update(nlist[i])
561
+ else:
562
+ dups = nlist[i][dmag < deleter]
563
+
564
+ if deletefrom == 'top' and np.any(dups >= natoms1):
565
+ dup.add(i)
566
+ elif deletefrom == 'bottom':
567
+ dup.update(dups[dups >= natoms1])
568
+
569
+ keepindex = [x for i, x in enumerate(range(system.natoms)) if i not in dup]
570
+ newsystem = system.atoms_ix[keepindex]
571
+
572
+ if deletefrom == 'top':
573
+ natoms1 = natoms1 - len(dup)
574
+
575
+ return newsystem, natoms1
576
+
577
+ def iterboundaryshift(self,
578
+ mults1: Optional[npt.ArrayLike] = None,
579
+ mults2: Optional[npt.ArrayLike] = None,
580
+ maxmult: Optional[int] = None,
581
+ minwidth: Optional[float] = None,
582
+ freesurface: bool = False,
583
+ straintype = 'top',
584
+ shifts1 = 0.0,
585
+ shifts2 = 0.0,
586
+ deleters = 0.1,
587
+ deletefrom = 'top'):
588
+ """
589
+ Generates multiple proposed phase/grain boundary configurations for the
590
+ same boundary orientation where the in-plane shifts are varied as well
591
+ as which overlapping atoms are deleted.
592
+
593
+ Parameters
594
+ ----------
595
+ mults1 : array-like object, optional
596
+ Three int size multipliers to use with the top grain. If given,
597
+ mults2 is required and maxmults and minwidth cannot be given. The
598
+ mults1 class attribute will be updated to this value by the method.
599
+ If mults1 is not given, then either maxmults must be given or the
600
+ mults1, mults2 class attributes must be set prior to calling.
601
+ mults2 : array-like object, optional
602
+ Three int size multipliers to use with the bottom grain. If given,
603
+ mults1 is required and maxmults and minwidth cannot be given. The
604
+ mults2 class attribute will be updated to this value by the method.
605
+ If mults2 is not given, then either maxmults must be given or the
606
+ mults1, mults2 class attributes must be set prior to calling.
607
+ maxmult : int, optional
608
+ The maximum size multiplier to use in searching for low strain
609
+ states for the in-plane box vectors. Both grains will be searched
610
+ up to this max, so only one grain's multiplier is guaranteed to be
611
+ maxmult or less. If given, mults1 and mults2 cannot be given. If
612
+ not given, then mults1 and mults2 must either be given or
613
+ previously set as class attributes.
614
+ minwidth: float, optional
615
+ A minimum width that is used to determine the size multipliers for
616
+ the out-of-plane box vectors of the two grains. This width limit is
617
+ independently applied to both grains (not the sum) and is taken in
618
+ the direction perpendicular to the grain boundary. Only allowed if
619
+ maxmult is given. If not given with maxmult, then will default to
620
+ 0.0.
621
+ freesurface: bool, optional
622
+ Indicates if the system is non-periodic along the cutboxvector and
623
+ therefore contains a free surface (value of True) or if all box
624
+ vectors are periodic and the system contains two boundaries (value
625
+ of False). Default value is False.
626
+ straintype: str, optional
627
+ Indicates how the lattice mismatch strain is applied. 'top' and
628
+ 'bottom' will strain only the associated grains while 'both' will
629
+ divide the strain between both grains.
630
+ shifts1: int, float, or list, optional
631
+ Indicates which rigid body shifts to apply along one of the two
632
+ in-plane box vectors. This is taken relative to the rcell's a
633
+ vector or b vector if cutboxvector='a', so shift values should
634
+ range between 0 and 1. Giving a single float value will only
635
+ perform one shift while giving a list of floats will iterate over
636
+ all values. If an int is given, then that number of equally-
637
+ spaced shifts will be explored between 0 <= shift1 < 1. Default
638
+ value is 0.0 (no shift, no iteration).
639
+ shifts2: int, float, or list, optional
640
+ Indicates which rigid body shifts to apply along one of the two
641
+ in-plane box vectors. This is taken relative to the rcell's c
642
+ vector or b vector if cutboxvector='c', so shift values should
643
+ range between 0 and 1. Giving a single float value will only
644
+ perform one shift while giving a list of floats will iterate over
645
+ all values. If an int is given, then that number of equally-
646
+ spaced shifts will be explored between 0 <= shift1 < 1. Default
647
+ value is 0.0 (no shift, no iteration).
648
+ deleters: float or list, optional
649
+ One or more interatomic spacing cutoffs to explore for
650
+ identifying overlapping atoms between the two grains. If multiple
651
+ values are given, systems are yielded only if they differ from
652
+ the previous system with the same in-plane shift. Default value is
653
+ 0.1 (one value, no iteration).
654
+ deletefrom: str, optional
655
+ Indicates which grain 'top' or 'bottom' the atoms found with
656
+ deletewidth will be deleted from. A value of 'both' will iterate
657
+ over both top and bottom deletions. Typically doesn't matter
658
+ for symmetric tilt grain boundaries but does matter for
659
+ asymmetric grain boundaries or phase boundaries.
660
+
661
+ Yields
662
+ -------
663
+ atomman.System
664
+ The grain/phase boundary atomic configuration
665
+ natoms1 : int
666
+ The number of atoms in the top grain. This can be used to determine
667
+ which atoms are in which grain as the first natoms1 atoms in the
668
+ returned system will be in the top grain, and the rest in the bottom
669
+ grain.
670
+ """
671
+
672
+ # Set up values to iterate over
673
+ shifts1 = self.interpret_shifts(shifts1)
674
+ shifts2 = self.interpret_shifts(shifts2)
675
+ deleters = [float(i) for i in iaslist(deleters)]
676
+ if deletefrom == 'both':
677
+ deletefroms = ['top', 'bottom']
678
+ else:
679
+ deletefroms = [deletefrom]
680
+
681
+ # Iterate over all combos
682
+ for shift1 in shifts1:
683
+ for shift2 in shifts2:
684
+ for deletefrom in deletefroms:
685
+
686
+ # deletewidth must be inner loop to do the natoms check
687
+ natoms = -999999
688
+ for deleter in deleters:
689
+ system, natoms1 = self.boundary(mults1=mults1,
690
+ mults2=mults2,
691
+ maxmult=maxmult,
692
+ minwidth=minwidth,
693
+ freesurface=freesurface,
694
+ straintype=straintype,
695
+ shift1=shift1,
696
+ shift2=shift2,
697
+ deleter=deleter,
698
+ deletefrom=deletefrom)
699
+
700
+ # Only yield systems where a different number of atoms was deleted
701
+ if system.natoms != natoms:
702
+ natoms = system.natoms
703
+ yield system, natoms1
704
+
705
+ def interpret_shifts(self, val):
706
+ """
707
+ If int, do equal shifts in range [0, 1).
708
+ If float or == 0, only do that value.
709
+ If list, do those values
710
+ """
711
+ # Test for int value
712
+ try:
713
+ assert int(val) == val
714
+ assert val > 0
715
+ except (AssertionError, ValueError, TypeError):
716
+ pass
717
+ else:
718
+ return np.linspace(0, 1, num=int(val), endpoint=False)
719
+
720
+ return [float(i) for i in iaslist(val)]
721
+
722
+
723
+ def interpret_rotation_uvws(*uvws,
724
+ conventional_setting='p'):
725
+ """
726
+ Interprets the three rotation uvws for a crystal.
727
+
728
+ Parameters
729
+ ----------
730
+ uvws : list, array
731
+ The three Miller uvw vectors or Miller-Bravais uvtw vectors that the
732
+ unit cell should be rotated to align with. The three vectors can
733
+ either be given directly or within a list. Each value in the list can
734
+ itself be an array-like object or a str representation of a Miller
735
+ vector.
736
+ conventional_setting : str, optional
737
+ Indicates the conventional lattice setting associated with the Miller
738
+ vectors given. Default value is 'p' for a primitive unit cell.
739
+
740
+ Returns
741
+ -------
742
+ uvws_conv : numpy.ndarray
743
+ 3x3 array of Miller vector indices relative to the conventional unit cell.
744
+ uvws_prim : numpy.ndarray
745
+ 3x3 array of Miller vector indices relative to the primitive unit cell.
746
+
747
+ Raises
748
+ ------
749
+ ValueError
750
+ If the generated uvws_prim indices are not integer values.
751
+ """
752
+ # Check for valid len of uvws
753
+ if len(uvws) == 1:
754
+ uvws = uvws[0]
755
+ if len(uvws) != 3:
756
+ raise ValueError('invalid uvws: 3 values expected')
757
+
758
+ # Convert given uvws into uvws_conv array
759
+ uvws_conv = []
760
+ for uvw in uvws:
761
+ if isinstance(uvw, str):
762
+ uvw = miller.fromstring(uvw)
763
+ else:
764
+ uvw = np.asarray(uvw, dtype=float)
765
+
766
+ if uvw.shape == (4,):
767
+ uvw = miller.vector4to3(uvw)
768
+ elif uvw.shape != (3,):
769
+ raise ValueError('invalid uvws: each uvw must have 3 or 4 indices')
770
+
771
+ uvws_conv.append(uvw)
772
+ uvws_conv = np.array(uvws_conv)
773
+
774
+
775
+ # Convert uvws1 to the primitive cell
776
+ uvws_prim = miller.vector_conventional_to_primitive(uvws_conv, setting=conventional_setting)
777
+
778
+ # Check that uvws_prim are ints
779
+ uvws_prim_int = np.array(np.rint(uvws_prim), dtype=int)
780
+ if np.allclose(uvws_prim, uvws_prim_int):
781
+ uvws_prim = uvws_prim_int
782
+ else:
783
+ raise ValueError('primitive Miller vector indices are not all ints, therefore they are not lattice vectors!')
784
+
785
+ return uvws_conv, uvws_prim
786
+
787
+ def clean_wrap(system, atol=1e-7):
788
+ """
789
+ Wrap atoms around periodic boundaries cleanly
790
+
791
+ Parameters
792
+ ----------
793
+ system : atomman.System
794
+ The atomic system to wrap the atoms for.
795
+ atol : float
796
+ The absolute tolerance to use for identifying atoms on a boundary.
797
+ Default value is 1e-7.
798
+ """
799
+ # Safely wrap atoms into the rcells
800
+ system.atoms.pos += atol
801
+ system.wrap()
802
+ system.atoms.pos -= atol
803
+ system.atoms.pos[np.isclose(system.atoms.pos, 0.0, atol=atol, rtol=0)] = 0
atomman/source/atomman/defect/DifferentialDisplacement.py ADDED
@@ -0,0 +1,733 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ from re import A
4
+ from typing import Optional, Tuple, Union
5
+
6
+ # http://www.numpy.org/
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+
10
+ # https://matplotlib.org/
11
+ import matplotlib.pyplot as plt
12
+ import matplotlib.patches as mpatches
13
+ from matplotlib import cm
14
+
15
+ # atomman imports
16
+ from ..tools import axes_check
17
+ from .. import Box, System, NeighborList
18
+ from . import Strain
19
+ from ..plot import interpolate_contour
20
+
21
+ class DifferentialDisplacement():
22
+ def __init__(self,
23
+ system0: System,
24
+ system1: System,
25
+ neighbors: Optional[NeighborList] = None,
26
+ cutoff: Optional[float] = None,
27
+ reference: int = 1):
28
+ """
29
+ Class initializer. Calls solve if either neighbors or cutoff are given.
30
+
31
+ Parameters
32
+ ----------
33
+ system0 : atomman.system
34
+ The base/reference system to use.
35
+ system1 : atomman.system
36
+ The defect/current system to use.
37
+ neighbors : atomman.NeighborList, optional
38
+ The neighbor list to use.
39
+ cutoff : float, optional
40
+ Cutoff distance for computing a neighbor list. If reference = 0, then system_0
41
+ will be used to generate the list. If reference = 1, then system_1 will be
42
+ used to generate the list.
43
+ reference : int, optional
44
+ Indicates which of the two systems should be used for the plotting
45
+ reference: 0 or 1. If 0, then system_0's atomic positions will be
46
+ used for the calculation and neighbors should be for system_0. If
47
+ 1 (default), then system_1's atomic positions will be used
48
+ for the calculation and neighbors should be for system_1.
49
+ """
50
+
51
+ if neighbors is not None or cutoff is not None:
52
+ self.solve(system0, system1, neighbors=neighbors, cutoff=cutoff, reference=reference)
53
+ else:
54
+ assert system0.natoms == system1.natoms
55
+
56
+ self.__system0 = system0
57
+ self.__system1 = system1
58
+ self.reference = reference
59
+ self.__neighbors = None
60
+ self.__ddvectors = None
61
+ self.__arrowcenters = None
62
+ self.__arrowuvectors = None
63
+
64
+ @property
65
+ def reference(self) -> int:
66
+ """int : Indicates which system (0 or 1) is used as the reference."""
67
+ return self.__reference
68
+
69
+ @reference.setter
70
+ def reference(self, value: int):
71
+ assert value == 0 or value == 1, 'reference must be 0 or 1'
72
+ self.__reference = value
73
+
74
+ @property
75
+ def system0(self) -> System:
76
+ """atomman.System : The defect-free base system."""
77
+ return self.__system0
78
+
79
+ @property
80
+ def system1(self) -> System:
81
+ """atomman.System : The defect containing system."""
82
+ return self.__system1
83
+
84
+ @property
85
+ def neighbors(self) -> NeighborList:
86
+ """atomman.NeighborList : The list of neighbors identified for the reference system."""
87
+ return self.__neighbors
88
+
89
+ @property
90
+ def ddvectors(self) -> Optional[np.ndarray]:
91
+ """numpy.array or None : The computed differential displacement vectors."""
92
+ return self.__ddvectors
93
+
94
+ @property
95
+ def arrowcenters(self) -> Optional[np.ndarray]:
96
+ """numpy.array or None : The identified center positions for the ddvectors."""
97
+ return self.__arrowcenters
98
+
99
+ @property
100
+ def arrowuvectors(self) -> Optional[np.ndarray]:
101
+ """numpy.array or None : The unit vectors between all pairs of atoms for which the ddvectors have been computed."""
102
+ return self.__arrowuvectors
103
+
104
+ def solve(self,
105
+ system0: Optional[System] = None,
106
+ system1: Optional[System] = None,
107
+ neighbors: Optional[NeighborList] = None,
108
+ cutoff: Optional[float] = None,
109
+ reference: Optional[int] = None):
110
+ """
111
+ Solves the differential displacement vectors.
112
+
113
+ Parameters
114
+ ----------
115
+ system0 : atomman.system, optional
116
+ The base/reference system to use.
117
+ system1 : atomman.system, optional
118
+ The defect/current system to use.
119
+ neighbors : atomman.NeighborList, optional
120
+ The neighbor list to use.
121
+ cutoff : float, optional
122
+ Cutoff distance for computing a neighbor list. If reference = 0, then system_0
123
+ will be used to generate the list. If reference = 1, then system_1 will be
124
+ used to generate the list.
125
+ reference : int, optional
126
+ Indicates which of the two systems should be used for the plotting reference: 0 or 1.
127
+ If 0, then system0's atomic positions will be used for the calculation and
128
+ neighbors should be for system0. If 1, then system1's atomic positions will be used
129
+ for the calculation and neighbors should be for system1. Default value is
130
+ whatever was set when the object was initialized.
131
+ """
132
+ # Handle parameters
133
+ if system0 is not None:
134
+ self.__system0 = system0
135
+ else:
136
+ system0 = self.system0
137
+ if system1 is not None:
138
+ self.__system1 = system1
139
+ else:
140
+ system1 = self.system1
141
+ assert system0.natoms == system1.natoms
142
+
143
+ if reference is None:
144
+ reference = self.reference
145
+ else:
146
+ self.reference = reference
147
+ if reference == 0:
148
+ refsystem = system0
149
+ else:
150
+ refsystem = system1
151
+
152
+ if neighbors is None:
153
+ if cutoff is not None:
154
+ self.__neighbors = neighbors = refsystem.neighborlist(cutoff=cutoff)
155
+ else:
156
+ if self.neighbors is not None:
157
+ neighbors = self.neighbors
158
+ else:
159
+ raise ValueError('Either neighbors or cutoff must be given')
160
+ else:
161
+ self.__neighbors = neighbors
162
+
163
+ all_ddvectors = []
164
+ all_arrowcenters = []
165
+ all_arrowuvectors = []
166
+
167
+ # Loop over all atoms i in ref system
168
+ for i in np.arange(refsystem.natoms):
169
+ neighs = neighbors[i]
170
+ if len(neighs) == 0:
171
+ continue
172
+
173
+ # Compute distance vectors between atom i and its neighbors for both systems
174
+ dvectors0 = system0.dvect(int(i), neighs)
175
+ dvectors1 = system1.dvect(int(i), neighs)
176
+ if dvectors0.shape == (3,):
177
+ dvectors0.shape = (1,3)
178
+ dvectors1.shape = (1,3)
179
+
180
+ # Compute differential displacement vectors
181
+ ddvectors = dvectors1 - dvectors0
182
+
183
+ # Compite center points and direction vectors
184
+ if reference == 0:
185
+ arrowcenters = system0.atoms.pos[i] + dvectors0 / 2
186
+ arrowuvectors = dvectors0 / np.linalg.norm(dvectors0, axis=1)[:,np.newaxis]
187
+ else:
188
+ arrowcenters = system1.atoms.pos[i] + dvectors1 / 2
189
+ arrowuvectors = dvectors1 / np.linalg.norm(dvectors1, axis=1)[:,np.newaxis]
190
+
191
+ # Append calculation values to associated lists
192
+ all_ddvectors.append(ddvectors)
193
+ all_arrowcenters.append(arrowcenters)
194
+ all_arrowuvectors.append(arrowuvectors)
195
+
196
+ # Save computed values to object properties
197
+ self.__ddvectors = np.concatenate(all_ddvectors)
198
+ self.__arrowcenters = np.concatenate(all_arrowcenters)
199
+ self.__arrowuvectors = np.concatenate(all_arrowuvectors)
200
+
201
+ def plot(self,
202
+ component: Union[str, npt.ArrayLike],
203
+ ddmax: Optional[float],
204
+ plotxaxis: Union[str, npt.ArrayLike] = 'x',
205
+ plotyaxis: Union[str, npt.ArrayLike] = 'y',
206
+ xlim: Optional[tuple] = None,
207
+ ylim: Optional[tuple] = None,
208
+ zlim: Optional[tuple] = None,
209
+ arrowscale: float = 1,
210
+ arrowwidth: float = 0.005,
211
+ use0z: bool = False,
212
+ atomcolor: Union[str, list, None] = None,
213
+ atomcmap: Union[str, list, None] = None,
214
+ atomsize: float = 0.5,
215
+ figsize: int = 10,
216
+ matplotlib_axes: Optional[plt.axes] = None
217
+ ) -> Optional[plt.figure]:
218
+
219
+ r"""
220
+ Creates a matplotlib figure of a differential displacement map. Atom
221
+ positions are represented as circles, while the selected components of the
222
+ differential displacement vectors are plotted as arrows.
223
+
224
+ Parameters
225
+ ----------
226
+ component : str or array-like object
227
+ Indicates the component(s) of the differential displacement to plot.
228
+ Values of 'x', 'y', or 'z' will plot the component along that
229
+ Cartesian direction. A value of 'projection' will plot the
230
+ differential displacement vectors as projected onto the plotting
231
+ plane, thereby showing the two components perpendicular to the line
232
+ direction. If a 3D vector is given, then the component parallel to
233
+ that direction will be used.
234
+ ddmax : float or None
235
+ The maximum differential displacement value allowed. Values will be
236
+ kept between +-ddmax by wrapping values with larger absolute values
237
+ around by adding/subtracting 2*ddmax. Typically, this is set to be
238
+ \|b\|/2, but can be defect-specific. For instance, fcc a/2<110>
239
+ dislocations and basal hcp dislocations are typically plotted with
240
+ ddmax=\|b\|/4. If set to None, then no wrapping is done.
241
+ plotxaxis : str or array-like object, optional
242
+ Indicates the Cartesian direction associated with the system's atomic
243
+ coordinates to align with the plotting x-axis. Values are either 3D
244
+ unit vectors, or strings 'x', 'y', or 'z' for the Cartesian axes
245
+ directions. plotxaxis and plotyaxis must be orthogonal. Default value
246
+ is 'x' = [1, 0, 0].
247
+ plotyaxis : str or array-like object, optional
248
+ Indicates the Cartesian direction associated with the system's atomic
249
+ coordinates to align with the plotting y-axis. Values are either 3D
250
+ unit vectors, or strings 'x', 'y', or 'z' for the Cartesian axes
251
+ directions. plotxaxis and plotyaxis must be orthogonal. Default value
252
+ is 'y' = [0, 1, 0].
253
+ xlim : tuple, optional
254
+ The minimum and maximum coordinates along the plotting x-axis to
255
+ include in the fit. Values are taken in the specified length_unit.
256
+ If not given, then the limits are set based on min and max atomic
257
+ coordinates along the plotting axis.
258
+ ylim : tuple, optional
259
+ The minimum and maximum coordinates along the plotting y-axis to
260
+ include in the fit. Values are taken in the specified length_unit.
261
+ If not given, then the limits are set based on min and max atomic
262
+ coordinates along the plotting axis.
263
+ zlim : tuple, optional
264
+ The minimum and maximum coordinates normal to the plotting axes
265
+ (i.e. plotxaxis X plotyaxis) to include in the fit. Values are taken
266
+ in the specified length_unit. The optimum zlim should encompass only
267
+ a single periodic slice. If not given, then the limits are set
268
+ based on min and max atomic coordinates along the axis.
269
+ arrowscale : float, optional
270
+ Scaling factor for the magnitude of the differential displacement
271
+ arrows. Default value is 1: no scaling, vectors are in units of length.
272
+ For major components, this is often set such that the max differential
273
+ displacement component after wrapping (see ddmax) is scaled to the
274
+ distance between the atom pairs in the plot. For minor components, this
275
+ is often set to a large value simply to make the components visible.
276
+ arrowwidth : float, optional
277
+ Scaling factor to use for the width of the plotted arrows. Default value is
278
+ 0.005 = 1/200.
279
+ use0z : bool, optional
280
+ If False (default), the z coordinates from the reference system will be
281
+ used for zlim and atomcmap colors. If True, the z coordinates will be
282
+ used from system0 even if system1 is the reference system.
283
+ atomcolor : str or list, optional
284
+ Matplotlib color name(s) to use to display the atoms. If str, that
285
+ color will be assigned to all atypes. If list, must give a color value
286
+ or None for each atype. Default value (None) will use cmap instead.
287
+ Note: atomcolor and atomcmap can be used together as long as exactly
288
+ one color or cmap is given for each unique atype.
289
+ atomcmap : str or list, optional
290
+ Matplotlib colormap name(s) to use to display the atoms. Atoms will
291
+ be colored based on their initial positions and scaled using zlim. If
292
+ str, that cmap will be assigned to all atypes. If list, must give a
293
+ cmap value or None for each atype. Default value (None) will use 'hsv'
294
+ cmap. Note: atomcolor and atomcmap can be used together as long as
295
+ exactly one color or cmap is given for each unique atype.
296
+ atomsize : float, optional
297
+ The circle radius size to use for the plotted atom positions in units of
298
+ length. Default value is 0.5.
299
+ figsize : float or tuple, optional
300
+ Specifies the size of the figure to create in inches. If a single value
301
+ is given, it will be used for the figure's width, and the height will be
302
+ scaled based on the xlim and ylim values. Alternatively, both the width
303
+ and height can be set by passing a tuple of two values, but the plot will
304
+ not be guaranteed to be "regular" with respect to length dimensions.
305
+ matplotlib_axes : matplotlib.Axes.axes, optional
306
+ An existing matplotlib axes object. If given, the differential displacement
307
+ plot will be added to the specified axes of an existing figure. This
308
+ allows for subplots to be constructed. Note that figsize will be ignored
309
+ as the figure would have to be created beforehand and no automatic
310
+ optimum scaling of the figure's dimensions will occur.
311
+
312
+ Returns
313
+ -------
314
+ matplotlib.Figure
315
+ The generated figure. Not returned if matplotlib_axes is given.
316
+ """
317
+
318
+ ###################### Parameter and plot setup ########################
319
+
320
+ # Interpret plot axis values
321
+ plotxaxis = self.__plotaxisoptions(plotxaxis)
322
+ plotyaxis = self.__plotaxisoptions(plotyaxis)
323
+
324
+ # Build transformation matrix, T, from plot axes.
325
+ T = axes_check([plotxaxis, plotyaxis, np.cross(plotxaxis, plotyaxis)])
326
+
327
+ # Extract positions and transform using T
328
+ if self.reference == 0:
329
+ atompos = np.inner(self.system0.atoms.pos, T)
330
+ refsystem = self.system0
331
+ else:
332
+ atompos = np.inner(self.system1.atoms.pos, T)
333
+ refsystem = self.system1
334
+ if use0z:
335
+ pos0 = np.inner(self.system0.atoms.pos, T)
336
+ atompos[:, 2] = pos0[:, 2]
337
+
338
+ # Set default plot limits
339
+ if xlim is None:
340
+ xlim = (atompos[:, 0].min(), atompos[:, 0].max())
341
+ if ylim is None:
342
+ ylim = (atompos[:, 1].min(), atompos[:, 1].max())
343
+ if zlim is None:
344
+ zlim = (atompos[:, 2].min(), atompos[:, 2].max())
345
+
346
+ # Define box for identifying only points inside
347
+ plotbox = Box(xlo = xlim[0] - 5, xhi = xlim[1] + 5,
348
+ ylo = ylim[0] - 5, yhi = ylim[1] + 5,
349
+ zlo = zlim[0], zhi = zlim[1])
350
+
351
+ # Set plot height if needed
352
+ if isinstance(figsize, (int, float)):
353
+ dx = xlim[1] - xlim[0]
354
+ dy = ylim[1] - ylim[0]
355
+ figsize = (figsize, figsize * dy / dx)
356
+
357
+ # Initial plot setup and parameters
358
+ if matplotlib_axes is None:
359
+ fig = plt.figure(figsize=figsize, dpi=72)
360
+ ax1 = fig.add_subplot(111)
361
+ else:
362
+ ax1 = matplotlib_axes
363
+ ax1.axis([xlim[0], xlim[1], ylim[0], ylim[1]])
364
+
365
+ # Handle atomcolor and atomcmap values
366
+ atomcolor, atomcmap = self.__atomcoloroptions(atomcolor, atomcmap)
367
+
368
+ ######################## Add atom circles to plot ##############################
369
+
370
+ # Loop over all atoms i in plotting box
371
+ for i in np.arange(refsystem.natoms)[plotbox.inside(atompos)]:
372
+ atype = refsystem.atoms.atype[i]
373
+ atype_index = refsystem.atypes.index(atype)
374
+
375
+ # Plot a circle for atom i
376
+ if atomcmap[atype_index] is not None:
377
+ color = atomcmap[atype_index]((atompos[i, 2] - zlim[0]) / (zlim[1] - zlim[0]))
378
+ elif atomcolor[atype_index] is not None:
379
+ color = atomcolor[atype_index]
380
+ else:
381
+ color = None
382
+ if color is not None:
383
+ ax1.add_patch(mpatches.Circle(atompos[i, :2], atomsize, fc=color, ec='k'))
384
+
385
+ ######################## Arrow setup ##############################
386
+
387
+ # Build arrows based on component
388
+ arrowlengths, arrowcenters = self.__buildarrows(T, plotbox, component, ddmax)
389
+
390
+ # Scale arrows
391
+ arrowlengths = arrowscale * arrowlengths
392
+
393
+ # Compute arrow widths based on lengths
394
+ arrowwidths = arrowwidth * (arrowlengths[:,0]**2 + arrowlengths[:,1]**2)**0.5
395
+
396
+ # Plot the arrows
397
+ for center, length, width in zip(arrowcenters, arrowlengths, arrowwidths):
398
+ if width > 1e-7:
399
+ ax1.quiver(center[0], center[1], length[0], length[1],
400
+ pivot='middle', angles='xy', scale_units='xy',
401
+ scale=1, width=width, minshaft=2)
402
+
403
+ if matplotlib_axes is None:
404
+ return fig
405
+
406
+ def __plotaxisoptions(self, plotaxis):
407
+ """Internal method for handling plotxaxis and plotyaxis values"""
408
+
409
+ # Give numeric values for str plot axis terms
410
+ if plotaxis == 'x':
411
+ plotaxis = [1.0, 0.0, 0.0]
412
+ elif plotaxis == 'y':
413
+ plotaxis = [0.0, 1.0, 0.0]
414
+ elif plotaxis == 'z':
415
+ plotaxis = [0.0, 0.0, 1.0]
416
+
417
+ # Convert to numpy array
418
+ return np.asarray(plotaxis, dtype=float)
419
+
420
+ def __atomcoloroptions(self, atomcolor, atomcmap):
421
+ """Internal method for handling atomcolor and atomcmap options"""
422
+
423
+ # Identify number of atom types in the reference system
424
+ if self.reference == 0:
425
+ natypes = self.system0.natypes
426
+ else:
427
+ natypes = self.system1.natypes
428
+
429
+ # Set default atomcmap
430
+ if atomcolor is None and atomcmap is None:
431
+ atomcmap = 'hsv'
432
+
433
+ # Normalize atomcolor values
434
+ if isinstance(atomcolor, str):
435
+ atomcolor = [atomcolor for i in range(natypes)]
436
+ elif atomcolor is None:
437
+ atomcolor = [None for i in range(natypes)]
438
+ else:
439
+ atomcolor = list(atomcolor)
440
+ if len(atomcolor) != natypes:
441
+ raise ValueError('Invalid number of atomcolor values')
442
+
443
+ # Normalize atomcmap values
444
+ if isinstance(atomcmap, str):
445
+ atomcmap = [atomcmap for i in range(natypes)]
446
+ elif atomcmap is None:
447
+ atomcmap = [None for i in range(natypes)]
448
+ else:
449
+ atomcmap = list(atomcmap)
450
+ if len(atomcmap) != natypes:
451
+ raise ValueError('Invalid number of atomcmap values')
452
+
453
+ # Check that no atype has both atomcmap and atomcolor
454
+ for color, cmap in zip(atomcolor, atomcmap):
455
+ if color is not None and cmap is not None:
456
+ raise ValueError('atomcmap and atomcolor cannot both be given for the same atype')
457
+
458
+ # Convert atomcmap str values to color map objects
459
+ for ic in range(natypes):
460
+ if atomcmap[ic] is not None:
461
+ atomcmap[ic] = cm.get_cmap(atomcmap[ic])
462
+
463
+ return atomcolor, atomcmap
464
+
465
+ def __buildarrows(self, T, plotbox, component, ddmax):
466
+ """Internal method for building the parameters for plotting the arrows"""
467
+
468
+ # Manage component
469
+ if isinstance(component, str):
470
+ if component == 'x':
471
+ component = np.array([1.0, 0.0, 0.0])
472
+ elif component == 'y':
473
+ component = np.array([0.0, 1.0, 0.0])
474
+ elif component == 'z':
475
+ component = np.array([0.0, 0.0, 1.0])
476
+ elif component != 'projection':
477
+ raise ValueError('Invalid component style: must be x, y, z, projection, or numpy array')
478
+ else:
479
+ component = np.asarray(component, dtype=float)
480
+ assert component.shape == (3,), 'Invalid numeric component: must be a 3D vector'
481
+ component = component / np.linalg.norm(component)
482
+
483
+ # Transform arrow-related vectors
484
+ ddvectors = np.inner(self.ddvectors, T)
485
+ arrowcenters = np.inner(self.arrowcenters, T)
486
+ arrowuvectors = np.inner(self.arrowuvectors, T)
487
+
488
+ # Identify only vectors in ploting box
489
+ inbounds = plotbox.inside(arrowcenters)
490
+ ddvectors = ddvectors[inbounds]
491
+ arrowcenters = arrowcenters[inbounds]
492
+ arrowuvectors = arrowuvectors[inbounds]
493
+
494
+ # Build arrows for the xy component option
495
+ if isinstance(component, str) and component == 'projection':
496
+
497
+ # Arrows are in-plane vector components
498
+ ddcomponents = (ddvectors[:,0]**2 + ddvectors[:,1]**2)**0.5
499
+ arrowuvectors = ddvectors[:, :2] / ddcomponents[:,np.newaxis]
500
+
501
+ # Scheme for direction uniqueness (not sure what other projects use?)
502
+ arrowuvectors[ddvectors[:, 2] > 0] *= -1
503
+
504
+ # Normalize ddcomponents to be between +-ddmax
505
+ if ddmax is not None and ddmax > 0:
506
+ while True:
507
+ mask = ddcomponents > ddmax
508
+ if np.sum(mask) == 0:
509
+ break
510
+ ddcomponents[mask] -= 2 * ddmax
511
+ while True:
512
+ mask = ddcomponents < -ddmax
513
+ if np.sum(mask) == 0:
514
+ break
515
+ ddcomponents[mask] += 2 * ddmax
516
+
517
+ # Arrows have magnitude of ddcomponent
518
+ arrowlengths = arrowuvectors * ddcomponents[:,np.newaxis]
519
+
520
+ # Build arrows for vector components
521
+ else:
522
+ component = T.dot(component)
523
+ ddcomponents = ddvectors.dot(component)
524
+
525
+ # Normalize ddcomponents to be between +-ddmax
526
+ if ddmax is not None and ddmax > 0:
527
+ while True:
528
+ mask = ddcomponents > ddmax
529
+ if np.sum(mask) == 0:
530
+ break
531
+ ddcomponents[mask] -= 2 * ddmax
532
+ while True:
533
+ mask = ddcomponents < -ddmax
534
+ if np.sum(mask) == 0:
535
+ break
536
+ ddcomponents[mask] += 2 * ddmax
537
+
538
+ # Arrows have magnitude of ddcomponent and direction of uvectors
539
+ arrowlengths = arrowuvectors * ddcomponents[:,np.newaxis]
540
+
541
+ return arrowlengths, arrowcenters
542
+
543
+ def plot_with_nye(self,
544
+ component: Union[str, npt.ArrayLike],
545
+ ddmax: Optional[float],
546
+ strain: Strain,
547
+ plotxaxis: Union[str, npt.ArrayLike] = 'x',
548
+ plotyaxis: Union[str, npt.ArrayLike] = 'y',
549
+ xlim: Optional[tuple] = None,
550
+ ylim: Optional[tuple] = None,
551
+ zlim: Optional[tuple] = None,
552
+ vlim: Optional[tuple] = None,
553
+ cmap: str = 'bwr',
554
+ arrowscale: float = 1,
555
+ arrowwidth: float = 0.005,
556
+ use0z: bool = False,
557
+ atomcolor: Union[str, list, None] = None,
558
+ atomcmap: Union[str, list, None] = None,
559
+ atomsize: float = 0.5,
560
+ figsize: int = 10,
561
+ xbins: int = 200,
562
+ ybins: int = 200,
563
+ colorbar: bool = True,
564
+ fill_value: float = np.nan,
565
+ matplotlib_axes: Optional[plt.axes] = None,
566
+ ) -> Tuple[float, plt.figure]:
567
+
568
+ """
569
+ Utility function for simple combined dd-Nye plots. Note that this
570
+ method is currently limited to components and plot axis values of
571
+ 'x', 'y', and 'z'. This could be generalized in the future, if
572
+ there is interest...
573
+
574
+ Parameters
575
+ ----------
576
+ component : str or array-like object
577
+ Indicates the component(s) of the differential displacement to plot.
578
+ Values of 'x', 'y', or 'z' will plot the component along that
579
+ Cartesian direction. A value of 'projection' will plot the
580
+ differential displacement vectors as projected onto the plotting
581
+ plane, thereby showing the two components perpendicular to the line
582
+ direction. If a 3D vector is given, then the component parallel to
583
+ that direction will be used.
584
+ ddmax : float or None
585
+ The maximum differential displacement value allowed. Values will be
586
+ kept between +-ddmax by wrapping values with larger absolute values
587
+ around by adding/subtracting 2*ddmax. Typically, this is set to be
588
+ |b|/2, but can be defect-specific. For instance, fcc a/2<110>
589
+ dislocations and basal hcp dislocations are typically plotted with
590
+ ddmax=|b|/4. If set to None, then no wrapping is done.
591
+ strain : atomman.defect.Strain
592
+ A strain object computed for the system in question. This will be used
593
+ to compute the Nye tensor values that are included in the plot.
594
+ plotxaxis : str or array-like object, optional
595
+ Indicates the Cartesian direction associated with the system's atomic
596
+ coordinates to align with the plotting x-axis. Values are either 3D
597
+ unit vectors, or strings 'x', 'y', or 'z' for the Cartesian axes
598
+ directions. plotxaxis and plotyaxis must be orthogonal. Default value
599
+ is 'x' = [1, 0, 0].
600
+ plotyaxis : str or array-like object, optional
601
+ Indicates the Cartesian direction associated with the system's atomic
602
+ coordinates to align with the plotting y-axis. Values are either 3D
603
+ unit vectors, or strings 'x', 'y', or 'z' for the Cartesian axes
604
+ directions. plotxaxis and plotyaxis must be orthogonal. Default value
605
+ is 'y' = [0, 1, 0].
606
+ xlim : tuple, optional
607
+ The minimum and maximum coordinates along the plotting x-axis to
608
+ include in the fit. Values are taken in the specified length_unit.
609
+ If not given, then the limits are set based on min and max atomic
610
+ coordinates along the plotting axis.
611
+ ylim : tuple, optional
612
+ The minimum and maximum coordinates along the plotting y-axis to
613
+ include in the fit. Values are taken in the specified length_unit.
614
+ If not given, then the limits are set based on min and max atomic
615
+ coordinates along the plotting axis.
616
+ zlim : tuple, optional
617
+ The minimum and maximum coordinates normal to the plotting axes
618
+ (i.e. plotxaxis X plotyaxis) to include in the fit. Values are taken
619
+ in the specified length_unit. The optimum zlim should encompass only
620
+ a single periodic slice. If not given, then the limits are set
621
+ based on min and max atomic coordinates along the axis.
622
+ vlim : tuple, optional
623
+ Range limits for the Nye tensor contour plot. If not given, will be
624
+ determined by the range of the Nye tensor component values.
625
+ cmap : str, optional
626
+ The matplotlib colormap to use for the Nye tensor contour plot.
627
+ Default value is 'bwr'.
628
+ arrowscale : float, optional
629
+ Scaling factor for the magnitude of the differential displacement
630
+ arrows. Default value is 1: no scaling, vectors are in units of length.
631
+ For major components, this is often set such that the max differential
632
+ displacement component after wrapping (see ddmax) is scaled to the
633
+ distance between the atom pairs in the plot. For minor components, this
634
+ is often set to a large value simply to make the components visible.
635
+ arrowwidth : float, optional
636
+ Scaling factor to use for the width of the plotted arrows. Default value is
637
+ 0.005 = 1/200.
638
+ use0z : bool, optional
639
+ If False (default), the z coordinates from the reference system will be
640
+ used for zlim and atomcmap colors. If True, the z coordinates will be
641
+ used from system0 even if system1 is the reference system.
642
+ atomcolor : str or list, optional
643
+ Matplotlib color name(s) to use to display the atoms. If str, that
644
+ color will be assigned to all atypes. If list, must give a color value
645
+ or None for each atype. Default value (None) will use cmap instead.
646
+ Note: atomcolor and atomcmap can be used together as long as exactly
647
+ one color or cmap is given for each unique atype.
648
+ atomcmap : str or list, optional
649
+ Matplotlib colormap name(s) to use to display the atoms. Atoms will
650
+ be colored based on their initial positions and scaled using zlim. If
651
+ str, that cmap will be assigned to all atypes. If list, must give a
652
+ cmap value or None for each atype. Default value (None) will use 'hsv'
653
+ cmap. Note: atomcolor and atomcmap can be used together as long as
654
+ exactly one color or cmap is given for each unique atype.
655
+ atomsize : float, optional
656
+ The circle radius size to use for the plotted atom positions in units of
657
+ length. Default value is 0.5.
658
+ figsize : float or tuple, optional
659
+ Specifies the size of the figure to create in inches. If a single value
660
+ is given, it will be used for the figure's width, and the height will be
661
+ scaled based on the xlim and ylim values. Alternatively, both the width
662
+ and height can be set by passing a tuple of two values, but the plot will
663
+ not be guaranteed to be "regular" with respect to length dimensions.
664
+ xbins : int, optional
665
+ Specifies the number of interpolation bins to use along the plotting
666
+ x-axis. Default value is 200.
667
+ ybins : int, optional
668
+ Specifies the number of interpolation bins to use along the plotting
669
+ y-axis. Default value is 200.
670
+ colorbar: bool, optional
671
+ If True (default) a colorbar will be added to the plot.
672
+ fill_value: float, optional
673
+ Value used to fill in for grid points failed to interpolate in the fit.
674
+ If not given, then the default is np.nan, which may cause an error in
675
+ plotting for too narrow xlim and ylim settings.
676
+ matplotlib_axes : matplotlib.Axes.axes, optional
677
+ An existing matplotlib axes object. If given, the differential displacement
678
+ plot will be added to the specified axes of an existing figure. This
679
+ allows for subplots to be constructed. Note that figsize will be ignored
680
+ as the figure would have to be created beforehand and no automatic
681
+ optimum scaling of the figure's dimensions will occur.
682
+
683
+ Returns
684
+ -------
685
+ float
686
+ The integer sum of the Nye tensor over the plotted area.
687
+ matplotlib.Figure
688
+ The generated figure.
689
+
690
+ """
691
+ # Identify Nye tensor components based on component and plot axes
692
+ try:
693
+ component_index = ['x', 'y', 'z'].index(component)
694
+ except:
695
+ raise ValueError('component is currently limited to values of "x", "y" or "z"')
696
+ try:
697
+ x_index = ['x', 'y', 'z'].index(plotxaxis)
698
+ except:
699
+ raise ValueError('plotxaxis is currently limited to values of "x", "y" or "z"')
700
+ try:
701
+ y_index = ['x', 'y', 'z'].index(plotyaxis)
702
+ except:
703
+ raise ValueError('plotyaxis is currently limited to values of "x", "y" or "z"')
704
+ line_index = 3 - (x_index + y_index)
705
+ prop_index = [line_index, component_index]
706
+
707
+ # Generate dd plot
708
+ if matplotlib_axes is None:
709
+ fig = self.plot(component, ddmax, plotxaxis=plotxaxis, plotyaxis=plotyaxis,
710
+ xlim=xlim, ylim=ylim, zlim=zlim, arrowscale=arrowscale,
711
+ arrowwidth=arrowwidth, use0z=use0z, atomcolor=atomcolor,
712
+ atomcmap=atomcmap, atomsize=atomsize, figsize=figsize)
713
+ matplotlib_axes = fig.axes[0]
714
+ else:
715
+ self.plot(component, ddmax, plotxaxis=plotxaxis, plotyaxis=plotyaxis,
716
+ xlim=xlim, ylim=ylim, zlim=zlim, arrowscale=arrowscale,
717
+ arrowwidth=arrowwidth, use0z=use0z, atomcolor=atomcolor,
718
+ atomcmap=atomcmap, atomsize=atomsize, figsize=figsize,
719
+ matplotlib_axes=matplotlib_axes)
720
+ fig = None
721
+
722
+ # Add Nye tensor surface plot
723
+ intsum = interpolate_contour(self.system1, 'nye', prop_index=prop_index, prop=strain.nye,
724
+ plotxaxis=plotxaxis, plotyaxis=plotyaxis,
725
+ xlim=xlim, ylim=ylim, zlim=zlim, vlim=vlim, cmap=cmap,
726
+ xbins=xbins, ybins=ybins, fill_value=fill_value,
727
+ matplotlib_axes=matplotlib_axes,
728
+ dots=False, colorbar=colorbar, title=False)[0]
729
+
730
+ if fig is None:
731
+ return intsum
732
+ else:
733
+ return intsum, fig
atomman/source/atomman/defect/Dislocation/__init__.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ import io
4
+ from typing import Optional, Union
5
+ from itertools import product
6
+
7
+ # http://www.numpy.org/
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+
11
+ # https://github.com/usnistgov/DataModelDict
12
+ from DataModelDict import DataModelDict as DM
13
+
14
+ from yabadaba.record import Record
15
+
16
+ # atomman imports
17
+ from .. import solve_volterra_dislocation, VolterraDislocation
18
+ from ... import System, ElasticConstants, load
19
+ from ...tools import miller, vect_angle, boolean
20
+ from ...library import load_record, Database
21
+
22
+ class Dislocation():
23
+
24
+ from ._monopole import monopole, cylinder_boundary, box_boundary
25
+ from ._periodicarray import (periodicarray, array_boundary,
26
+ build_disl_array)
27
+ from ._dipole import dipole, dipole_displacement
28
+
29
+ def __init__(self,
30
+ ucell: System,
31
+ C: ElasticConstants,
32
+ burgers: npt.ArrayLike,
33
+ ξ_uvw: npt.ArrayLike,
34
+ slip_hkl: npt.ArrayLike,
35
+ conventional_setting: str = 'p',
36
+ ucell_setting: Optional[str] = None,
37
+ m: Union[str, npt.ArrayLike] = 'y',
38
+ n: Union[str, npt.ArrayLike] = 'z',
39
+ shift: Optional[npt.ArrayLike] = None,
40
+ shiftindex: Optional[int] = None,
41
+ shiftscale: bool = False,
42
+ tol: float = 1e-8):
43
+ """
44
+ Class initializer. Solves the dislocation solution and rotates the
45
+ given unit cell to the proper orientation.
46
+
47
+ Parameters
48
+ ----------
49
+ ucell : atomman.System
50
+ The unit cell to use as the seed for generating the dislocation
51
+ monopole system.
52
+ C : atomman.ElasticConstants
53
+ The elastic constants associated with the bulk crystal structure
54
+ for ucell.
55
+ burgers : array-like object
56
+ The dislocation's Burgers vector given as a Miller or
57
+ Miller-Bravais vector relative to ucell.
58
+ ξ_uvw : array-like object
59
+ The dislocation's line direction given as a Miller or
60
+ Miller-Bravais vector relative to ucell.
61
+ slip_hkl : array-like object
62
+ The dislocation's slip plane given as a Miller or Miller-Bravais
63
+ plane relative to ucell.
64
+ conventional_setting : str, optional
65
+ Indicates the space lattice setting of the given unit cell, i.e.
66
+ 'p' for primitive, 'i' for body-centered, 'f' for face-centered,
67
+ 'a', 'b', or 'c' for side-centered and 't1', or 't2' for trigonal
68
+ in a hexagonal setting. Setting this with the appropriate
69
+ conventional unit cell allows for identifying lattice vectors that
70
+ are not integers with respect to the conventional unit cell. This
71
+ also creates the rotated cell from a compatible primitive cell,
72
+ thereby the final dislocation configurations can be smaller than
73
+ possible solely from the conventional unit cell.
74
+ m : str or array-like object, optional
75
+ The Cartesian axis to align with the dislocation solution's m-axis,
76
+ i.e. the in-plane direction perpendicular to the dislocation line.
77
+ Can be specified as a 3D vector or str values 'x', 'y', or 'z'.
78
+ Default value is 'y' as this corresponds to the optimum alignment
79
+ for LAMMPS systems.
80
+ n : str or array-like object, optional
81
+ The Cartesian axis to align with the dislocation solution's n-axis,
82
+ i.e. the slip plane normal. Can be specified as a 3D vector or str
83
+ values 'x', 'y', or 'z'. Default value is 'z' as this corresponds
84
+ to the optimum alignment for LAMMPS systems.
85
+ shift : array-like object, optional
86
+ A rigid body shift to apply to the rotated cell prior to inserting
87
+ the dislocation. Should be selected such that the ideal slip plane
88
+ does not correspond to any atomic planes. Is taken as absolute if
89
+ shiftscale is False, or relative to the rotated cell's box vectors
90
+ if shiftscale is True. Cannot be given with shiftindex. If
91
+ neither shift nor shiftindex is given then shiftindex = 0 is used.
92
+ shiftindex : float, optional
93
+ The index of the identified optimum shifts based on the rotated
94
+ cell to use. Different values allow for the selection of different
95
+ atomic planes neighboring the slip plane. Note that shiftindex
96
+ values only apply shifts normal to the slip plane; best shifts for
97
+ non-planar dislocations (like bcc screw) may also need a shift in
98
+ the slip plane. Cannot be given with shiftindex. If neither shift
99
+ nor shiftindex is given then shiftindex = 0 is used.
100
+ shiftscale : bool, optional
101
+ If False (default), a given shift value will be taken as absolute
102
+ Cartesian. If True, a given shift will be taken relative to the
103
+ rotated cell's box vectors.
104
+ tol : float
105
+ A cutoff tolerance used with obtaining the dislocation solution.
106
+ Only needs to be changed if there are issues with obtaining a
107
+ solution.
108
+ """
109
+
110
+ # Generate the dislocation solution
111
+ self.__dislsol = solve_volterra_dislocation(C, burgers, ξ_uvw=ξ_uvw,
112
+ slip_hkl=slip_hkl, m=m, n=n,
113
+ cart_axes=True,
114
+ box=ucell.box, tol=tol)
115
+ self.__transform = self.dislsol.transform
116
+
117
+ if ucell_setting is not None:
118
+ raise TypeError('ucell_setting has been renamed conventional_setting for consistency')
119
+
120
+ # Build rcell and set orientation parameters
121
+ self.__set_cells(ucell, ξ_uvw, setting=conventional_setting, maxindex=5, tol=tol)
122
+
123
+ # Set shift value based on shift parameters
124
+ self.__identify_shifts(tol)
125
+ self.set_shift(shift, shiftindex, shiftscale)
126
+
127
+ # Set base_system and disl_system to None
128
+ self.__base_system = None
129
+ self.__disl_system = None
130
+
131
+ @classmethod
132
+ def fromrecord(cls,
133
+ record: Union[str, io.IOBase, DM, Record],
134
+ ucell: System,
135
+ C: ElasticConstants,
136
+ tol: float = 1e-8):
137
+ """
138
+ Initializes a Dislocation object based on pre-defined dislocation
139
+ parameters from a reference record.
140
+
141
+ Parameters
142
+ ----------
143
+ record : atomman.library.record.Dislocation, str, file-like object or DataModelDict
144
+ A Dislocation record object or the model contents for one.
145
+ ucell : atomman.System
146
+ The unit cell to use as the seed for generating the dislocation
147
+ monopole system.
148
+ C : atomman.ElasticConstants
149
+ The elastic constants associated with the bulk crystal structure
150
+ for ucell.
151
+ model : str, file-like object or DataModelDict
152
+ The reference record containing dislocation parameters to use.
153
+ tol : float
154
+ A cutoff tolerance used with obtaining the dislocation solution.
155
+ Only needs to be changed if there are issues with obtaining a
156
+ solution.
157
+ """
158
+ # Create record object if needed
159
+ if not isinstance(record, Record):
160
+ record = load_record('dislocation', model=record)
161
+
162
+ # Extract the dislocation parameters
163
+ slip_hkl = miller.fromstring(record.parameters['slip_hkl'])
164
+ ξ_uvw = miller.fromstring(record.parameters['ξ_uvw'])
165
+ burgers = miller.fromstring(record.parameters['burgers'])
166
+ m = np.fromstring(record.parameters.get('m', '0 1 0'), sep=' ')
167
+ n = np.fromstring(record.parameters.get('n', '0 0 1'), sep=' ')
168
+ conventional_setting = record.parameters.get('conventional_setting', 'p')
169
+
170
+ shift = record.parameters.get('shift', None)
171
+ shiftindex = record.parameters.get('shiftindex', None)
172
+ shiftscale = boolean(record.parameters.get('shiftscale', False))
173
+
174
+ if shift is not None:
175
+ shift = np.fromstring(shift, sep=' ')
176
+ elif shiftindex is not None:
177
+ shiftindex = int(shiftindex)
178
+
179
+ return cls(ucell, C, burgers, ξ_uvw, slip_hkl, m=m, n=n, shift=shift,
180
+ shiftindex=shiftindex, shiftscale=shiftscale,
181
+ conventional_setting=conventional_setting, tol=tol)
182
+
183
+ @classmethod
184
+ def fromdatabase(cls,
185
+ name: Optional[str] = None,
186
+ ucell: Optional[System] = None,
187
+ C: Optional[ElasticConstants] = None,
188
+ database: Optional[Database] = None,
189
+ prompt: bool = True,
190
+ tol: float = 1e-8,
191
+ **kwargs):
192
+ """
193
+ Construct a Dislocation object based on record(s) retrieved from the
194
+ reference database.
195
+
196
+ Parameters
197
+ ----------
198
+ name : str or None, optional
199
+ The name of the dislocation record to retrieve from the database.
200
+ Alternatively, you can use any other query keyword arguments supported
201
+ by the dislocation record style (see **kwargs below for more info).
202
+ ucell : atomman.System or None, optional
203
+ The unit cell to use in generating the system. If None (default), then
204
+ the crystal_prototype record that matches the defect's family setting
205
+ will be loaded from the database. Note that if None then the
206
+ crystal-specific info (lattice constants and symbols) should be given
207
+ here as kwargs (see below).
208
+ C : atomman.ElasticConstants, optional
209
+ The elastic constants associated with the bulk crystal structure
210
+ for ucell. Required, but future versions may fetch from the database.
211
+ database : atomman.library.Database or None, optional
212
+ A Database object to use to fetch the records. If None (default), then
213
+ a new Database instance will be created.
214
+ prompt : bool
215
+ If prompt=True (default) then a screen input will ask for a selection
216
+ if multiple matching dislocation (or crystal_prototype) records are
217
+ found. If prompt=False, then an error will be thrown if multiple
218
+ matches are found.
219
+ maxindex : int, optional
220
+ Max uvw index value to use in identifying the best uvw set for the
221
+ out-of-plane vector. If not given, will use the largest absolute
222
+ index between the given hkl and the initial in-plane vector guesses.
223
+ tol : float, optional
224
+ Tolerance parameter used to round off near-zero values. Default
225
+ value is 1e-8.
226
+ **kwargs : any
227
+ The recognized kwargs include the query keywords for free_surface
228
+ records (key, id, character, family), and the
229
+ crystal-specific parameters recognized by the prototype load style
230
+ (a, b, c, alpha, beta, gamma, symbols). The non-trivial
231
+ crystal-specific parameters should be for the crystal if ucell is
232
+ not given above as the crystal prototype lacks this information.
233
+ """
234
+ # Initialize a database if needed
235
+ if database is None:
236
+ database = Database()
237
+
238
+ # Extract ucell parameters from kwargs
239
+ prototype_kwargs = {}
240
+ prototype_kwargs_names = ['a', 'b', 'c', 'alpha', 'beta', 'gamma', 'symbols']
241
+ for prototype_kwargs_name in prototype_kwargs_names:
242
+ if prototype_kwargs_name in kwargs:
243
+ prototype_kwargs[prototype_kwargs_name] = kwargs.pop(prototype_kwargs_name)
244
+
245
+ # Fetch matching defect record
246
+ record = database.get_record('dislocation', name=name, prompt=prompt, **kwargs)
247
+
248
+ # Fetch crystal prototype unit cell if needed
249
+ if ucell is None:
250
+ ucell = load('prototype', name=record.family, **prototype_kwargs)
251
+ else:
252
+ if len(prototype_kwargs) > 0:
253
+ raise ValueError('crystal-specific kwargs cannot be given with ucell')
254
+
255
+ return cls.fromrecord(record=record, ucell=ucell, C=C, tol=tol)
256
+
257
+ @property
258
+ def dislsol(self) -> VolterraDislocation:
259
+ """atomman.defect.VolterraDislocation: The elastic dislocation solution"""
260
+ return self.__dislsol
261
+
262
+ @property
263
+ def uvws(self) -> np.ndarray:
264
+ """numpy.NDArray: The 3x3 array of uvw Miller vectors that correspond
265
+ to the rcell orientation.
266
+ """
267
+ return self.__uvws
268
+
269
+ @property
270
+ def uvws_prim(self) -> np.ndarray:
271
+ """numpy.NDArray: The 3x3 array of uvw Miller vectors used to rotate
272
+ ucell_prim to rcell.
273
+ """
274
+ return self.__uvws_prim
275
+
276
+ @property
277
+ def transform(self) -> np.ndarray:
278
+ """numpy.NDArray: The 3x3 Cartesian transformation matrix associated
279
+ with rotating from ucell to rcell.
280
+ """
281
+ return self.__transform
282
+
283
+ @property
284
+ def ucell(self) -> System:
285
+ """atomman.System: The reference conventional unit cell for the
286
+ dislocation system.
287
+ """
288
+ return self.__ucell
289
+
290
+ @property
291
+ def ucell_prim(self) -> System:
292
+ """atomman.System: The primitive unit cell of ucell used as the basis
293
+ for constructing rcell.
294
+ """
295
+ return self.__ucell_prim
296
+
297
+ @property
298
+ def rcell(self) -> System:
299
+ """atomman.System: The rotated cell that coincides with the dislocation
300
+ solution orientation.
301
+ """
302
+ return self.__rcell
303
+
304
+ @property
305
+ def lineindex(self) -> int:
306
+ """int: The index of the box vector that coincides with the dislocation
307
+ line: 0=a, 1=b, 2=c.
308
+ """
309
+ return self.__lineindex
310
+
311
+ @property
312
+ def cutindex(self) -> int:
313
+ """int: The index of the box vector that is not within the slip plane:
314
+ 0=a, 1=b, 2=c.
315
+ """
316
+ return self.__cutindex
317
+
318
+ @property
319
+ def motionindex(self) -> int:
320
+ """int: The index of the box vector that is not the line or cut
321
+ directions: 0=a, 1=b, 2=c.
322
+ """
323
+ return self.__motionindex
324
+
325
+ @property
326
+ def shifts(self) -> list:
327
+ """list: All identified shifts that will place the slip plane halfway
328
+ between atomic planes.
329
+ """
330
+ return self.__shifts
331
+
332
+ @property
333
+ def shift(self) -> np.ndarray:
334
+ """numpy.NDArray: The particular shift value that will be or was used
335
+ to construct the dislocation system.
336
+ """
337
+ return self.__shift
338
+
339
+ @property
340
+ def base_system(self) -> System:
341
+ """atomman.System: The "perfect crystal" reference system associated
342
+ with the dislocation system.
343
+ """
344
+ if self.__base_system is not None:
345
+ return self.__base_system
346
+ else:
347
+ raise ValueError(
348
+ 'base_system not built yet: must call monopole() or periodicarray() first'
349
+ )
350
+
351
+ @property
352
+ def disl_system(self) -> System:
353
+ """atomman.System: The generated dislocation system."""
354
+ if self.__disl_system is not None:
355
+ return self.__disl_system
356
+ else:
357
+ raise ValueError(
358
+ 'disl_system not built yet: must call monopole() or periodicarray() first'
359
+ )
360
+
361
+ def __set_cells(self, ucell, ξ_uvw, setting, maxindex=5, tol=1e-8):
362
+
363
+ # Extract dislsol
364
+ dislsol = self.dislsol
365
+
366
+ # Get primitive cell and associated transformation matrix
367
+ ucell_prim, c2p_transform = ucell.dump('conventional_to_primitive', setting=setting,
368
+ return_transform=True, atol=tol)
369
+
370
+ # Convert ξ_uvw to the primitive cell
371
+ ξ_uvw = np.asarray(ξ_uvw, dtype=float)
372
+ if ξ_uvw.shape[-1] == 4:
373
+ ξ_uvw = miller.vector4to3(ξ_uvw)
374
+ hexindices = True
375
+ else:
376
+ hexindices = False
377
+ ξ_uvw_p = miller.vector_conventional_to_primitive(ξ_uvw, setting=setting)
378
+
379
+ # Get Cartesian m, n axes relative to ucell_prim
380
+ m_cart = c2p_transform.dot(dislsol.m.dot(dislsol.transform))
381
+ n_cart = c2p_transform.dot(dislsol.n.dot(dislsol.transform))
382
+
383
+ # Generate an array of all int uvws with abs(u, v, w) <= maxindex
384
+ alluvws = np.array([p for p in product(range(-maxindex, maxindex+1), repeat=3)])
385
+ alluvws = alluvws[np.abs(alluvws).sum(axis=1) != 0] # Remove [0, 0, 0]
386
+
387
+ # Convert alluvws to Cartesian wrt the primitive ucell box
388
+ alluvws_cart = ucell_prim.box.vector_crystal_to_cartesian(alluvws)
389
+
390
+ # Compute the angles between alluvws and the m, n axes
391
+ m_angle = vect_angle(alluvws_cart, m_cart)
392
+ n_angle = vect_angle(alluvws_cart, n_cart)
393
+
394
+ # Find in-plane uvw closest to m
395
+ inplane_uvws = alluvws[np.isclose(n_angle, 90.0)]
396
+ inplane_m_angle = m_angle[np.isclose(n_angle, 90.0)]
397
+ m_uvw = inplane_uvws[np.isclose(inplane_m_angle, inplane_m_angle.min())]
398
+ if len(m_uvw) > 0:
399
+ m_uvw = m_uvw[0] / np.gcd.reduce(np.asarray(m_uvw[0], dtype=int))
400
+ else:
401
+ raise ValueError('Failed to find vector near edge component direction')
402
+
403
+ # Find uvw closest to n
404
+ n_uvw = alluvws[np.isclose(n_angle, n_angle.min())]
405
+ if len(n_uvw) > 0:
406
+ n_uvw = n_uvw[0] / np.gcd.reduce(np.asarray(n_uvw[0], dtype=int))
407
+ else:
408
+ raise ValueError('Failed to find vector near slip plane normal')
409
+
410
+ # Identify lineindex and cutindex
411
+ indices = np.array([0, 1, 2])
412
+ cutindex = indices[np.isclose(np.abs(dislsol.n), 1.0)][0]
413
+ lineindex = indices[np.isclose(np.abs(dislsol.ξ), 1.0)][0]
414
+ if cutindex == lineindex:
415
+ raise RuntimeError('Encountered cutindex == lineindex: should not be possible!')
416
+
417
+ # Orient the uvw sets based on cutboxvector and ξboxvector
418
+ if cutindex == 2:
419
+ if lineindex == 0:
420
+ uvws = np.array([ξ_uvw_p, m_uvw, n_uvw])
421
+ else:
422
+ uvws = np.array([-m_uvw, ξ_uvw_p, n_uvw])
423
+
424
+ elif cutindex == 1:
425
+ if lineindex == 2:
426
+ uvws = np.array([m_uvw, n_uvw, ξ_uvw_p])
427
+ else:
428
+ uvws = np.array([ξ_uvw_p, n_uvw, -m_uvw])
429
+
430
+ elif cutindex == 0:
431
+ if lineindex == 1:
432
+ uvws = np.array([n_uvw, ξ_uvw_p, m_uvw])
433
+ else:
434
+ uvws = np.array([n_uvw, -m_uvw, ξ_uvw_p])
435
+
436
+ # Generate rcell
437
+ rcell = ucell_prim.rotate(uvws)
438
+
439
+ # Save cells and orientation parameters as object attributes
440
+ self.__ucell = ucell
441
+ self.__ucell_prim = ucell_prim
442
+ self.__rcell = rcell
443
+
444
+ uvws_conv = miller.vector_primitive_to_conventional(uvws, setting=setting)
445
+ if hexindices:
446
+ self.__uvws = miller.vector3to4(uvws_conv)
447
+ else:
448
+ self.__uvws = uvws_conv
449
+ self.__uvws_prim = uvws
450
+ self.__lineindex = lineindex
451
+ self.__cutindex = cutindex
452
+ self.__motionindex = 3 - (lineindex + cutindex)
453
+
454
+ def __identify_shifts(self, tol):
455
+
456
+ # Define out of plane unit vector
457
+ ovect = self.dislsol.n
458
+
459
+ # Get out of plane width
460
+ rcellwidth = self.rcell.box.vects[self.cutindex, self.cutindex]
461
+
462
+ # Get the unique coordinates normal to the plane
463
+ pos = self.rcell.atoms.pos
464
+ numdec = - int(np.floor(np.log10(tol)))
465
+ coords = np.unique(pos[:, self.cutindex].round(numdec))
466
+
467
+ # Add periodic replica if missing
468
+ if not np.isclose(coords[-1] - coords[0], rcellwidth, rtol=0.0, atol=tol):
469
+ coords = np.append(coords, coords[0] + rcellwidth)
470
+
471
+ # Compute the shifts
472
+ relshifts = rcellwidth - (coords[1:] + coords[:-1]) / 2
473
+ relshifts[relshifts > rcellwidth] -= rcellwidth
474
+ relshifts[relshifts < 0.0] += rcellwidth
475
+ self.__shifts = np.outer(np.sort(relshifts), ovect)
476
+
477
+ def set_shift(self,
478
+ shift: Optional[npt.ArrayLike] = None,
479
+ shiftindex: Optional[int] = None,
480
+ shiftscale: bool = False):
481
+ """
482
+ Directly set the shift value based on shiftindex, or shift and shiftscale.
483
+ NOTE that the shift value can alternatively be set during class initialization
484
+ or when surface() is called.
485
+
486
+ Parameters
487
+ ----------
488
+ shift : array-like object, optional
489
+ Applies a shift to all atoms. Different values allow for free surfaces with
490
+ different termination planes to be selected. shift is taken as absolute
491
+ if shiftscale is False, or relative to the rotated cell's box vectors
492
+ if shiftscale is True. Cannot be given with shiftindex. If
493
+ neither shift nor shiftindex is given then shiftindex = 0 is used.
494
+ shiftindex : float, optional
495
+ The index of the identified shifts based on the rotated
496
+ cell to use. Different values allow for the selection of different
497
+ atomic planes neighboring the slip plane. Cannot be given with shift.
498
+ If neither shift nor shiftindex is given then shiftindex = 0 is used.
499
+ shiftscale : bool, optional
500
+ If False (default), a given shift value will be taken as absolute
501
+ Cartesian. If True, a given shift will be taken relative to the
502
+ rotated cell's box vectors.
503
+ """
504
+ # Handle shift parameters
505
+ if shift is not None:
506
+ if shiftindex is not None:
507
+ raise ValueError('shift and shiftindex cannot both be given')
508
+ if shiftscale is True:
509
+ self.__shift = miller.vector_crystal_to_cartesian(shift, self.rcell.box)
510
+ else:
511
+ self.__shift = np.asarray(shift)
512
+ assert self.__shift.shape == (3,)
513
+
514
+ elif shiftindex is not None:
515
+ self.__shift = self.shifts[shiftindex]
516
+
517
+ else:
518
+ self.__shift = self.shifts[0]
519
+
520
+ def set_systems(self,
521
+ base_system: System,
522
+ disl_system: System):
523
+ """
524
+ Used by the configuration generators to set base and dislocation systems
525
+ as class attributes.
526
+
527
+ Parameters
528
+ ----------
529
+ base_system : atomman.System
530
+ The base reference system.
531
+ disl_system : atomman.System
532
+ The dislocation system.
533
+ """
534
+ self.__base_system = base_system
535
+ self.__disl_system = disl_system
atomman/source/atomman/defect/Dislocation/_dipole.py ADDED
@@ -0,0 +1,300 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ from typing import Optional, Tuple, Union
3
+ from copy import deepcopy
4
+
5
+ # http://www.numpy.org/
6
+ import numpy as np
7
+ import numpy.typing as npt
8
+
9
+ # Local imports
10
+ from ... import System
11
+ from . import VolterraDislocation
12
+
13
+ @staticmethod
14
+ def dipole_displacement(dislsol: VolterraDislocation,
15
+ pos: npt.ArrayLike,
16
+ x1: Union[float, npt.ArrayLike],
17
+ x2: Union[float, npt.ArrayLike],
18
+ mvect: npt.ArrayLike,
19
+ nvect: npt.ArrayLike,
20
+ N: int = 5) -> np.ndarray:
21
+ """
22
+ Uses the method of Cai, Bulatov, Chang, Li & Yip, Phil Mag 2003, 83(5), 539-567
23
+ https://doi.org/10.1080/0141861021000051109 to compute the displacement associated
24
+ with a dislocation dipole in a periodic lattice.
25
+
26
+ Parameters
27
+ ----------
28
+ dislsol : atomman.defect.VolterraDislocation
29
+ A Volterra dislocation solution object. This provides the dislocation
30
+ orientation (m, n vectors) and the displacement solutions for the two
31
+ individual dislocations of opposite Burgers vectors.
32
+ pos : array-like object
33
+ The coordinates to evaluate the displacement at.
34
+ x1 : float or array-like object
35
+ The coordinate(s) of the first dislocation. If a float is given, it will be
36
+ multiplied by the dislocation solution's m vector.
37
+ x2 : float or array-like object
38
+ The coordinate(s) of the second dislocation. If a float is given, it will be
39
+ multiplied by the dislocation solution's m vector.
40
+ mvect : array-like object
41
+ One of the two 2D cell vectors. This should be parallel to the dislocation
42
+ solution's m vector and a whole periodic lattice vector.
43
+ nvect : array-like object
44
+ One of the two 2D cell vectors. This should be a whole lattice vector with
45
+ major component along the dislocation solution's n vector. It does not need
46
+ to be parallel to n and can have components in either of the two coordinate
47
+ directions.
48
+ N : int, optional
49
+ Indicates how many image cells are used. A rectangular grid of images is used
50
+ meaning that there will be NxN total cells evaluated, of which NxN-1 will be
51
+ image cells. Default value is 5.
52
+
53
+ Returns
54
+ -------
55
+ displacement : numpy.NDArray
56
+ The associated displacement field evaluated at pos for the dipole configuration.
57
+ """
58
+
59
+ # Extract the dislocation solution m,n axes
60
+ m = dislsol.m
61
+ n = dislsol.n
62
+
63
+ # Manage input parameters
64
+ pos = np.asarray(pos)
65
+ mvect = np.asarray(mvect)
66
+ nvect = np.asarray(nvect)
67
+ if isinstance(x1, (float, int)):
68
+ x1 = x1 * m
69
+ else:
70
+ x1 = np.asarray(x1)
71
+ if isinstance(x2, (float, int)):
72
+ x2 = x2 * m
73
+ else:
74
+ x2 = np.asarray(x2)
75
+
76
+ # Define the non-periodic component of the displacement
77
+ def nonperiodic(pos):
78
+ """
79
+ The dipole displacement field in an infinite medium, i.e. no periodic
80
+ replicas only the two dislocations themselves.
81
+ """
82
+ disp1 = dislsol.displacement(pos - x1)
83
+ disp2 = dislsol.displacement(pos - x2)
84
+
85
+ return disp1 - disp2
86
+
87
+ # Define the periodic component of the displacement
88
+ def periodic(pos):
89
+ """
90
+ The dipole displacement field including periodic replicas from i,j = -N to N
91
+ in both directions normal to the dislocation line. NOTE that this includes the
92
+ non-periodic component i=j=0.
93
+ """
94
+ disp = np.zeros_like(pos)
95
+ for i in range(-N, N+1):
96
+ for j in range(-N, N+1):
97
+ shifted_pos = pos + i * mvect + j * nvect
98
+ disp += nonperiodic(shifted_pos)
99
+
100
+ return disp
101
+
102
+ # Get m, n components of mvect and nvect
103
+ mn = np.array([mvect, nvect]).dot(np.array([m, n]).T)
104
+
105
+ # Get reciprocal of mn
106
+ reciprocal_mn = np.linalg.inv(mn).T
107
+
108
+ # Compute the displacement values at the four corners of the 2D [mvect, nvect] cell
109
+ corners = np.array([
110
+ -0.5 * mvect - 0.5 * nvect, # bottom left corner, blc
111
+ 0.5 * mvect - 0.5 * nvect, # blc + mvect
112
+ -0.5 * mvect + 0.5 * nvect, # blc + nvect
113
+ 0.5 * mvect + 0.5 * nvect]) # blc + mvect + nvect
114
+ disp_corners = periodic(corners)
115
+
116
+ # Find total changes in displacement along mvect and nvect
117
+ delta_disp = np.array([disp_corners[1] - disp_corners[0], disp_corners[2] - disp_corners[0]])
118
+
119
+ # Define the correction component of the displacement
120
+ def correction(pos):
121
+ """
122
+ This computes the linear displacement correction to apply to the system
123
+ to ensure that it is compatible across the periodic boundaries
124
+ """
125
+ # Transform pos to be relative to mn and multiply by delta disp
126
+ pos_mn = np.vstack([pos.dot(m), pos.dot(n)]).T
127
+ relpos_mn = np.inner(pos_mn, reciprocal_mn)
128
+ disp = relpos_mn.dot(delta_disp)
129
+
130
+ return disp
131
+
132
+ # Verify the correction against the corners
133
+ correction_test = disp_corners - correction(corners)
134
+ assert(np.allclose(correction_test[0], correction_test[1]))
135
+ assert(np.allclose(correction_test[0], correction_test[2]))
136
+ assert(np.allclose(correction_test[0], correction_test[3]))
137
+
138
+ # Compute the displacements for pos
139
+ disp = periodic(pos) - correction(pos)
140
+
141
+ return disp
142
+
143
+ def dipole(self,
144
+ sizemults: Tuple,
145
+ boxtilt: bool = True,
146
+ numreplicas: int = 5,
147
+ shift: Optional[npt.ArrayLike] = None,
148
+ shiftindex: Optional[int] = None,
149
+ shiftscale: bool = False,
150
+ center: Optional[npt.ArrayLike] = None,
151
+ centerscale: bool = False,
152
+ return_base_system: bool = False
153
+ ) -> Union[System, Tuple[System, System]]:
154
+ """
155
+ Constructs a dislocation dipole configuration as described by Li, Wang,
156
+ Chang, Cai, Bulatov, Ho, Yip, Phys Rev B 70(10) (2004) 104113
157
+ https://doi.org/10.1103/Physrevb.70.104113. Two parallel dislocations with
158
+ opposite Burgers vectors will be inserted into the system using a Volterra
159
+ solution. The resulting configuration is periodic in all three directions
160
+ and constitutes a regularly-spaced 2D grid of parallel dislocations. A
161
+ shear strain is also applied to the system of 1/2 the Burgers vector to
162
+ counteract the elastic strain of the dislocations and ensure stability.
163
+
164
+ Parameters
165
+ ----------
166
+ sizemults : tuple
167
+ The three size multipliers to use when generating the system. Values
168
+ should be positive integers if boxtilt is False. When boxtilt is True,
169
+ the multipliers are limited to values that result in full lattice
170
+ vectors once the tilt is added. Depending on the system, fractional
171
+ values may be possible, or some integer values not allowed.
172
+ boxtilt : bool, optional
173
+ If True (default) then a tilt will be applied to the system such that
174
+ the resulting periodic configuration will be consistent with a
175
+ "quadripole" representation in which each dislocation will be
176
+ surrounded by dislocations of the opposite sign in both the m- and n-
177
+ directions. This is achieved by adding half of the box vector
178
+ most aligned with the m-axis to the box vector most aligned with the
179
+ n-axis. A value of False will not tilt the system, so only the
180
+ sizemults will be applied to the rotated cell. The non-tilted system
181
+ will have dislocations of the same sign aligned along the n-axis.
182
+ numreplicas : int, optional
183
+ Indicates how many image cells are used for computing the displacement
184
+ field of the dipole. A rectangular grid of images is used
185
+ meaning that there will be NxN total cells evaluated, of which NxN-1
186
+ will be image cells. Default value is 5.
187
+ shift : array-like object, optional
188
+ A rigid body shift to apply to the rotated cell prior to inserting
189
+ the dislocation. Should be selected such that the ideal slip plane
190
+ does not correspond to any atomic planes. Is taken as absolute if
191
+ shiftscale is False, or relative to the rotated cell's box vectors
192
+ if shiftscale is True. Cannot be given with shiftindex. If
193
+ neither shift nor shiftindex is given will use the shift set during
194
+ class initialization.
195
+ shiftindex : float, optional
196
+ The index of the identified optimum shifts based on the rotated
197
+ cell to use. Different values allow for the selection of different
198
+ atomic planes neighboring the slip plane. Note that shiftindex
199
+ values only apply shifts normal to the slip plane; best shifts for
200
+ non-planar dislocations (like bcc screw) may also need a shift in
201
+ the slip plane. Cannot be given with shiftindex. If neither shift
202
+ nor shiftindex is given then shiftindex = 0 is used then will use
203
+ the shift set during class initialization.
204
+ shiftscale : bool, optional
205
+ If False (default), a given shift value will be taken as absolute
206
+ Cartesian. If True, a given shift will be taken relative to the
207
+ rotated cell's box vectors.
208
+ center : array-like object or None, optional
209
+ Indicates where the dislocations are positioned in the configuration
210
+ relative to the default locations. For dipole configurations, the
211
+ default locations are at relative positions of (1/4, 1/2) and
212
+ (3/4, 1/2) of the box dimensions of the final configuration that
213
+ correspond to the dislocation solution's m- and n-axes.
214
+ centerscale : bool, optional
215
+ If False (default), a given center value will be taken as absolute
216
+ Cartesian. If True, a given center will be taken relative to the
217
+ rotated cell's box vectors.
218
+ return_base_system : bool, optional
219
+ If True then the dislocation-free base system corresponding to the
220
+ dislocation system will also be returned. The base system is used
221
+ as a reference state for most of the dislocation analysis tools.
222
+
223
+ Returns
224
+ -------
225
+ base_system : atomman.System
226
+ The base "perfect crystal" reference system associated with the
227
+ dislocation system. Only returned if return_base_system is True.
228
+ disl_system : atomman.System
229
+ The generated dislocation monopole system.
230
+ """
231
+ # Extract box vector orientations
232
+ cutindex = self.cutindex
233
+ #lineindex = self.lineindex
234
+ motionindex = self.motionindex
235
+
236
+ # Multiply primitive uvws by sizemults to get dipole rotation uvws
237
+ uvws_dipole = (np.asarray(sizemults) * self.uvws_prim.T).T
238
+
239
+ # Apply boxtilt
240
+ if boxtilt:
241
+ uvws_dipole[cutindex] += uvws_dipole[motionindex] / 2
242
+
243
+ # Check that the uvws are lattice vectors
244
+ if not np.allclose(uvws_dipole, np.asarray(np.rint(uvws_dipole), dtype='int64')):
245
+ raise ValueError(f'sizemults (and boxtilt) did not result in int lattice vectors: {uvws_dipole}')
246
+
247
+ # Create base_system
248
+ base_system = self.ucell_prim.rotate(uvws_dipole)
249
+
250
+ # Handle shift parameters
251
+ if shift is not None or shiftindex is not None:
252
+ self.set_shift(shift, shiftindex, shiftscale)
253
+ shift = self.shift
254
+
255
+ # Handle center parameter
256
+ if center is None:
257
+ center = np.array([0,0,0])
258
+ else:
259
+ center = np.asarray(center)
260
+ if centerscale:
261
+ center = self.rcell.box.vector_crystal_to_cartesian(center)
262
+
263
+ # Apply the rigid body shift to the atoms
264
+ base_system.atoms.pos += shift
265
+
266
+ # Move the box origin to the center of the system
267
+ neworigin = - (base_system.box.vects[cutindex] + base_system.box.vects[motionindex]) / 2
268
+ base_system.box_set(vects=base_system.box.vects, origin=neworigin)
269
+
270
+ # Identify x1 and x2 positions
271
+ m = self.dislsol.m
272
+ length = base_system.box.vects.dot(m).dot(m)
273
+ x2 = (length / 4) * m
274
+ x1 = -x2
275
+
276
+ # Shift atoms so that dislocation solution is centered at x1
277
+ base_system.atoms.pos += x1
278
+ base_system.wrap()
279
+
280
+ # Compute displacement solution
281
+ mvect = base_system.box.vects[motionindex]
282
+ nvect = base_system.box.vects[cutindex]
283
+ disp = self.dipole_displacement(self.dislsol, base_system.atoms.pos - center,
284
+ x1, x2, mvect, nvect, N=numreplicas)
285
+
286
+ # Create the dislocation system
287
+ disl_system = deepcopy(base_system)
288
+ disl_system.atoms.pos += disp
289
+
290
+ # Apply the balancing strain (ONLY FOR SCREW RIGHT NOW!)
291
+ newvects = disl_system.box.vects
292
+ newvects[cutindex] -= 0.5 * self.dislsol.burgers
293
+ disl_system.box_set(vects=newvects, origin=disl_system.box.origin, scale=True)
294
+
295
+ self.set_systems(base_system, disl_system)
296
+
297
+ if return_base_system:
298
+ return base_system, disl_system
299
+ else:
300
+ return disl_system
atomman/source/atomman/defect/Dislocation/_monopole.py ADDED
@@ -0,0 +1,335 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ from copy import deepcopy
4
+ from typing import Optional, Tuple, Union
5
+
6
+ # http://www.numpy.org/
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+
10
+ # atomman imports
11
+ from ... import Box, System
12
+ from ...region import PlaneSet, Cylinder
13
+
14
+ def box_boundary(self,
15
+ box: Box,
16
+ width: float) -> PlaneSet:
17
+ """
18
+ Constructs a shape associated with the box-style boundary region. Used
19
+ by the monopole() generation method. The returned shape will encompass
20
+ all atoms except those within width distance of the two non-periodic
21
+ surfaces.
22
+
23
+ Parameters
24
+ ----------
25
+ box : atomman.Box
26
+ The box associated with the full (base) system.
27
+ width : float
28
+ The width of the boundary region
29
+
30
+ Returns
31
+ -------
32
+ atomman.region.PlaneSet
33
+ The Shape object excluding the boundary region
34
+ """
35
+ # Get plane shapes for the two non-periodic directions
36
+ planes = []
37
+ for i in range(3):
38
+ if i == self.lineindex:
39
+ continue
40
+ planes.append(box.planes[i])
41
+ planes.append(box.planes[i+3])
42
+
43
+ # Shift plane points by width in the plane normal directions
44
+ for plane in planes:
45
+ plane.point -= width * plane.normal
46
+
47
+ # Create and return shape
48
+ return PlaneSet(planes)
49
+
50
+ def cylinder_boundary(self,
51
+ box: Box,
52
+ width: float) -> Cylinder:
53
+ """
54
+ Constructs a shape associated with the cylinder-style boundary region.
55
+ Used by the monopole() generation method. The returned shape will
56
+ encompass a cylinder of atoms centered around the dislocation line
57
+ leaving a boundary region that will be at least width wide everywhere.
58
+
59
+ Parameters
60
+ ----------
61
+ box : atomman.Box
62
+ The box associated with the full (base) system.
63
+ width : float
64
+ The minimum width of the boundary region.
65
+
66
+ Returns
67
+ -------
68
+ atomman.region.Cylinder
69
+ The Shape object excluding the boundary region
70
+ """
71
+ # Reduce the problem to 2D: solution independent of ξ position
72
+ mn = np.array([self.dislsol.m, self.dislsol.n])
73
+ vect1 = mn.dot(box.vects[self.lineindex - 2])
74
+ vect2 = mn.dot(box.vects[self.lineindex - 1])
75
+ origin = mn.dot(box.origin)
76
+
77
+ # Compute normal vectors to box vectors
78
+ normal_vect1 = np.array([-vect1[1], vect1[0]])
79
+ normal_vect2 = np.array([vect2[1], -vect2[0]])
80
+ normal_vect1 = normal_vect1 / np.linalg.norm(normal_vect1)
81
+ normal_vect2 = normal_vect2 / np.linalg.norm(normal_vect2)
82
+
83
+ def line(p1, p2):
84
+ """
85
+ Defines a 2D line as used by the intersection function
86
+ """
87
+ A = (p1[1] - p2[1])
88
+ B = (p2[0] - p1[0])
89
+ C = (p1[0]*p2[1] - p2[0]*p1[1])
90
+ return A, B, -C
91
+
92
+ def intersection(L1, L2):
93
+ """
94
+ Identifies the (x,y) coordinates where two 2D lines intersect
95
+ """
96
+ D = L1[0] * L2[1] - L1[1] * L2[0]
97
+ Dx = L1[2] * L2[1] - L1[1] * L2[2]
98
+ Dy = L1[0] * L2[2] - L1[2] * L2[0]
99
+ if D != 0:
100
+ x = Dx / D
101
+ y = Dy / D
102
+ return x, y
103
+ else:
104
+ return False
105
+
106
+ # Define normal lines as originating at (0,0)
107
+ normal_line_1 = line([0,0], normal_vect1)
108
+ normal_line_2 = line([0,0], normal_vect2)
109
+
110
+ # Define boundary lines based on 2D box corners
111
+ bound_bot1 = line(origin, origin + vect1)
112
+ bound_bot2 = line(origin, origin + vect2)
113
+ bound_top1 = line(origin + vect2, origin + vect2 + vect1)
114
+ bound_top2 = line(origin + vect1, origin + vect1 + vect2)
115
+
116
+ # Identify intersection points between normal lines and the boundary lines
117
+ intersections = np.array([intersection(normal_line_1, bound_bot1),
118
+ intersection(normal_line_2, bound_bot2),
119
+ intersection(normal_line_1, bound_top1),
120
+ intersection(normal_line_2, bound_top2)])
121
+
122
+ # Find distance between (0,0) and the closest intercept
123
+ smallest = np.min(np.linalg.norm(intersections, axis=1))
124
+
125
+ # Radius = smallest distance minus the boundary width
126
+ radius = smallest - width
127
+
128
+ # Axis is along the line direction and includes point (0,0,0)
129
+ center1 = np.zeros(3)
130
+ center2 = box.vects[self.lineindex]
131
+
132
+ return Cylinder(center1, center2, radius, endcaps=False)
133
+
134
+ def monopole(self,
135
+ sizemults: Optional[tuple] = None,
136
+ amin: float = 0.0,
137
+ bmin: float = 0.0,
138
+ cmin: float = 0.0,
139
+ shift: Optional[npt.ArrayLike] = None,
140
+ shiftindex: Optional[int] = None,
141
+ shiftscale: bool = False,
142
+ center: Optional[npt.ArrayLike] = None,
143
+ centerscale: bool = False,
144
+ boundaryshape: str = 'cylinder',
145
+ boundarywidth: float = 0.0,
146
+ boundaryscale: bool = False,
147
+ return_base_system: bool = False
148
+ ) -> Union[System, Tuple[System, System]]:
149
+ """
150
+ Constructs a dislocation monopole atomic configuration containing a
151
+ single perfectly straight dislocation. The resulting system will be
152
+ periodic along the box vector direction that corresponds to the
153
+ dislocation's line direction, and non-periodic in the other two box
154
+ vector directions. Boundary atoms near the two free surfaces will be
155
+ identified by changing their atype values making it easy to identify
156
+ them later for assigning different boundary conditions.
157
+
158
+ Parameters
159
+ ----------
160
+ sizemults : tuple, optional
161
+ The size multipliers to use when generating the system. Values are
162
+ limited to being positive integers. The multipliers for the two
163
+ non-periodic directions must be even. If not given, the default
164
+ multipliers will be 2 for the non-periodic directions and 1 for the
165
+ periodic direction.
166
+ amin : float, optional
167
+ A minimum thickness to use for the a box vector direction of the
168
+ final system. Default value is 0.0. For the non-periodic
169
+ directions, the resulting vector multiplier will be even. If both
170
+ amin and sizemults is given, then the larger multiplier for the two
171
+ will be used.
172
+ bmin : float, optional
173
+ A minimum thickness to use for the b box vector direction of the
174
+ final system. Default value is 0.0. For the non-periodic
175
+ directions, the resulting vector multiplier will be even. If both
176
+ bmin and sizemults is given, then the larger multiplier for the two
177
+ will be used.
178
+ cmin : float, optional
179
+ A minimum thickness to use for the c box vector direction of the
180
+ final system. Default value is 0.0. For the non-periodic
181
+ directions, the resulting vector multiplier will be even. If both
182
+ cmin and sizemults is given, then the larger multiplier for the two
183
+ will be used.
184
+ shift : array-like object, optional
185
+ A rigid body shift to apply to the rotated cell prior to inserting
186
+ the dislocation. Should be selected such that the ideal slip plane
187
+ does not correspond to any atomic planes. Is taken as absolute if
188
+ shiftscale is False, or relative to the rotated cell's box vectors
189
+ if shiftscale is True. Cannot be given with shiftindex. If
190
+ neither shift nor shiftindex is given will use the shift set during
191
+ class initialization.
192
+ shiftindex : float, optional
193
+ The index of the identified optimum shifts based on the rotated
194
+ cell to use. Different values allow for the selection of different
195
+ atomic planes neighboring the slip plane. Note that shiftindex
196
+ values only apply shifts normal to the slip plane; best shifts for
197
+ non-planar dislocations (like bcc screw) may also need a shift in
198
+ the slip plane. Cannot be given with shiftindex. If neither shift
199
+ nor shiftindex is given then shiftindex = 0 is used then will use
200
+ the shift set during class initialization.
201
+ shiftscale : bool, optional
202
+ If False (default), a given shift value will be taken as absolute
203
+ Cartesian. If True, a given shift will be taken relative to the
204
+ rotated cell's box vectors.
205
+ center : array-like object or None, optional
206
+ Indicates where the dislocation is positioned in the system relative
207
+ to the default position at (0, 0) along the box vectors associated
208
+ with the dislocation solution's m- and n-axes.
209
+ centerscale : bool, optional
210
+ If False (default), a given center value will be taken as absolute
211
+ Cartesian. If True, a given center will be taken relative to the
212
+ rotated cell's box vectors.
213
+ boundaryshape : str, optional
214
+ Indicates the shape of the boundary region to use. Options are
215
+ 'cylinder' (default) and 'box'. For 'cylinder', the non-boundary
216
+ region is defined by a cylinder with axis along the dislocation
217
+ line and a radius that ensures the boundary is at least
218
+ boundarywidth thick. For 'box', the boundary region will be
219
+ exactly boundarywidth thick all around.
220
+ boundarywidth : float, optional
221
+ The width of the boundary region to apply. Default value is 0.0,
222
+ i.e. no boundary region. All atoms in the boundary region will
223
+ have their atype values changed.
224
+ boundaryscale : bool, optional
225
+ If False (Default), the boundarywidth will be taken as absolute.
226
+ If True, the boundarywidth will be taken relative to the magnitude
227
+ of the unit cell's a box vector.
228
+ return_base_system : bool, optional
229
+ If True then the dislocation-free base system corresponding to the
230
+ dislocation system will also be returned. The base system is used
231
+ as a reference state for most of the dislocation analysis tools.
232
+
233
+ Returns
234
+ -------
235
+ base_system : atomman.System
236
+ The base "perfect crystal" reference system associated with the
237
+ dislocation system. Only returned if return_base_system is True.
238
+ disl_system : atomman.System
239
+ The generated dislocation monopole system.
240
+ """
241
+ # Set default sizemults
242
+ if sizemults is None:
243
+ sizemults = [2,2,2]
244
+ sizemults[self.lineindex] = 1
245
+ else:
246
+ sizemults = deepcopy(sizemults)
247
+ try:
248
+ assert len(sizemults) == 3
249
+ assert isinstance(sizemults[0], int) and sizemults[0] > 0
250
+ assert isinstance(sizemults[1], int) and sizemults[1] > 0
251
+ assert isinstance(sizemults[2], int) and sizemults[2] > 0
252
+ assert sizemults[self.lineindex - 1] % 2 == 0
253
+ assert sizemults[self.lineindex - 2] % 2 == 0
254
+ except AssertionError as err:
255
+ raise TypeError('Invalid sizemults: must be 3 positive integers, and the two not along the dislocation line must be even') from err
256
+
257
+ # Adjust multipliers based on min parameters
258
+ if amin > 0.0:
259
+ amult = int(np.ceil(amin / self.rcell.box.a))
260
+ if self.lineindex != 0 and amult % 2 == 1:
261
+ amult += 1
262
+ if amult > sizemults[0]:
263
+ sizemults[0] = amult
264
+
265
+ if bmin > 0.0:
266
+ bmult = int(np.ceil(bmin / self.rcell.box.b))
267
+ if self.lineindex != 1 and bmult % 2 == 1:
268
+ bmult += 1
269
+ if bmult > sizemults[1]:
270
+ sizemults[1] = bmult
271
+
272
+ if cmin > 0.0:
273
+ cmult = int(np.ceil(cmin / self.rcell.box.c))
274
+ if self.lineindex != 2 and cmult % 2 == 1:
275
+ cmult += 1
276
+ if cmult > sizemults[2]:
277
+ sizemults[2] = cmult
278
+
279
+ # Modify the non-periodic size multipliers
280
+ sizemults[self.lineindex] = (0, sizemults[self.lineindex])
281
+ sizemults[self.lineindex - 1] = (-sizemults[self.lineindex - 1] // 2,
282
+ sizemults[self.lineindex - 1] // 2)
283
+ sizemults[self.lineindex - 2] = (-sizemults[self.lineindex - 2] // 2,
284
+ sizemults[self.lineindex - 2] // 2)
285
+
286
+ # Handle shift parameters
287
+ if shift is not None or shiftindex is not None:
288
+ self.set_shift(shift, shiftindex, shiftscale)
289
+ shift = self.shift
290
+
291
+ # Handle center parameter
292
+ if center is None:
293
+ center = np.array([0,0,0])
294
+ else:
295
+ center = np.asarray(center)
296
+ if centerscale:
297
+ center = self.rcell.box.vector_crystal_to_cartesian(center)
298
+
299
+ # Handle boundary parameters
300
+ if boundaryscale is True:
301
+ boundarywidth = boundarywidth * self.ucell.box.a
302
+ if boundaryshape not in ['cylinder', 'box']:
303
+ raise ValueError('boundaryshape must be "cylinder" or "box"')
304
+
305
+ # Create the system where the dislocation will be inserted
306
+ base_system = self.rcell.supersize(*sizemults)
307
+ base_system.atoms.pos += shift
308
+ base_system.wrap()
309
+
310
+ # Copy the system and displace atoms according to the dislocation solution
311
+ disl_system = deepcopy(base_system)
312
+ disl_system.atoms.pos += self.dislsol.displacement(disl_system.atoms.pos - center)
313
+ disl_system.pbc = [False, False, False]
314
+ disl_system.pbc[self.lineindex] = True
315
+ disl_system.wrap()
316
+
317
+ self.set_systems(base_system, disl_system)
318
+
319
+ # Apply boundary region
320
+ if boundarywidth > 0.0:
321
+
322
+ if boundaryshape == 'box':
323
+ shape = self.box_boundary(base_system.box, boundarywidth)
324
+
325
+ elif boundaryshape == 'cylinder':
326
+ shape = self.cylinder_boundary(base_system.box, boundarywidth)
327
+
328
+ # Change atypes of atoms outside box
329
+ disl_system.atoms.atype[shape.outside(disl_system.atoms.pos)] += base_system.natypes
330
+ disl_system.symbols = 2 * base_system.symbols
331
+
332
+ if return_base_system:
333
+ return base_system, disl_system
334
+ else:
335
+ return disl_system
atomman/source/atomman/defect/Dislocation/_periodicarray.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ from copy import deepcopy
4
+ from typing import Optional, Tuple, Union
5
+
6
+ # http://www.numpy.org/
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+
10
+ # atomman imports
11
+ import atomman.unitconvert as uc
12
+ from ... import Box, System
13
+ from ...region import PlaneSet
14
+
15
+ def array_boundary(self,
16
+ box: Box,
17
+ width: float) -> PlaneSet:
18
+ """
19
+ Constructs a shape associated with the boundary regions used by the
20
+ periodicarray() generation method. The returned shape will encompass
21
+ all atoms except those within width distance of the non-periodic
22
+ surface.
23
+
24
+ Parameters
25
+ ----------
26
+ box : atomman.Box
27
+ The box associated with the full (base) system.
28
+ width : float
29
+ The width of the boundary region
30
+
31
+ Returns
32
+ -------
33
+ atomman.region.PlaneSet
34
+ The Shape object excluding the boundary region
35
+ """
36
+ # Get plane shapes for the two non-periodic directions
37
+ planes = [box.planes[self.cutindex], box.planes[self.cutindex + 3]]
38
+
39
+ # Shift plane points by width in the plane normal directions
40
+ for plane in planes:
41
+ plane.point -= width * plane.normal
42
+
43
+ # Create and return shape
44
+ return PlaneSet(planes)
45
+
46
+ def periodicarray(self,
47
+ sizemults: Optional[tuple] = None,
48
+ amin: float = 0.0,
49
+ bmin: float = 0.0,
50
+ cmin: float = 0.0,
51
+ shift: Optional[npt.ArrayLike] = None,
52
+ shiftindex: Optional[int] = None,
53
+ shiftscale: bool = False,
54
+ center: Optional[npt.ArrayLike] = None,
55
+ centerscale: bool = False,
56
+ boundarywidth: float = 0.0,
57
+ boundaryscale: bool = False,
58
+ linear: bool = False,
59
+ cutoff: Optional[float] = None,
60
+ return_base_system: bool = False
61
+ ) -> Union[System, Tuple[System, System]]:
62
+ """
63
+ Constructs a dislocation monopole atomic configuration containing a
64
+ single perfectly straight dislocation. The resulting system will be
65
+ periodic along the box vector direction that corresponds to the
66
+ dislocation's line direction, and non-periodic in the other two box
67
+ vector directions. Boundary atoms near the two free surfaces will be
68
+ identified by changing their atype values making it easy to identify
69
+ them later for assigning different boundary conditions.
70
+
71
+ Parameters
72
+ ----------
73
+ sizemults : tuple, optional
74
+ The size multipliers to use when generating the system. Values
75
+ are limited to being positive integers. The multipliers for the
76
+ two non-periodic directions must be even. If not given, the
77
+ default multipliers will be 2 for the non-periodic directions and
78
+ 1 for the periodic direction.
79
+ amin : float, optional
80
+ A minimum thickness to use for the a box vector direction of the
81
+ final system. Default value is 0.0. For the non-periodic
82
+ directions, the resulting vector multiplier will be even. If both
83
+ amin and sizemults is given, then the larger multiplier for the two
84
+ will be used.
85
+ bmin : float, optional
86
+ A minimum thickness to use for the b box vector direction of the
87
+ final system. Default value is 0.0. For the non-periodic
88
+ directions, the resulting vector multiplier will be even. If both
89
+ bmin and sizemults is given, then the larger multiplier for the two
90
+ will be used.
91
+ cmin : float, optional
92
+ A minimum thickness to use for the c box vector direction of the
93
+ final system. Default value is 0.0. For the non-periodic
94
+ directions, the resulting vector multiplier will be even. If both
95
+ cmin and sizemults is given, then the larger multiplier for the two
96
+ will be used.
97
+ shift : float, optional
98
+ A rigid body shift to apply to the rotated cell prior to inserting
99
+ the dislocation. Should be selected such that the ideal slip plane
100
+ does not correspond to any atomic planes. Is taken as absolute if
101
+ shiftscale is False, or relative to the rotated cell's box vectors
102
+ if shiftscale is True. Cannot be given with shiftindex. If
103
+ neither shift nor shiftindex is given then shiftindex = 0 is used.
104
+ shiftindex : float, optional
105
+ The index of the identified optimum shifts based on the rotated
106
+ cell to use. Different values allow for the selection of different
107
+ atomic planes neighboring the slip plane. Note that shiftindex
108
+ values only apply shifts normal to the slip plane; best shifts for
109
+ non-planar dislocations (like bcc screw) may also need a shift in
110
+ the slip plane. Cannot be given with shiftindex. If neither shift
111
+ nor shiftindex is given then shiftindex = 0 is used.
112
+ shiftscale : bool, optional
113
+ If False (default), a given shift value will be taken as absolute
114
+ Cartesian. If True, a given shift will be taken relative to the
115
+ rotated cell's box vectors.
116
+ center : array-like object or None, optional
117
+ Indicates where the dislocation is positioned in the system relative
118
+ to the default position at (0, 0) along the box vectors associated
119
+ with the dislocation solution's m- and n-axes.
120
+ centerscale : bool, optional
121
+ If False (default), a given center value will be taken as absolute
122
+ Cartesian. If True, a given center will be taken relative to the
123
+ rotated cell's box vectors.
124
+ boundarywidth : float, optional
125
+ The width of the boundary region to apply. Default value is 0.0,
126
+ i.e. no boundary region. All atoms in the boundary region will
127
+ have their atype values changed and will be displaced by linear
128
+ displacements.
129
+ boundaryscale : bool, optional
130
+ If False (Default), the boundarywidth will be taken as absolute.
131
+ If True, the boundarywidth will be taken relative to the magnitude
132
+ of the unit cell's a box vector.
133
+ linear : bool, optional
134
+ If True, then only linear displacements will be used and not the
135
+ dislocation solution. Using only linear displacements is useful
136
+ for screw dislocations and dislocations with large stacking fault
137
+ distances. If False (default) then the dislocation solution will
138
+ be used for the middle displacements and linear displacements only
139
+ in the boundary region.
140
+ cutoff : float, optional
141
+ Cutoff distance to use for identifying duplicate atoms to remove.
142
+ For dislocations with an edge component, applying the displacements
143
+ creates an extra half-plane of atoms that will have (nearly)
144
+ identical positions with other atoms after altering the boundary
145
+ conditions. Default value is 0.5 Angstrom.
146
+ return_base_system : bool, optional
147
+ If True then the dislocation-free base system corresponding to the
148
+ dislocation system will also be returned. The base system is used
149
+ as a reference state for most of the dislocation analysis tools.
150
+
151
+ Returns
152
+ -------
153
+ base_system : atomman.System
154
+ The base "perfect crystal" reference system associated with the
155
+ dislocation system. If the Burgers vector has an edge component
156
+ then the atoms deleted when generating disl_system will also be
157
+ deleted from base_system. Only returned if return_base_system is
158
+ True.
159
+ disl_system : atomman.System
160
+ The generated periodic array of dislocations system.
161
+ """
162
+ # Set default sizemults
163
+ if sizemults is None:
164
+ sizemults = [2,2,2]
165
+ sizemults[self.lineindex] = 1
166
+ else:
167
+ try:
168
+ assert len(sizemults) == 3
169
+ assert isinstance(sizemults[0], int) and sizemults[0] > 0
170
+ assert isinstance(sizemults[1], int) and sizemults[1] > 0
171
+ assert isinstance(sizemults[2], int) and sizemults[2] > 0
172
+ assert sizemults[self.lineindex - 1] % 2 == 0
173
+ assert sizemults[self.lineindex - 2] % 2 == 0
174
+ except AssertionError as err:
175
+ raise TypeError('Invalid sizemults: must be 3 positive integers, and the two not along the dislocation line must be even') from err
176
+
177
+ # Adjust multipliers based on min parameters
178
+ if amin > 0.0:
179
+ amult = int(np.ceil(amin / self.rcell.box.a))
180
+ if self.lineindex != 0 and amult % 2 == 1:
181
+ amult += 1
182
+ if amult > sizemults[0]:
183
+ sizemults[0] = amult
184
+
185
+ if bmin > 0.0:
186
+ bmult = int(np.ceil(bmin / self.rcell.box.b))
187
+ if self.lineindex != 1 and bmult % 2 == 1:
188
+ bmult += 1
189
+ if bmult > sizemults[1]:
190
+ sizemults[1] = bmult
191
+
192
+ if cmin > 0.0:
193
+ cmult = int(np.ceil(cmin / self.rcell.box.c))
194
+ if self.lineindex != 2 and cmult % 2 == 1:
195
+ cmult += 1
196
+ if cmult > sizemults[2]:
197
+ sizemults[2] = cmult
198
+
199
+ # Modify the non-periodic size multipliers
200
+ sizemults[self.lineindex] = (0, sizemults[self.lineindex])
201
+ sizemults[self.lineindex - 1] = (-sizemults[self.lineindex - 1] // 2,
202
+ sizemults[self.lineindex - 1] // 2)
203
+ sizemults[self.lineindex - 2] = (-sizemults[self.lineindex - 2] // 2,
204
+ sizemults[self.lineindex - 2] // 2)
205
+
206
+ # Handle shift parameters
207
+ if shift is not None or shiftindex is not None:
208
+ self.set_shift(shift, shiftindex, shiftscale)
209
+ shift = self.shift
210
+
211
+ # Handle center parameter
212
+ if center is None:
213
+ center = np.array([0,0,0])
214
+ else:
215
+ center = np.asarray(center)
216
+ if centerscale:
217
+ center = self.rcell.box.vector_crystal_to_cartesian(center)
218
+
219
+ # Handle boundary parameters
220
+ if boundaryscale is True:
221
+ boundarywidth = boundarywidth * self.ucell.box.a
222
+
223
+ # Create the system where the dislocation will be inserted
224
+ base_system = self.rcell.supersize(*sizemults)
225
+ base_system.atoms.pos += shift
226
+ base_system.wrap()
227
+
228
+ # Create the dislocation system
229
+ disl_system = self.build_disl_array(base_system, center, linear=linear,
230
+ bwidth=boundarywidth, cutoff=cutoff)
231
+ # Trim deleted atoms from base_system
232
+ base_system = base_system.atoms_ix[disl_system.atoms.old_id]
233
+
234
+ # Apply boundary region
235
+ if boundarywidth > 0.0:
236
+
237
+ shape = self.array_boundary(base_system.box, boundarywidth)
238
+
239
+ # Change atypes of atoms outside box
240
+ disl_system.atoms.atype[shape.outside(disl_system.atoms.pos)] += base_system.natypes
241
+ disl_system.symbols = 2 * base_system.symbols
242
+
243
+ self.set_systems(base_system, disl_system)
244
+
245
+ if return_base_system:
246
+ return base_system, disl_system
247
+ else:
248
+ return disl_system
249
+
250
+
251
+ def build_disl_array(self,
252
+ base_system: System,
253
+ center: npt.ArrayLike,
254
+ linear: bool = False,
255
+ bwidth: Optional[float] = None,
256
+ cutoff: Optional[float] = None,
257
+ ) -> System:
258
+ """
259
+ Method that converts a bulk crystal system into a periodic array of
260
+ dislocations. A single dislocation is inserted using a dislocation
261
+ solution. The system's box and pbc are altered such that the system is
262
+ periodic and compatible across the two box vectors contained in the slip
263
+ plane. The third box vector is non-periodic, resulting in free surfaces
264
+ parallel to the dislocation's slip plane.
265
+
266
+ Parameters
267
+ ----------
268
+ base_system : atomman.System
269
+ A perfect, bulk atomic system.
270
+ dislsol : atomman.defect.VolterraDislocation, optional
271
+ A dislocation solution to use to displace atoms by. If not given,
272
+ all atoms will be given linear displacements associated with the
273
+ long-range limits.
274
+ m : array-like object, optional
275
+ The dislocation solution m unit vector. This vector is in the slip
276
+ plane and perpendicular to the dislocation line direction. Only needed
277
+ if dislsol is not given.
278
+ n : array-like object, optional
279
+ The dislocation solution n unit vector. This vector is normal to the
280
+ slip plane. Only needed if dislsol is not given.
281
+ burgers : array-like object, optional
282
+ The Cartesian Burger's vector for the dislocation relative to the
283
+ given system's Cartesian coordinates. Only needed if dislsol is not
284
+ given.
285
+ bwidth : float, optional
286
+ The width of the boundary region at the free surfaces. Atoms within
287
+ the boundaries will be displaced by linear displacements instead of
288
+ by the dislocation solution. Only given if dislsol is not None.
289
+ Default value if dislsol is given is 10 Angstroms.
290
+ cutoff : float, optional
291
+ Cutoff distance to use for identifying duplicate atoms to remove.
292
+ For dislocations with an edge component, applying the displacements
293
+ creates an extra half-plane of atoms that will have (nearly) identical
294
+ positions with other atoms after altering the boundary conditions.
295
+ Default cutoff value is 0.5 Angstrom.
296
+
297
+ Returns
298
+ -------
299
+ atomman.System
300
+ The resulting periodic array of dislocations system. An additional
301
+ atoms property 'old_id' will be added to map the atoms in the defect
302
+ system back to the associated atoms in the original system.
303
+ """
304
+
305
+ # ------------------------ Parameter handling --------------------------- #
306
+
307
+ # Set default values
308
+ if bwidth is None:
309
+ bwidth = uc.set_in_units(10, 'angstrom')
310
+ if cutoff is None:
311
+ cutoff = uc.set_in_units(0.5, 'angstrom')
312
+
313
+ # Extract dislocation solution values
314
+ m = self.dislsol.m
315
+ n = self.dislsol.n
316
+ burgers = self.dislsol.burgers
317
+
318
+ # Extract system values
319
+ pos = base_system.atoms.pos
320
+ vects = base_system.box.vects
321
+ spos = base_system.atoms_prop(key='pos', scale=True)
322
+
323
+ # Extract orientation values
324
+ lineindex = self.lineindex
325
+ cutindex = self.cutindex
326
+ motionindex = self.motionindex
327
+
328
+ # Check for atoms exactly on the slip plane
329
+ if np.isclose(spos[:, cutindex], 0.5, rtol=0).sum() > 0:
330
+ raise ValueError("atom positions found on slip plane: apply a coordinate shift")
331
+
332
+ # ---------------------- Boundary modification -------------------------- #
333
+
334
+ # Modify box vector in the motion direction by +- burgers/2
335
+ newvects = deepcopy(vects)
336
+ if burgers.dot(m) > 0:
337
+ newvects[motionindex] -= burgers / 2
338
+ else:
339
+ newvects[motionindex] += burgers / 2
340
+ newbox = Box(vects=newvects, origin=base_system.box.origin)
341
+
342
+ # Make boundary condition perpendicular to slip plane non-periodic
343
+ newpbc = [True, True, True]
344
+ newpbc[cutindex] = False
345
+
346
+ # Get length of system along motionindex
347
+ # WHICH IS BEST!?!?!?
348
+ length = np.abs(vects[motionindex].dot(m))
349
+ #length = np.linalg.norm(newvects[motionindex])
350
+
351
+ # -------------------- duplicate atom identification -------------------- #
352
+
353
+ # Create test system to identify "duplicate" atoms
354
+ testsystem = System(atoms=deepcopy(base_system.atoms), box=newbox,
355
+ pbc=newpbc, symbols=base_system.symbols)
356
+
357
+ # Apply linear gradient shift to all atoms
358
+ testsystem.atoms.pos += linear_displacement(pos - center, burgers, length, m, n)
359
+ testsystem.atoms.old_id = range(testsystem.natoms)
360
+
361
+ # Identify atoms at the motionindex boundary to include in the duplicate check
362
+ spos = testsystem.atoms_prop(key='pos', scale=True)
363
+ sburgers = np.abs(2 * burgers[motionindex] / (length))
364
+ boundaryatoms = testsystem.atoms[ (spos[:, motionindex] < sburgers)
365
+ | (spos[:, motionindex] > 1.0 - sburgers) ]
366
+
367
+ # Compare distances between boundary atoms to identify duplicates
368
+ dup_atom_ids = []
369
+ for ni, i in enumerate(boundaryatoms.old_id[:-1]):
370
+ js = boundaryatoms.old_id[ni+1:]
371
+ try:
372
+ distances = np.linalg.norm(testsystem.dvect(i, js), axis=1)
373
+ mindistance = distances.min()
374
+ except:
375
+ mindistance = np.linalg.norm(testsystem.dvect(i, js))
376
+ if mindistance < cutoff:
377
+ dup_atom_ids.append(i)
378
+ ii = np.ones(base_system.natoms, dtype=bool)
379
+ ii[dup_atom_ids] = False
380
+
381
+ # Count found duplicate atoms
382
+ found = base_system.natoms - ii.sum()
383
+
384
+ # Count expected number of duplicates based on volume change
385
+ expected = base_system.natoms - (base_system.natoms * newbox.volume / base_system.box.volume)
386
+ if np.isclose(expected, round(expected)):
387
+ expected = int(round(expected))
388
+ else:
389
+ raise ValueError('expected number of atoms to delete not an integer: check burgers vector')
390
+
391
+ # Compare found versus expected number of atoms
392
+ if found != expected:
393
+ raise ValueError('Deleted atom mismatch: expected %i, found %i. Adjust system dimensions and/or cutoff' %(expected, found))
394
+
395
+ # ---------------------- Build dislocation system ----------------------- #
396
+
397
+ # Generate new system with duplicate atoms removed
398
+ disl_system = System(atoms=base_system.atoms[ii], box=newbox, pbc=newpbc,
399
+ symbols=base_system.symbols)
400
+
401
+ # Define old_id so atoms in newsystem can be mapped back to system
402
+ disl_system.atoms.old_id = np.where(ii)[0]
403
+
404
+ if linear:
405
+ # Use only linear displacements
406
+ disp = linear_displacement(disl_system.atoms.pos - center, burgers, length, m, n)
407
+
408
+ else:
409
+ # Identify boundary atoms
410
+ miny = base_system.box.origin.dot(n)
411
+ maxy = miny + vects[cutindex].dot(n)
412
+ if maxy < miny:
413
+ miny, maxy = maxy, miny
414
+ y = disl_system.atoms.pos.dot(n)
415
+ ii = np.where((y <= miny + bwidth) | (y >= maxy - bwidth))
416
+
417
+ # Use dislsol in middle and linear displacements at boundary
418
+ disp = self.dislsol.displacement(disl_system.atoms.pos - center)
419
+ disp[:, cutindex] -= disp[:, cutindex].mean()
420
+ disp[ii] = linear_displacement(disl_system.atoms.pos[ii] - center, burgers,
421
+ length, m, n)
422
+
423
+ # Displace atoms and wrap
424
+ disl_system.atoms.pos += disp
425
+ disl_system.wrap()
426
+
427
+ return disl_system
428
+
429
+ def linear_displacement(pos, burgers, length, m, n):
430
+ """
431
+ Computes linear displacements associated with a dislocation in a system.
432
+
433
+ Parameters
434
+ ----------
435
+ pos : array-like object
436
+ List of Cartesian atomic positions.
437
+ burgers : array-like object
438
+ The Cartesian Burgers vector
439
+ length : float
440
+ The total length of the system along the m direction.
441
+ m : array-like object, optional
442
+ The dislocation solution m unit vector. This vector is in the slip
443
+ plane and perpendicular to the dislocation line direction. Only needed
444
+ if dislsol is not given.
445
+ n : array-like object, optional
446
+ The dislocation solution n unit vector. This vector is normal to the
447
+ slip plane. Only needed if dislsol is not given.
448
+ """
449
+
450
+ return np.outer(np.sign(pos.dot(n)) * (0.25 - pos.dot(m) / (2 * length)), burgers)
atomman/source/atomman/defect/FreeSurface.py ADDED
@@ -0,0 +1,634 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ import io
4
+ from itertools import product
5
+ from typing import Optional, Tuple, Union
6
+
7
+ # https://github.com/usnistgov/DataModelDict
8
+ from DataModelDict import DataModelDict as DM
9
+
10
+ # http://www.numpy.org/
11
+ import numpy as np
12
+ import numpy.typing as npt
13
+
14
+ from yabadaba.record import Record
15
+
16
+ from . import free_surface_basis
17
+ from ..tools import miller
18
+ from ..region import Plane
19
+ from .. import System, load
20
+ from ..library import load_record, Database
21
+
22
+ try:
23
+ import spglib
24
+ has_spglib = True
25
+ except ImportError:
26
+ has_spglib = False
27
+
28
+ class FreeSurface():
29
+ """
30
+ Class for generating free surface atomic configurations using clean planar slices.
31
+ """
32
+
33
+ def __init__(self,
34
+ hkl: npt.ArrayLike,
35
+ ucell: System,
36
+ cutboxvector: str = 'c',
37
+ maxindex: Optional[int] = None,
38
+ conventional_setting: str = 'p',
39
+ shift: Optional[npt.ArrayLike] = None,
40
+ shiftindex: Optional[int] = None,
41
+ shiftscale: bool = False,
42
+ tol: float = 1e-7):
43
+ """
44
+ Class initializer. Identifies the proper rotations for the given hkl plane
45
+ and cutboxvector, and creates the rotated cell.
46
+
47
+ Parameters
48
+ ----------
49
+ hkl : array-like object
50
+ The free surface plane to generate expressed in either 3 indices
51
+ Miller (hkl) format or 4 indices Miller-Bravais (hkil) format.
52
+ ucell : atomman.System
53
+ The unit cell to use in generating the system.
54
+ cutboxvector : str, optional
55
+ Specifies which of the three box vectors corresponds to the
56
+ out-of-plane vector. Default value is c.
57
+ maxindex : int, optional
58
+ Max uvw index value to use in identifying the best uvw set for the
59
+ out-of-plane vector. If not given, will use the largest absolute
60
+ index between the given hkl and the initial in-plane vector guesses.
61
+ conventional_setting : str, optional
62
+ Indicates the space lattice setting of the given unit cell, i.e.
63
+ 'p' for primitive, 'i' for body-centered, 'f' for face-centered,
64
+ 'a', 'b', or 'c' for side-centered and 't1', or 't2' for trigonal
65
+ in a hexagonal setting. Setting this with the appropriate
66
+ conventional unit cell allows for identifying lattice vectors that
67
+ are not integers with respect to the conventional unit cell. This
68
+ also creates the rotated cell from a compatible primitive cell,
69
+ thereby the final dislocation configurations can be smaller than
70
+ possible solely from the conventional unit cell.
71
+ shift : array-like object, optional
72
+ Applies a shift to all atoms. Different values allow for free surfaces with
73
+ different termination planes to be selected. shift is taken as absolute
74
+ if shiftscale is False, or relative to the rotated cell's box vectors
75
+ if shiftscale is True. Cannot be given with shiftindex. If
76
+ neither shift nor shiftindex is given then shiftindex = 0 is used.
77
+ shiftindex : float, optional
78
+ The index of the identified shifts based on the rotated
79
+ cell to use. Different values allow for the selection of different
80
+ atomic planes neighboring the slip plane. Cannot be given with shift.
81
+ If neither shift nor shiftindex is given then shiftindex = 0 is used.
82
+ shiftscale : bool, optional
83
+ If False (default), a given shift value will be taken as absolute
84
+ Cartesian. If True, a given shift will be taken relative to the
85
+ rotated cell's box vectors.
86
+ tol : float, optional
87
+ Tolerance parameter used to round off near-zero values. Default
88
+ value is 1e-8.
89
+ """
90
+
91
+ # Pass parameters to free_surface_basis to get the rotation uvws
92
+ uvws = free_surface_basis(hkl, box=ucell.box, cutboxvector=cutboxvector, maxindex=maxindex,
93
+ conventional_setting=conventional_setting)
94
+
95
+ # Generate the rotated cell
96
+ # rcell.box.vects == uvws @ ucell.box.vects @ transform.T
97
+ rcell, transform = ucell.rotate(uvws, return_transform=True)
98
+
99
+ # Transform uvws to the conventional cell representation
100
+ if uvws.shape == (3,3):
101
+ uvws = miller.vector_primitive_to_conventional(uvws, conventional_setting)
102
+
103
+ # Set cutindex and rcellwidth based on cutboxvector
104
+ if cutboxvector == 'a':
105
+ if rcell.box.bvect[0] != 0.0 or rcell.box.cvect[0] != 0.0:
106
+ raise ValueError("box bvect and cvect cannot have x component for cutboxvector='a'")
107
+ cutindex = 0
108
+
109
+ elif cutboxvector == 'b':
110
+ if rcell.box.avect[1] != 0.0 or rcell.box.cvect[1] != 0.0:
111
+ raise ValueError("box avect and cvect cannot have y component for cutboxvector='b'")
112
+ cutindex = 1
113
+
114
+ elif cutboxvector == 'c':
115
+ if rcell.box.avect[2] != 0.0 or rcell.box.bvect[2] != 0.0:
116
+ raise ValueError("box avect and bvect cannot have z component for cutboxvector='c'")
117
+ cutindex = 2
118
+
119
+ # Define out of plane unit vector
120
+ ovect = np.zeros(3)
121
+ ovect[cutindex] = 1.0
122
+
123
+ # Get out of plane width
124
+ rcellwidth = rcell.box.vects[cutindex, cutindex]
125
+
126
+ # Get the unique coordinates normal to the plane
127
+ pos = rcell.atoms.pos
128
+ numdec = - int(np.floor(np.log10(tol)))
129
+ _, unique_indices = np.unique(pos[:, cutindex].round(numdec), return_index=True)
130
+ coords = pos[unique_indices, cutindex]
131
+
132
+ # Add periodic replica if missing
133
+ if not np.isclose(coords[-1] - coords[0], rcellwidth, rtol=0.0, atol=tol):
134
+ coords = np.append(coords, coords[0] + rcellwidth)
135
+
136
+ # Compute the shifts
137
+ relshifts = rcellwidth - (coords[1:] + coords[:-1]) / 2
138
+ relshifts[relshifts > rcellwidth] -= rcellwidth
139
+ relshifts[relshifts < 0.0] += rcellwidth
140
+ shifts = np.outer(np.sort(relshifts), ovect)
141
+
142
+ # Save attributes
143
+ self.__hkl = np.asarray(hkl)
144
+ self.__ucell = ucell
145
+ self.__rcell = rcell
146
+ self.__cutboxvector = cutboxvector
147
+ self.__cutindex = cutindex
148
+ self.__uvws = uvws
149
+ self.__rcellwidth = rcellwidth
150
+ self.__shifts = shifts
151
+ self.__transform = transform
152
+ self.__conventional_setting = conventional_setting
153
+ self.__system = None
154
+ self.__surfacearea = None
155
+
156
+ # Set shift
157
+ self.set_shift(shift=shift, shiftindex=shiftindex, shiftscale=shiftscale)
158
+
159
+ @classmethod
160
+ def fromrecord(cls,
161
+ record: Union[str, io.IOBase, DM, Record],
162
+ ucell: Union[System, str, io.IOBase],
163
+ maxindex: Optional[int] = None,
164
+ tol: float = 1e-7):
165
+ """
166
+ Construct a FreeSurface object based on parameters in a free_surface
167
+ record and unit cell information.
168
+
169
+ Parameters
170
+ ----------
171
+ record : atomman.library.record.FreeSurface, str, file-like object or DataModelDict
172
+ A FreeSurface record object or the model contents for one.
173
+ ucell : atomman.System
174
+ The unit cell to use in generating the system.
175
+ maxindex : int, optional
176
+ Max uvw index value to use in identifying the best uvw set for the
177
+ out-of-plane vector. If not given, will use the largest absolute
178
+ index between the given hkl and the initial in-plane vector guesses.
179
+ tol : float, optional
180
+ Tolerance parameter used to round off near-zero values. Default
181
+ value is 1e-8.
182
+ """
183
+ # Create record object if needed
184
+ if not isinstance(record, Record):
185
+ record = load_record('free_surface', model=record)
186
+
187
+ # Extract parameters in the record
188
+ hkl = miller.fromstring(record.parameters['hkl'])
189
+ shiftindex = int(record.parameters.get('shiftindex', 0))
190
+ cutboxvector = record.parameters['cutboxvector']
191
+ conventional_setting = record.parameters.get('conventional_setting', 'p')
192
+
193
+ return cls(hkl=hkl, ucell=ucell, cutboxvector=cutboxvector,
194
+ maxindex=maxindex, shiftindex=shiftindex,
195
+ conventional_setting=conventional_setting, tol=tol)
196
+
197
+ @classmethod
198
+ def fromdatabase(cls,
199
+ name: Optional[str] = None,
200
+ ucell: Optional[System] = None,
201
+ database: Optional[Database] = None,
202
+ prompt: bool = True,
203
+ maxindex: Optional[int] = None,
204
+ tol: float = 1e-7,
205
+ **kwargs):
206
+ """
207
+ Construct a FreeSurface object based on record(s) retrieved from the
208
+ reference database.
209
+
210
+ Parameters
211
+ ----------
212
+ name : str or None, optional
213
+ The name of the free_surface record to retrieve from the database.
214
+ Alternatively, you can use any other query keyword arguments supported
215
+ by the free_surface record style (see **kwargs below for more info).
216
+ ucell : atomman.System or None, optional
217
+ The unit cell to use in generating the system. If None (default), then
218
+ the crystal_prototype record that matches the defect's family setting
219
+ will be loaded from the database. Note that if None then the
220
+ crystal-specific info (lattice constants and symbols) should be given
221
+ here as kwargs (see below).
222
+ database : atomman.library.Database or None, optional
223
+ A Database object to use to fetch the records. If None (default), then
224
+ a new Database instance will be created.
225
+ prompt : bool
226
+ If prompt=True (default) then a screen input will ask for a selection
227
+ if multiple matching free_surface (or crystal_prototype) records are
228
+ found. If prompt=False, then an error will be thrown if multiple
229
+ matches are found.
230
+ maxindex : int, optional
231
+ Max uvw index value to use in identifying the best uvw set for the
232
+ out-of-plane vector. If not given, will use the largest absolute
233
+ index between the given hkl and the initial in-plane vector guesses.
234
+ tol : float, optional
235
+ Tolerance parameter used to round off near-zero values. Default
236
+ value is 1e-8.
237
+ **kwargs : any
238
+ The recognized kwargs include the query keywords for free_surface
239
+ records (key, id, family, hkl, shiftindex, cutboxvector), and the
240
+ crystal-specific parameters recognized by the prototype load style
241
+ (a, b, c, alpha, beta, gamma, symbols). The non-trivial
242
+ crystal-specific parameters should be for the crystal if ucell is
243
+ not given above as the crystal prototype lacks this information.
244
+ """
245
+ # Initialize a database if needed
246
+ if database is None:
247
+ database = Database()
248
+
249
+ # Extract ucell parameters from kwargs
250
+ prototype_kwargs = {}
251
+ prototype_kwargs_names = ['a', 'b', 'c', 'alpha', 'beta', 'gamma', 'symbols']
252
+ for prototype_kwargs_name in prototype_kwargs_names:
253
+ if prototype_kwargs_name in kwargs:
254
+ prototype_kwargs[prototype_kwargs_name] = kwargs.pop(prototype_kwargs_name)
255
+
256
+ # Fetch matching defect record
257
+ record = database.get_record('free_surface', name=name, prompt=prompt, **kwargs)
258
+
259
+ # Fetch crystal prototype unit cell if needed
260
+ if ucell is None:
261
+ ucell = load('prototype', name=record.family, **prototype_kwargs)
262
+ else:
263
+ if len(prototype_kwargs) > 0:
264
+ raise ValueError('crystal-specific kwargs cannot be given with ucell')
265
+
266
+ return cls.fromrecord(record=record, ucell=ucell, maxindex=maxindex, tol=tol)
267
+
268
+ @property
269
+ def hkl(self) -> np.ndarray:
270
+ """numpy.ndarray : Crystal plane in Miller or Miller-Bravais indices"""
271
+ return self.__hkl
272
+
273
+ @property
274
+ def ucell(self) -> System:
275
+ """atomman.System : The unit cell to use in building the defect system."""
276
+ return self.__ucell
277
+
278
+ @property
279
+ def rcell(self) -> System:
280
+ """atomman.System : the rotated cell to use in building the defect system."""
281
+ return self.__rcell
282
+
283
+ @property
284
+ def cutboxvector(self) -> str:
285
+ """str : The box vector for the cut direction."""
286
+ return self.__cutboxvector
287
+
288
+ @property
289
+ def cutindex(self) -> int:
290
+ """int : The Cartesian index for the cut direction."""
291
+ return self.__cutindex
292
+
293
+ @property
294
+ def uvws(self) -> np.ndarray:
295
+ """
296
+ numpy.ndarray : The conventional Miller or Miller-Bravais crystal
297
+ vectors associated with the rcell box vectors.
298
+ """
299
+ return self.__uvws
300
+
301
+ @property
302
+ def rcellwidth(self) -> float:
303
+ """float : The width of rcell in the cutindex direction."""
304
+ return self.__rcellwidth
305
+
306
+ @property
307
+ def shifts(self) -> list:
308
+ """list : All shift values that place the fault halfway between atomic layers in rcell."""
309
+ return self.__shifts
310
+
311
+ @property
312
+ def shift(self) -> np.ndarray:
313
+ """
314
+ numpy.NDArray : The particular shift value that will be or was used to
315
+ construct the defect system
316
+ """
317
+ return self.__shift
318
+
319
+ @property
320
+ def system(self) -> System:
321
+ """atomman.System : The built free surface system."""
322
+ if self.__system is not None:
323
+ return self.__system
324
+ else:
325
+ raise AttributeError('system not yet built. Use build_system() or surface().')
326
+
327
+ @property
328
+ def surfacearea(self) -> float:
329
+ """float : The surface area of one of the hkl planes."""
330
+ if self.__surfacearea is not None:
331
+ return self.__surfacearea
332
+ else:
333
+ raise AttributeError('system not yet built. Use build_system() or surface().')
334
+
335
+ @property
336
+ def transform(self) -> np.ndarray:
337
+ """numpy.ndarray : The Cartesian transformation tensor associated with rotating from ucell to rcell"""
338
+ return self.__transform
339
+
340
+ @property
341
+ def conventional_setting(self) -> str:
342
+ """str : The lattice setting/centering associated with the conventional cell (used if ucell is primitive)"""
343
+ return self.__conventional_setting
344
+
345
+ def surface(self,
346
+ shift: Optional[npt.ArrayLike] = None,
347
+ shiftindex: Optional[int] = None,
348
+ shiftscale: bool = None,
349
+ vacuumwidth: Optional[float] = None,
350
+ minwidth: Optional[float] = None,
351
+ sizemults: Optional[list] = None,
352
+ even: bool = False) -> System:
353
+ """
354
+ Generates and returns a free surface atomic system.
355
+
356
+ Parameters
357
+ ----------
358
+ shift : array-like object, optional
359
+ Applies a shift to all atoms. Different values allow for free surfaces with
360
+ different termination planes to be selected. shift is taken as absolute
361
+ if shiftscale is False, or relative to the rotated cell's box vectors
362
+ if shiftscale is True. Cannot be given with shiftindex. If
363
+ neither shift nor shiftindex is given then the current value set to the
364
+ shift attribute will be used.
365
+ shiftindex : float, optional
366
+ The index of the identified shifts based on the rotated
367
+ cell to use. Different values allow for the selection of different
368
+ atomic planes neighboring the slip plane. Cannot be given with shift.
369
+ If neither shift nor shiftindex is given then the current value set to
370
+ the shift attribute will be used.
371
+ shiftscale : bool, optional
372
+ If False (default), a given shift value will be taken as absolute
373
+ Cartesian. If True, a given shift will be taken relative to the
374
+ rotated cell's box vectors.
375
+ vacuumwidth : float, optional
376
+ If given, the free surface is created by modifying the system's box to insert
377
+ a region of vacuum with this width. This is typically used for DFT calculations
378
+ where it is computationally preferable to insert a vacuum region and keep all
379
+ dimensions periodic.
380
+ sizemults : list or tuple, optional
381
+ The three System.supersize multipliers [a_mult, b_mult, c_mult] to use on the
382
+ rotated cell to build the final system. Note that the cutboxvector sizemult
383
+ must be an integer and not a tuple. Default value is [1, 1, 1].
384
+ minwidth : float, optional
385
+ If given, the sizemult along the cutboxvector will be selected such that the
386
+ width of the resulting final system in that direction will be at least this
387
+ value. If both sizemults and minwidth are given, then the larger of the two
388
+ in the cutboxvector direction will be used.
389
+ even : bool, optional
390
+ A True value means that the sizemult for cutboxvector will be made an even
391
+ number by adding 1 if it is odd. Default value is False.
392
+
393
+ Returns
394
+ -------
395
+ atomman.System
396
+ The free surface atomic system.
397
+ """
398
+
399
+ # Set default function values
400
+ if shift is not None or shiftindex is not None:
401
+ self.set_shift(shift=shift, shiftindex=shiftindex, shiftscale=shiftscale)
402
+ shift = self.shift
403
+
404
+ if sizemults is None:
405
+ sizemults = [1, 1, 1]
406
+
407
+ # Handle minwidth
408
+ if minwidth is not None:
409
+ mult = int(np.ceil(minwidth / self.rcellwidth))
410
+
411
+ sizemult = sizemults[self.cutindex]
412
+ if mult > np.abs(sizemult):
413
+ sizemults[self.cutindex] = np.sign(sizemult) * mult
414
+
415
+ # Handle even
416
+ if even and sizemults[self.cutindex] % 2 == 1:
417
+ if sizemults[self.cutindex] > 0:
418
+ sizemults[self.cutindex] += 1
419
+ else:
420
+ sizemults[self.cutindex] -= 1
421
+
422
+ # Define out of plane unit vector
423
+ ovect = np.zeros(3)
424
+ ovect[self.cutindex] = 1.0
425
+
426
+ # Supersize and shift the system
427
+ system = self.rcell.supersize(*sizemults)
428
+ system.atoms.pos += shift
429
+ system.wrap()
430
+
431
+ # Change system's pbc
432
+ system.pbc = [True, True, True]
433
+ system.pbc[self.cutindex] = False
434
+
435
+ # Insert vacuumwidth
436
+ if vacuumwidth is not None:
437
+ if vacuumwidth < 0:
438
+ raise ValueError('vacuumwidth must be positive')
439
+ newvects = system.box.vects
440
+ newvects[self.cutindex, self.cutindex] += vacuumwidth
441
+ neworigin = system.box.origin - ovect * vacuumwidth / 2
442
+ system.box_set(vects=newvects, origin=neworigin)
443
+
444
+ # Compute surfacearea based on cutboxvector
445
+ if self.cutboxvector == 'a':
446
+ surfacearea = np.linalg.norm(np.cross(system.box.bvect, system.box.cvect))
447
+
448
+ elif self.cutboxvector == 'b':
449
+ surfacearea = np.linalg.norm(np.cross(system.box.avect, system.box.cvect))
450
+
451
+ elif self.cutboxvector == 'c':
452
+ surfacearea = np.linalg.norm(np.cross(system.box.avect, system.box.bvect))
453
+
454
+ # Save attributes
455
+ self.__system = system
456
+ self.__surfacearea = surfacearea
457
+
458
+ return self.system
459
+
460
+ def unique_shifts(self,
461
+ symprec: float = 1e-5,
462
+ trial_image_range: int = 1,
463
+ atol: float = 1e-8,
464
+ return_indices: bool = False
465
+ ) -> Union[np.ndarray, Tuple[np.ndarray, list]]:
466
+ """
467
+ Use crystal symmetry operations to filter the list of shift values to
468
+ only those that are symmetrically unique. Note that the identified
469
+ unique shifts can still result in the creation of energetically
470
+ equivalent free surfaces if the free surface introduces a symmetry
471
+ operation not present in the bulk crystal.
472
+
473
+ Parameters
474
+ ----------
475
+ symprec: float
476
+ The symmetry precision tolerance value used in spglib.
477
+ trial_image_range: int, more than or equal to 1.
478
+ Maximum cell images searched in finding translationally equivalent
479
+ planes. The default value is one, which corresponds to search the
480
+ 27 neighbor images, [-1, 1]^3. The default value may not be
481
+ sufficient for largely distorted lattice.
482
+ atol: float
483
+ The absolute tolerance used in comparing two crystal planes.
484
+ return_indices: bool
485
+ If True then the indices of shift that correspond to the
486
+ identified unique shifts will be returned as well. Default value
487
+ is False (only return the shift vectors).
488
+
489
+ Returns
490
+ -------
491
+ unique_shifts: np.ndarray, (# of unique shifts, 3)
492
+ The symmetrically unique shift vectors.
493
+ unique_indices: list
494
+ The indices of shifts that correspond to the identified unique
495
+ shifts.
496
+ """
497
+ if not has_spglib:
498
+ raise ImportError("FreeSurface.unique_shifts requires spglib. Use `pip install spglib`")
499
+ if (not isinstance(trial_image_range, int)) or (trial_image_range <= 0):
500
+ raise ValueError("trial_image_range should be positive integer.")
501
+
502
+ # Planes for each shift
503
+ normal = np.zeros(3)
504
+ normal[self.cutindex] = 1.0
505
+ planes = [Plane(normal, shift) for shift in self.shifts]
506
+
507
+ # Get symmetry operations of rotated ucell
508
+ vects, positions, numbers = self.ucell.dump('spglib_cell')
509
+ rotated_vects = np.dot(vects, self.transform.T)
510
+ dataset = spglib.get_symmetry_dataset((rotated_vects, positions, numbers), symprec=symprec)
511
+
512
+ # Compatibility fix for newspglib
513
+ if hasattr(dataset, 'rotations'):
514
+ dataset = {'rotations': dataset.rotations,
515
+ 'translations': dataset.translations,
516
+ 'primitive_lattice': dataset.primitive_lattice}
517
+
518
+ # Convert operations to Cartesian
519
+ operations = []
520
+ vects_tinv = np.linalg.inv(rotated_vects.T)
521
+ for rotation, translation in zip(dataset['rotations'], dataset['translations']):
522
+ rotation_cart = np.dot(np.dot(rotated_vects.T, rotation), vects_tinv)
523
+ translation_cart = np.inner(translation, rotated_vects.T)
524
+
525
+ # It is sufficient to consider only symmetry operation that preserve the normal vector.
526
+ if np.allclose(np.dot(rotation_cart, normal), normal):
527
+ operations.append((rotation_cart, translation_cart))
528
+
529
+ unique_shifts = []
530
+ unique_indices = []
531
+
532
+ # Use primitive vectors for the search if available
533
+ try:
534
+ primitive_vects = dataset['primitive_lattice']
535
+ except KeyError:
536
+ primitive_vects = rotated_vects
537
+
538
+ # List of trial displacements to search for a translation between two planes
539
+ # The range [-1, 1] may not be sufficient for largely distorted lattice.
540
+ trial_images = list(product(range(-trial_image_range, trial_image_range + 1), repeat=3))
541
+
542
+ def is_equivalent_by_primitive_vects(plane1, plane2):
543
+ # Check if two planes can be transformed to each other by lattice vectors
544
+ for image in trial_images:
545
+ rotation = np.eye(3)
546
+ translation = np.inner(image, primitive_vects.T)
547
+ new_plane1 = plane1.operate(rotation, translation)
548
+ if new_plane1.isclose(plane2, atol=atol):
549
+ return True
550
+ return False
551
+
552
+ for i in range(len(self.shifts)-1, -1, -1):
553
+ plane_i = planes[i]
554
+
555
+ # Obtain symmetrically equivalent planes with the i-th plane
556
+ equivalent_planes = []
557
+ for rotation, translation in operations:
558
+ new_plane = plane_i.operate(rotation, translation)
559
+
560
+ # If the new plane is already found, skip it.
561
+ skip = False
562
+ for old_plane in equivalent_planes:
563
+ if new_plane.isclose(old_plane, atol=atol):
564
+ skip = True
565
+ continue
566
+ if skip:
567
+ continue
568
+ equivalent_planes.append(new_plane)
569
+
570
+ # Compare with remained shifts
571
+ is_unique = True
572
+ for j in range(i-1, -1, -1):
573
+ plane_j = planes[j]
574
+ # Here, the two planes have the normal vector.
575
+ for plane in equivalent_planes:
576
+ if is_equivalent_by_primitive_vects(plane, plane_j):
577
+ is_unique = False
578
+ break
579
+
580
+ if not is_unique:
581
+ break
582
+
583
+ if is_unique:
584
+ unique_indices.append(i)
585
+
586
+ unique_indices.sort()
587
+ unique_shifts = self.shifts[unique_indices]
588
+
589
+ if return_indices is True:
590
+ return unique_shifts, unique_indices
591
+ return unique_shifts
592
+
593
+ def set_shift(self,
594
+ shift: Optional[npt.ArrayLike] = None,
595
+ shiftindex: Optional[int] = None,
596
+ shiftscale: bool = False):
597
+ """
598
+ Directly set the shift value based on shiftindex, or shift and shiftscale.
599
+ NOTE that the shift value can alternatively be set during class initialization
600
+ or when surface() is called.
601
+
602
+ Parameters
603
+ ----------
604
+ shift : array-like object, optional
605
+ Applies a shift to all atoms. Different values allow for free surfaces with
606
+ different termination planes to be selected. shift is taken as absolute
607
+ if shiftscale is False, or relative to the rotated cell's box vectors
608
+ if shiftscale is True. Cannot be given with shiftindex. If
609
+ neither shift nor shiftindex is given then shiftindex = 0 is used.
610
+ shiftindex : float, optional
611
+ The index of the identified shifts based on the rotated
612
+ cell to use. Different values allow for the selection of different
613
+ atomic planes neighboring the slip plane. Cannot be given with shift.
614
+ If neither shift nor shiftindex is given then shiftindex = 0 is used.
615
+ shiftscale : bool, optional
616
+ If False (default), a given shift value will be taken as absolute
617
+ Cartesian. If True, a given shift will be taken relative to the
618
+ rotated cell's box vectors.
619
+ """
620
+ # Handle shift parameters
621
+ if shift is not None:
622
+ if shiftindex is not None:
623
+ raise ValueError('shift and shiftindex cannot both be given')
624
+ if shiftscale is True:
625
+ self.__shift = miller.vector_crystal_to_cartesian(shift, self.rcell.box)
626
+ else:
627
+ self.__shift = np.asarray(shift)
628
+ assert self.__shift.shape == (3,)
629
+
630
+ elif shiftindex is not None:
631
+ self.__shift = self.shifts[shiftindex]
632
+
633
+ else:
634
+ self.__shift = self.shifts[0]
atomman/source/atomman/defect/GRIP.py ADDED
@@ -0,0 +1,545 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Optional
2
+ import secrets
3
+
4
+ import numpy as np
5
+
6
+ from .. import System
7
+ from . import GrainBoundary
8
+ from . import interstitial_site_finder
9
+
10
+ from yabadaba.record import Record
11
+
12
+ class GRIP(Record):
13
+ """
14
+ Class for managing input settings and building grain boundary
15
+ configurations according to the GRIP algorithm.
16
+ """
17
+
18
+ ########################## Basic metadata fields ##########################
19
+
20
+ @property
21
+ def style(self) -> str:
22
+ """str: The record style"""
23
+ return 'grip'
24
+
25
+ @property
26
+ def modelroot(self) -> str:
27
+ """str: The root element of the content"""
28
+ return 'grip'
29
+
30
+ ####################### Define Values and attributes #######################
31
+
32
+ def _init_values(self):
33
+ """
34
+ Method that defines the value objects for the Record. This should
35
+ call the super of this method, then use self._add_value to create new Value objects.
36
+ Note that the order values are defined matters
37
+ when build_model is called!!!
38
+ """
39
+ # Shift input settings
40
+ self._add_value('float', 'shift_delta', defaultvalue=0.05,
41
+ modelpath='input-parameter.shift.delta',
42
+ description=' '.join([
43
+ 'The spacing to use between possible shift samples.',
44
+ 'Default value is 0.05. Note shifts should range',
45
+ 'from 0 to 1.']))
46
+
47
+ # Temperature input settings
48
+ self._add_value('float', 'temperature_min', defaultvalue=0,
49
+ modelpath='input-parameter.temperature.min',
50
+ description=' '.join([
51
+ 'The minimum temperature to include in the sampling.']))
52
+ self._add_value('float', 'temperature_max', defaultvalue=2000,
53
+ modelpath='input-parameter.temperature.max',
54
+ description=' '.join([
55
+ 'The maximum temperature to include in the sampling.']))
56
+ self._add_value('float', 'temperature_delta', defaultvalue=100,
57
+ modelpath='input-parameter.temperature.delta',
58
+ description=' '.join([
59
+ 'The spacing to use between possible temperature',
60
+ 'samples. Default value is 100.']))
61
+ self._add_value('str', 'temperature_sample_style', defaultvalue='uniform',
62
+ modelpath='input-parameter.temperature.sample_style',
63
+ allowedvalues=['uniform'],
64
+ description=' '.join([
65
+ 'The sampling style to use for the temperature.',
66
+ '"uniform" will uniformly sample from the range provided',
67
+ 'and is the only currently supported style.']))
68
+
69
+ # Sizemults input settings
70
+ self._add_value('int', 'sizemults_max1', defaultvalue=3,
71
+ modelpath='input-parameter.sizemults.max1',
72
+ description=' '.join([
73
+ 'The maximum size multiplier to use for the first in-plane',
74
+ 'box vector. The selected sample will be between 1 and this',
75
+ 'value inclusively. Default value is 3.']))
76
+ self._add_value('int', 'sizemults_max2', defaultvalue=3,
77
+ modelpath='input-parameter.sizemults.max2',
78
+ description=' '.join([
79
+ 'The maximum size multiplier to use for the second in-plane',
80
+ 'box vector. The selected sample will be between 1 and this',
81
+ 'value inclusively. Default value is 3.']))
82
+ self._add_value('str', 'sizemults_sample_style',
83
+ modelpath='input-parameter.sizemults.sample_style',
84
+ defaultvalue='exponential',
85
+ allowedvalues=['uniform', 'exponential'],
86
+ description=' '.join([
87
+ 'Indicates the sample style to use for the sizemults.',
88
+ '"exponential" (default) will place higher sample weights',
89
+ 'on smaller sizemults. "uniform" will equally sample from',
90
+ 'the sizemults range.']))
91
+
92
+ # Runsteps input settings
93
+ self._add_value('float', 'runsteps_chance', defaultvalue=0.95,
94
+ modelpath='input-parameter.runsteps.chance',
95
+ description=' '.join([
96
+ 'A probability rate between 0 and 1 for if the MD relaxation',
97
+ 'will be performed: a value of 0 means MD is never done and',
98
+ 'a value of 1 means MD is always done. Default value is',
99
+ '0.95 (5% chance of no MD).']))
100
+ self._add_value('int', 'runsteps_min', defaultvalue=5000,
101
+ modelpath='input-parameter.runsteps.min',
102
+ description=' '.join([
103
+ 'The minimum runsteps value to include in the sampling.',
104
+ 'Default value is 5000.']))
105
+ self._add_value('int', 'runsteps_max', defaultvalue=500000,
106
+ modelpath='input-parameter.runsteps.max',
107
+ description=' '.join([
108
+ 'The maximum runsteps value to include in the sampling.',
109
+ 'Default value is 500000.']))
110
+ self._add_value('int', 'runsteps_delta', defaultvalue=1000,
111
+ modelpath='input-parameter.runsteps.delta',
112
+ description=' '.join([
113
+ 'The spacing to use between possible runsteps samples.',
114
+ 'Default value is 1000.']))
115
+ self._add_value('str', 'runsteps_sample_style',
116
+ modelpath='input-parameter.runsteps.sample_style',
117
+ defaultvalue='exponential',
118
+ allowedvalues=['uniform', 'exponential'],
119
+ description=' '.join([
120
+ 'The sampling style to use for the runsteps.',
121
+ '"uniform" will uniformly sample from the range provided.',
122
+ '"exponential" (default) will weight smaller runsteps as',
123
+ 'being more likely.']))
124
+
125
+ # Grain boundary builder settings
126
+ self._add_value('int', 'maxmult', defaultvalue=10,
127
+ modelpath='input-parameter.builder.maxmult',
128
+ description=' '.join([
129
+ 'The maximum size multiplier to use in searching for',
130
+ 'correspondence between the in-plane vectors of both',
131
+ 'grains. Both grains will be searched up to this max,'
132
+ "so only one grain's multiplier is guaranteed to be",
133
+ 'maxmult or less. Default value is 10.']))
134
+ self._add_value('float', 'minwidth', defaultvalue=30,
135
+ modelpath='input-parameter.builder.minwidth',
136
+ description=' '.join([
137
+ 'The minimum width for both grains perpendicular to',
138
+ 'the grain boundary. Default value is 30.']))
139
+
140
+ # Atom deletion settings
141
+ self._add_value('float', 'delete_min', defaultvalue=0.0,
142
+ modelpath='input-parameter.delete.min',
143
+ description=' '.join([
144
+ 'Minimum fraction of atoms to delete. Default value is 0.0.']))
145
+ self._add_value('float', 'delete_max', defaultvalue=1.0,
146
+ modelpath='input-parameter.delete.max',
147
+ description=' '.join([
148
+ 'Maximum fraction of atoms to delete. Default value is 1.0.']))
149
+
150
+ # Atom perturbation settings
151
+ self._add_value('float', 'perturb_width', defaultvalue=10,
152
+ modelpath='input-parameter.perturb.width',
153
+ description=' '.join([
154
+ 'The width from the grain boundary in each grain within which',
155
+ 'atoms will be perturbed. Default value is 10.']))
156
+ self._add_value('float', 'perturb_max1', defaultvalue=0.3,
157
+ modelpath='input-parameter.perturb.max1',
158
+ description=' '.join([
159
+ 'The maximum atomic perturbations that will be applied to',
160
+ 'atoms in grain 1. Default value is 0.3.']))
161
+ self._add_value('float', 'perturb_max2', defaultvalue=0.0,
162
+ modelpath='input-parameter.perturb.max2',
163
+ description=' '.join([
164
+ 'The maximum atomic perturbations that will be applied to',
165
+ 'atoms in grain 2. Default value is 0.0.']))
166
+
167
+ # Interstitial site settings
168
+ self._add_value('int', 'interstitial_max_num', defaultvalue=2,
169
+ modelpath='input-parameter.interstitial.max_num',
170
+ description=' '.join([
171
+ "Specifies the max number of interstitial sites to fill.",
172
+ "Note that the actual max may be less if the number of",
173
+ "atoms or the number of interstitial sites at the grain",
174
+ "boundary are smaller. Default value is 2."]))
175
+ self._add_value('float', 'interstitial_width', defaultvalue=1.5,
176
+ modelpath='input-parameter.interstitial.width',
177
+ description=' '.join([
178
+ "Only interstitial sites within the interstitial_width",
179
+ "distance from the grain boundary will be considered for",
180
+ "filling, and only atoms within 2*interstitial_width from",
181
+ "the grain boundary will be considered for moving."]))
182
+ self._add_value('str', 'interstitial_sample_style',
183
+ modelpath='input-parameter.interstitial.sample_style',
184
+ defaultvalue='uniform',
185
+ allowedvalues=['uniform', 'volume'],
186
+ description=' '.join([
187
+ "The sampling weight to use for selection of the interstitial",
188
+ "sites to fill. If 'uniform' (default), all sites will have",
189
+ "equal likelihood of being filled. If 'volume', then the",
190
+ "likelihood of sites being filled is weighted based on the",
191
+ "site's volume."]))
192
+
193
+ # Generated calculation settings and metadata
194
+ self._add_value('int', 'randomseed',
195
+ modelpath='calculation-parameter.randomseed',
196
+ description="The random number generator seed.")
197
+ self._add_value('float', 'shift1',
198
+ modelpath='calculation-parameter.shift1',
199
+ description="The relative shift along the first in-plane box vector.")
200
+ self._add_value('float', 'shift2',
201
+ modelpath='calculation-parameter.shift2',
202
+ description="The relative shift along the second in-plane box vector.")
203
+ self._add_value('int', 'sizemult1',
204
+ modelpath='calculation-parameter.sizemult1',
205
+ description="The size multiplier along the first in-plane box vector.")
206
+ self._add_value('int', 'sizemult2',
207
+ modelpath='calculation-parameter.sizemult2',
208
+ description="The size multiplier along the second in-plane box vector.")
209
+ self._add_value('int', 'runsteps',
210
+ modelpath='calculation-parameter.runsteps',
211
+ description="The number of MD relaxation steps to perform.")
212
+ self._add_value('float', 'temperature',
213
+ modelpath='calculation-parameter.temperature',
214
+ description="The temperature at which to perform the MD relaxation.")
215
+ self._add_value('float', 'density',
216
+ modelpath='calculation-parameter.density',
217
+ description="The atomic density of the grain boundary after atom deletion.")
218
+ self._add_value('int', 'ninterstitials',
219
+ modelpath='calculation-parameter.ninterstitials',
220
+ description="The number of interstitial positions filled.")
221
+
222
+ @property
223
+ def defaultname(self) -> Optional[str]:
224
+ """str: The name to default to, usually based on other properties"""
225
+ return 'grip-settings'
226
+
227
+ def boundary(self,
228
+ grainboundary: GrainBoundary,
229
+ randomseed: Optional[int] = None,
230
+ decimals: int = 6,
231
+ verbose: bool = False,
232
+ **kwargs):
233
+
234
+ # Update any calculation input settings if provided
235
+ if len(kwargs) > 0:
236
+ self.set_values(**kwargs)
237
+
238
+ # Set the random number seed and create a generator
239
+ if randomseed is None:
240
+ randomseed = secrets.randbits(32)
241
+ self.randomseed = randomseed
242
+ rng = np.random.default_rng(seed=randomseed)
243
+
244
+ # Select random input values
245
+ self.__set_shifts(rng, verbose)
246
+ self.__set_sizemults(rng, verbose)
247
+ self.__set_runsteps(rng, verbose)
248
+ self.__set_temperature(rng, verbose)
249
+
250
+ # Build and modify the grain boundary system
251
+ system, natoms1 = self.__build_boundary(grainboundary, verbose)
252
+ system, natoms1 = self.__delete_atoms(system, natoms1, rng,
253
+ grainboundary, decimals, verbose)
254
+ self.__perturb_atoms(system, natoms1, rng, grainboundary, verbose)
255
+ self.__interstitial_atoms(system, rng, grainboundary, verbose)
256
+
257
+ return system, natoms1
258
+
259
+ def __set_shifts(self,
260
+ rng: np.random.Generator,
261
+ verbose: bool):
262
+ """
263
+ Selects the two in-plane vector shifts to apply to the system using
264
+ random uniform samples.
265
+ """
266
+ # Build array of possible shifts to sample from
267
+ possibleshifts = np.arange(0, 1, self.shift_delta)
268
+
269
+ # Select gb shifts uniformly from possible values
270
+ self.shift1 = rng.choice(possibleshifts)
271
+ self.shift2 = rng.choice(possibleshifts)
272
+
273
+ if verbose:
274
+ print('shift1:', self.shift1)
275
+ print('shift2:', self.shift2)
276
+
277
+ def __set_sizemults(self,
278
+ rng: np.random.Generator,
279
+ verbose: bool):
280
+ """
281
+ Selects random sizemults values for the two in-plane box vectors.
282
+ """
283
+ # Build array of all possible sizemults
284
+ sizemults1 = np.arange(1, self.sizemults_max1 + 1)
285
+ sizemults2 = np.arange(1, self.sizemults_max2 + 1)
286
+
287
+ if self.sizemults_sample_style == 'uniform':
288
+ # Uniformly sample from allowed values
289
+ self.sizemult1 = rng.choice(sizemults1)
290
+ self.sizemult2 = rng.choice(sizemults2)
291
+
292
+ elif self.sizemults_sample_style == 'exponential':
293
+ # Place higher weights on smaller systems
294
+ sizemultsweights1 = np.exp(-sizemults1) / np.sum(np.exp(-sizemults1))
295
+ sizemultsweights2 = np.exp(-sizemults2) / np.sum(np.exp(-sizemults2))
296
+
297
+ # Select samples using the weights
298
+ self.sizemult1 = rng.choice(sizemults1, p=sizemultsweights1)
299
+ self.sizemult2 = rng.choice(sizemults2, p=sizemultsweights2)
300
+
301
+ else:
302
+ raise ValueError('Unsupported sizemults_sample_style value')
303
+
304
+ if verbose:
305
+ print('sizemult1:', self.sizemult1)
306
+ print('sizemult2:', self.sizemult2, flush=True)
307
+
308
+ def __set_runsteps(self,
309
+ rng: np.random.Generator,
310
+ verbose: bool):
311
+ """
312
+ Select a random number of runsteps and temperature for performing the
313
+ MD relaxation.
314
+ """
315
+
316
+ if rng.random() > self.runsteps_chance:
317
+ # Skip MD run if probability check fails
318
+ self.runsteps = 0
319
+
320
+ elif self.runsteps_sample_style == 'uniform':
321
+ # Uniformly sample the number of MD steps
322
+ self.runsteps = int(np.round(rng.choice(np.arange(self.runsteps_min, self.runsteps_max + 1, self.runsteps_delta))))
323
+
324
+ elif self.runsteps_sample_style == 'exponential':
325
+ # Exponentially scale the number of MD steps
326
+ C = np.log(self.runsteps_max / self.runsteps_min)
327
+ self.runsteps = int(np.round(self.runsteps_min * np.exp(C * rng.random()) / self.runsteps_delta) * self.runsteps_delta)
328
+
329
+ else:
330
+ raise ValueError('Unsupported runsteps_sample_style value')
331
+
332
+ if verbose:
333
+ print('runsteps:', self.runsteps, flush=True)
334
+
335
+ def __set_temperature(self,
336
+ rng: np.random.Generator,
337
+ verbose: bool):
338
+ """
339
+ Select a random number of runsteps and temperature for performing the
340
+ MD relaxation.
341
+ """
342
+ if self.runsteps == 0:
343
+ self.temperature = 0.0
344
+
345
+ elif self.temperature_sample_style == 'uniform':
346
+ # Select a random uniform sample for the temperature
347
+ self.temperature = np.round(rng.choice(np.arange(self.temperature_min, self.temperature_max + 1, self.temperature_delta)))
348
+
349
+ else:
350
+ raise ValueError('Unsupported temperature_sample_style value')
351
+
352
+ if verbose:
353
+ print('temperature:', self.temperature, flush=True)
354
+
355
+ def __build_boundary(self,
356
+ gb: GrainBoundary,
357
+ verbose: bool):
358
+ """
359
+ Builds the grain boundary system using the builder and the selected
360
+ sizemults and shifts.
361
+ """
362
+ mults1, mults2, strain = gb.identifymults(maxmult=self.maxmult,
363
+ minwidth=self.minwidth,
364
+ setvalues=False)
365
+
366
+ # Multiply the in-plane sizemults by the GRIP values
367
+ i1 = 0
368
+ i2 = 2
369
+ if gb.cutboxvector == 'a':
370
+ i1 = 1
371
+ elif gb.cutboxvector == 'c':
372
+ i2 = 1
373
+ mults1[i1] *= self.sizemult1
374
+ mults2[i1] *= self.sizemult1
375
+ mults1[i2] *= self.sizemult2
376
+ mults2[i2] *= self.sizemult2
377
+
378
+ if verbose:
379
+ print('mults1:', mults1)
380
+ print('mults2:', mults2)
381
+
382
+ # Call super to generate the boundary system
383
+ system, natoms1 = gb.boundary(mults1=mults1, mults2=mults2,
384
+ freesurface=True, straintype='top',
385
+ shift1=self.shift1, shift2=self.shift2,
386
+ deleter=0.0)
387
+
388
+ if verbose:
389
+ print('system width:', system.box.vects[gb.cutindex, gb.cutindex])
390
+ print('natoms (initial):', system.natoms, flush=True)
391
+
392
+ return system, natoms1
393
+
394
+ def __delete_atoms(self,
395
+ system: System,
396
+ natoms1: int,
397
+ rng: np.random.Generator,
398
+ gb: GrainBoundary,
399
+ decimals: int,
400
+ verbose: bool):
401
+ """
402
+ Randomly selects atoms for deletion from the grain boundary.
403
+ """
404
+ # GB plane region ranges from min coord in top grain to that plus dlat
405
+ dlat = gb.dlat(decimals) - 10**-decimals
406
+ mincoord = system.atoms.pos[:natoms1, gb.cutindex].min()
407
+ boundaryz = mincoord + dlat
408
+
409
+ # Identify and count gb plane atoms
410
+ inplane = np.where(system.atoms.pos[:natoms1, gb.cutindex] < boundaryz)[0]
411
+ natomsplane = len(inplane)
412
+
413
+ # Pick a random number of atoms to delete
414
+ ndel = int(rng.integers(np.floor(natomsplane * (1 - self.delete_max)),
415
+ np.ceil(natomsplane * (1 - self.delete_min)),
416
+ endpoint=True))
417
+
418
+ # Randomly select ndel atoms for deletion
419
+ todelete = rng.choice(inplane, size=ndel, replace=False)
420
+ keepindex = [x for i, x in enumerate(range(system.natoms)) if i not in todelete]
421
+
422
+ if verbose:
423
+ print('# atoms being deleted:', ndel)
424
+ for index in todelete:
425
+ print('atom deleted at', system.atoms.pos[index])
426
+
427
+ # Delete the selected atoms
428
+ newsystem = system.atoms_ix[keepindex]
429
+ newnatoms1 = natoms1 - ndel
430
+
431
+ # Compute the grain boundary atomic density
432
+ self.density = (natomsplane - ndel) / natomsplane
433
+
434
+ if verbose:
435
+ print('natoms (now):', newsystem.natoms)
436
+ print('gb density:', self.density, flush=True)
437
+
438
+ return newsystem, newnatoms1
439
+
440
+ def __perturb_atoms(self,
441
+ system: System,
442
+ natoms1: int,
443
+ rng: np.random.Generator,
444
+ gb: GrainBoundary,
445
+ verbose: bool):
446
+ """
447
+ Randomly perturb atoms near the grain boundary.
448
+ """
449
+ # Split atomic positions by grain
450
+ pos1 = system.atoms.pos[:natoms1]
451
+ pos2 = system.atoms.pos[natoms1:]
452
+
453
+ # Perturb grain 1 atoms
454
+ mincoord = pos1[:, gb.cutindex].min()
455
+ boundary = mincoord + self.perturb_width
456
+ inboundary = pos1[:, gb.cutindex] < boundary
457
+ pos1[inboundary, :] += self.perturb_max1 * rng.random([np.sum(inboundary), 3])
458
+
459
+ # Perturb grain 2 atoms
460
+ maxcoord = pos2[:, gb.cutindex].max()
461
+ boundary = maxcoord - self.perturb_width
462
+ inboundary = pos2[:, gb.cutindex] > boundary
463
+ pos2[inboundary, :] += self.perturb_max2 * rng.random([np.sum(inboundary), 3])
464
+
465
+ # Join pos and update in system
466
+ system.pos = np.vstack([pos1, pos2])
467
+
468
+ if verbose:
469
+ print('atoms perturbed', flush=True)
470
+
471
+ def __interstitial_atoms(self,
472
+ system: System,
473
+ rng: np.random.Generator,
474
+ gb: GrainBoundary,
475
+ verbose: bool):
476
+ """
477
+ Randomly moves atoms near the grain boundary into interstitial sites.
478
+ This identifies both interstitial sites and atoms near the grain
479
+ boundary, then randomly selects a random number of atoms to move into
480
+ randomly selected interstitial sites.
481
+ """
482
+ # Quick return if max is 0
483
+ if self.interstitial_max_num <= 0:
484
+ return
485
+
486
+ # Create slice of the atomic system around the grain boundary
487
+ search_width = 10 * self.interstitial_width
488
+ in_search = ((system.atoms.pos[:, gb.cutindex] > -search_width) &
489
+ (system.atoms.pos[:, gb.cutindex] < search_width))
490
+ search_system = system.atoms_ix[in_search]
491
+
492
+ # Find interstitial sites from the search system
493
+ allsites = interstitial_site_finder(search_system)
494
+
495
+ # Filter out sites away from the grain boundary
496
+ sites = []
497
+ for site in allsites:
498
+ if (site.pos[gb.cutindex] > -self.interstitial_width and
499
+ site.pos[gb.cutindex] < self.interstitial_width):
500
+ sites.append(site)
501
+ all_site_ids = [i for i in range(len(sites))]
502
+
503
+ if verbose:
504
+ print('# total interstitial sites:', len(sites), flush=True)
505
+
506
+ # Find ids of atoms near the grain boundary
507
+ in_boundary = ((system.atoms.pos[:, gb.cutindex] > -2 * self.interstitial_width) &
508
+ (system.atoms.pos[:, gb.cutindex] < 2 * self.interstitial_width))
509
+ atom_ids = np.where(in_boundary)[0]
510
+
511
+ if verbose:
512
+ print('# atoms for interstitial shifts:', len(atom_ids))
513
+
514
+ # Select max number to move based on max_num, num atoms and num sites
515
+ max_num = min([self.interstitial_max_num, len(sites), len(atom_ids)])
516
+
517
+ # Pick a random number of atoms to move from 0 to max_num
518
+ nmove = int(rng.integers(0, max_num, endpoint=True))
519
+
520
+ if verbose:
521
+ print('# atoms being shifted:', nmove)
522
+
523
+ # Shuffle atom_ids and only select the first nmove
524
+ np.random.shuffle(atom_ids)
525
+ atom_ids = atom_ids[:nmove]
526
+
527
+ if self.interstitial_sample_style == 'uniform':
528
+ # Randomly select interstitial sites to fill with no weights
529
+ site_ids = rng.choice(all_site_ids, size=nmove, replace=False)
530
+
531
+ elif self.interstitial_sample_style == 'volume':
532
+ # Randomly select interstitial sites to fill weighted towards large volumes
533
+ volumes = np.array([site.volume for site in sites])
534
+ weights = volumes / np.sum(volumes)
535
+ site_ids = rng.choice(all_site_ids, size=nmove, replace=False, weights=weights)
536
+
537
+ # Move atoms to the interstitial sites
538
+ for atom_id, site_id in zip(atom_ids, site_ids):
539
+ if verbose:
540
+ print('atom at', system.atoms.pos[atom_id])
541
+ print('moved to', sites[site_id].pos, flush=True)
542
+ system.atoms.pos[atom_id] = sites[site_id].pos
543
+
544
+ self.ninterstitials = nmove
545
+
atomman/source/atomman/defect/GammaSurface.py ADDED
@@ -0,0 +1,1366 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python imports
3
+ from collections import OrderedDict
4
+ import io
5
+ from typing import Optional, Tuple, Union
6
+
7
+ # http://pandas.pydata.org/
8
+ import pandas as pd
9
+
10
+ # http://www.numpy.org/
11
+ import numpy as np
12
+ import numpy.typing as npt
13
+
14
+ # https://www.scipy.org/
15
+ from scipy.interpolate import Rbf, NearestNDInterpolator
16
+
17
+ # http://matplotlib.org/
18
+ import matplotlib.pyplot as plt
19
+
20
+ # https://github.com/usnistgov/DataModelDict
21
+ from DataModelDict import DataModelDict as DM
22
+
23
+ # atomman imports
24
+ from .. import Box
25
+ from .. import unitconvert as uc
26
+ from ..tools import miller
27
+ from ..mep import create_path, BasePath
28
+
29
+ class GammaSurface(object):
30
+ """
31
+ Class for representing gamma surfaces, i.e., generalized stacking faults.
32
+ """
33
+
34
+ def __init__(self,
35
+ model: Union[str, io.IOBase, DM, None] = None,
36
+ a1vect: Optional[npt.ArrayLike] = None,
37
+ a2vect: Optional[npt.ArrayLike] = None,
38
+ a1: Optional[npt.ArrayLike] = None,
39
+ a2: Optional[npt.ArrayLike] = None,
40
+ E_gsf: Optional[npt.ArrayLike] = None,
41
+ box: Optional[Box] = None,
42
+ delta: Optional[npt.ArrayLike] = None):
43
+ """
44
+ Class initializer. Parameter model must be given alone. Otherwise,
45
+ all or none of a1vect, a2vect, a1, a2, and E_gsf must be given.
46
+
47
+ Parameters
48
+ ----------
49
+ model : str, file-like object, DataModelDict, optional
50
+ XML/JSON data model containing the stacking fault information.
51
+ a1vect : array-like object, optional
52
+ The a1 shifting vector. If box is given, a1vect is taken as a
53
+ crystal lattice vector, otherwise as a Cartesian vector.
54
+ a2vect : array-like object, optional
55
+ The a2 shifting vector. If box is given, a2vect is taken as a
56
+ crystal lattice vector, otherwise as a Cartesian vector.
57
+ a1 : array-like object, optional
58
+ List of fractional coordinates along a1vect corresponding to the
59
+ E_gsf (and delta) values.
60
+ a2 : array-like object, optional
61
+ List of fractional coordinates along a2vect corresponding to the
62
+ E_gsf (and delta) values.
63
+ E_gsf : array-like object, optional
64
+ List of generalized stacking fault energies for the positions
65
+ associated with the corresponding (a1, a2) fractional coordinates.
66
+ box : atomman.Box, optional
67
+ Defines unit cell box dimensions for conversion between crystal
68
+ lattice and Cartesian vectors. If not given, will be set as a
69
+ square unit box, thus no conversion will occur (i.e. a1vect,
70
+ a2vect will be Cartesian).
71
+ delta : array-like object, optional
72
+ List of change in displacements normal to the fault plane for the
73
+ positions associated with the corresponding (a1, a2) fractional
74
+ coordinates.
75
+ """
76
+
77
+ # Load model if given
78
+ if model is not None:
79
+ try:
80
+ assert box is None
81
+ assert a1vect is None
82
+ assert a2vect is None
83
+ assert a1 is None
84
+ assert a2 is None
85
+ assert E_gsf is None
86
+ assert delta is None
87
+ except:
88
+ raise TypeError('model cannot be given with any other parameter')
89
+ else:
90
+ self.model(model=model)
91
+
92
+ # Set values if given
93
+ elif (a1 is not None or a2 is not None or E_gsf is not None
94
+ or a1vect is not None or a2vect is not None
95
+ or delta is not None or box is not None):
96
+ try:
97
+ assert a1vect is not None
98
+ assert a2vect is not None
99
+ assert a1 is not None
100
+ assert a2 is not None
101
+ assert E_gsf is not None
102
+ except:
103
+ raise TypeError('Defining data requires a1vect, a2vect, a1, a2 and E_gsf')
104
+ else:
105
+ self.set(a1vect, a2vect, a1, a2, E_gsf, box=box, delta=delta)
106
+
107
+ # Set flag for no supplied data
108
+ else:
109
+ self.__hasdata = False
110
+
111
+ @property
112
+ def data(self) -> pd.DataFrame:
113
+ """pandas.DataFrame : The raw data."""
114
+ if self.__hasdata:
115
+ return self.__data
116
+ else:
117
+ raise AttributeError('gamma surface data not set')
118
+
119
+ @property
120
+ def a1vect(self) -> np.ndarray:
121
+ """numpy.ndarray : The a1 shifting vector."""
122
+ if self.__hasdata:
123
+ return self.__a1vect
124
+ else:
125
+ raise AttributeError('gamma surface data not set')
126
+
127
+ @property
128
+ def a2vect(self) -> np.ndarray:
129
+ """numpy.ndarray : The a2 shifting vector."""
130
+ if self.__hasdata:
131
+ return self.__a2vect
132
+ else:
133
+ raise AttributeError('gamma surface data not set')
134
+
135
+ @property
136
+ def planenormal(self) -> np.ndarray:
137
+ """numpy.ndarray : The Cartesian vector normal to the fault plane."""
138
+ if self.__hasdata:
139
+ return self.__planenormal
140
+ else:
141
+ raise AttributeError('gamma surface data not set')
142
+
143
+ @property
144
+ def box(self) -> Box:
145
+ """
146
+ atomman.Box : A unit cell box used for converting between
147
+ crystal lattice and Cartesian vectors.
148
+ """
149
+ if self.__hasdata:
150
+ return self.__box
151
+ else:
152
+ raise AttributeError('gamma surface data not set')
153
+
154
+ def set(self,
155
+ a1vect: npt.ArrayLike,
156
+ a2vect: npt.ArrayLike,
157
+ a1: npt.ArrayLike,
158
+ a2: npt.ArrayLike,
159
+ E_gsf: npt.ArrayLike,
160
+ box: Optional[Box] = None,
161
+ delta: Optional[npt.ArrayLike] = None):
162
+ """
163
+ Sets generalized stacking fault data.
164
+
165
+ Parameters
166
+ ----------
167
+ a1vect : array-like object
168
+ The a1 shifting vector. If box is given, a1vect is taken as a
169
+ crystal lattice vector, otherwise as a Cartesian vector.
170
+ a2vect : array-like object
171
+ The a2 shifting vector. If box is given, a2vect is taken as a
172
+ crystal lattice vector, otherwise as a Cartesian vector.
173
+ a1 : array-like object
174
+ List of fractional coordinates along a1vect corresponding to the
175
+ E_gsf (and delta) values.
176
+ a2 : array-like object
177
+ List of fractional coordinates along a2vect corresponding to the
178
+ E_gsf (and delta) values.
179
+ E_gsf : array-like object
180
+ List of generalized stacking fault energies for the positions
181
+ associated with the corresponding (a1, a2) fractional coordinates.
182
+ box : atomman.Box, optional
183
+ Defines unit cell box dimensions for conversion between crystal
184
+ lattice and Cartesian vectors. If not given, will be set as a
185
+ square unit box, thus no conversion will occur (i.e. a1vect,
186
+ a2vect will be Cartesian).
187
+ delta : array-like object, optional
188
+ List of change in displacements normal to the fault plane for the
189
+ positions associated with the corresponding (a1, a2) fractional
190
+ coordinates.
191
+ """
192
+ # Set a1vect
193
+ if isinstance(a1vect, str):
194
+ a1vect = a1vect.split()
195
+ a1vect = np.asarray(a1vect, dtype=float)
196
+ if a1vect.shape == (4,):
197
+ a1vect = miller.vector4to3(a1vect)
198
+ elif a1vect.shape != (3,):
199
+ raise ValueError('a1vect must be a 3D vector')
200
+ self.__a1vect = a1vect
201
+
202
+ # Set a2vect
203
+ if isinstance(a2vect, str):
204
+ a2vect = a2vect.split()
205
+ a2vect = np.asarray(a2vect, dtype=float)
206
+ if a2vect.shape == (4,):
207
+ a2vect = miller.vector4to3(a2vect)
208
+ elif a2vect.shape != (3,):
209
+ raise ValueError('a2vect must be a 3D vector')
210
+ self.__a2vect = a2vect
211
+
212
+ # Set box
213
+ if box is None:
214
+ box = Box()
215
+ if not isinstance(box, Box):
216
+ raise TypeError('box must be an atomman.Box')
217
+ self.__box = box
218
+
219
+ # Set plane normal
220
+ a1vect = np.dot(a1vect, box.vects)
221
+ a2vect = np.dot(a2vect, box.vects)
222
+ planenormal = np.cross(a1vect, a2vect)
223
+ self.__planenormal = planenormal / np.linalg.norm(planenormal)
224
+
225
+ # Set data
226
+ data = OrderedDict()
227
+ data['a1'] = a1
228
+ data['a2'] = a2
229
+ data['E_gsf'] = E_gsf
230
+ if delta is not None:
231
+ data['delta'] = delta
232
+ self.__data = pd.DataFrame(data)
233
+
234
+ # Fit
235
+ self.__hasdata = True
236
+ self.fit()
237
+
238
+ def fit(self):
239
+ """
240
+ Defines the interpolation functions from the raw data.
241
+ """
242
+
243
+ # Ignore a1, a2=1.0 values if included
244
+ shortdata = self.data[~(np.isclose(self.data.a1, 1.0) | np.isclose(self.data.a2, 1.0))]
245
+
246
+ # Create supercells of values
247
+ a1 = shortdata.a1
248
+ a2 = shortdata.a2
249
+ a1 = np.concatenate([a1-1, a1-1, a1-1, a1, a1, a1, a1+1, a1+1, a1+1])
250
+ a2 = np.concatenate([a2-1, a2, a2+1, a2-1, a2, a2+1, a2-1, a2, a2+1])
251
+
252
+ # Find values in 0-1 cell +- one point
253
+ ua1 = np.unique(a1)
254
+ ua2 = np.unique(a2)
255
+ a1min = ua1[np.where(np.isclose(ua1, 0.0))[0][0] - 1] - 1e-8
256
+ a1max = ua1[np.where(np.isclose(ua1, 1.0))[0][-1] + 1] + 1e-8
257
+ a2min = ua2[np.where(np.isclose(ua2, 0.0))[0][0] - 1] - 1e-8
258
+ a2max = ua2[np.where(np.isclose(ua2, 1.0))[0][-1] + 1] + 1e-8
259
+ ix = np.where((a1 >= a1min) & (a1 <= a1max) & (a2 >= a2min) & (a2 <= a2max))
260
+
261
+ # Fit energy
262
+ E_gsf = np.concatenate([shortdata.E_gsf] * 9)
263
+ self.__E_gsf_fit = Rbf(a1[ix], a2[ix], E_gsf[ix])
264
+ self.__E_gsf_nearest = NearestNDInterpolator(np.array([a1[ix], a2[ix]]).T, E_gsf[ix])
265
+
266
+ # Fit delta
267
+ if 'delta' in self.data:
268
+ delta = np.concatenate([shortdata.delta] * 9)
269
+ self.__delta_fit = Rbf(a1[ix], a2[ix], delta[ix])
270
+ self.__delta_nearest = NearestNDInterpolator(np.array([a1[ix], a2[ix]]).T, delta[ix])
271
+
272
+ def model(self,
273
+ model: Union[str, io.IOBase, DM, None] = None,
274
+ length_unit: str = 'angstrom',
275
+ energyperarea_unit: str = 'mJ/m^2') -> Optional[DM]:
276
+ """
277
+ Return or set DataModelDict representation of the gamma surface.
278
+
279
+ Parameters
280
+ ----------
281
+ model : str, file-like object or DataModelDict, optional
282
+ XML/JSON content to extract gamma surface energy from. If not
283
+ given, model content will be generated.
284
+ length_unit : str, optional
285
+ Units to report delta displacement values in when a new model is
286
+ generated. Default value is 'angstrom'.
287
+ energyperarea_unit : str, optional
288
+ Units to report fault energy values in when a new model is
289
+ generated. Default value is 'mJ/m^2'.
290
+
291
+ Returns
292
+ -------
293
+ DataModelDict
294
+ A dictionary containing the stacking fault data of the
295
+ GammaSurface object. Returned if model is not given.
296
+ """
297
+ # Set values if model given
298
+ if model is not None:
299
+ model = DM(model).find('stacking-fault-map')
300
+
301
+ # Read in box, a1vect and a2vect
302
+ box = Box(avect = model['box']['avect'],
303
+ bvect = model['box']['bvect'],
304
+ cvect = model['box']['cvect'])
305
+
306
+ a1vect = model['shift-vector-1']
307
+ a2vect = model['shift-vector-2']
308
+
309
+ # Read in stacking fault data
310
+ gsf = model.find('stacking-fault-relation')
311
+
312
+ a1 = gsf['shift-vector-1-fraction']
313
+ a2 = gsf['shift-vector-2-fraction']
314
+ E_gsf = uc.value_unit(gsf['energy'])
315
+ try:
316
+ delta = uc.value_unit(gsf['plane-separation'])
317
+ except:
318
+ delta = None
319
+ self.set(a1vect, a2vect, a1, a2, E_gsf, box=box, delta=delta)
320
+
321
+ # Generate model
322
+ else:
323
+ model = DM()
324
+ model['stacking-fault-map'] = sfm = DM()
325
+ sfm['box'] = DM()
326
+ sfm['box']['avect'] = list(self.box.avect)
327
+ sfm['box']['bvect'] = list(self.box.bvect)
328
+ sfm['box']['cvect'] = list(self.box.cvect)
329
+ sfm['shift-vector-1'] = list(self.a1vect)
330
+ sfm['shift-vector-2'] = list(self.a2vect)
331
+ sfm['stacking-fault-relation'] = sfr = DM()
332
+ sfr['shift-vector-1-fraction'] = list(self.data.a1)
333
+ sfr['shift-vector-2-fraction'] = list(self.data.a2)
334
+ sfr['energy'] = uc.model(self.data.E_gsf, energyperarea_unit)
335
+ if 'delta' in self.data:
336
+ sfr['plane-separation'] = uc.model(self.data.delta, length_unit)
337
+
338
+ return model
339
+
340
+ def a12_to_pos(self,
341
+ a1: npt.ArrayLike,
342
+ a2: npt.ArrayLike,
343
+ a1vect: Optional[npt.ArrayLike] = None,
344
+ a2vect: Optional[npt.ArrayLike] = None) -> np.ndarray:
345
+ """
346
+ Conversion function from normalized a1, a2 coordinates to Cartesian
347
+ positions.
348
+
349
+ Parameters
350
+ ----------
351
+ a1 : array-like object
352
+ Fractional distance(s) along a1 vector.
353
+ a2 : array-like object
354
+ Fractional distance(s) along a2 vector.
355
+ a1vect : array-like object, optional
356
+ Crystal vector for the a1 vector. Default value of None uses the
357
+ saved a1vect.
358
+ a2vect : array-like object, optional
359
+ Crystal vector for the a2 vector. Default value of None uses the
360
+ saved a2vect.
361
+
362
+ Returns
363
+ -------
364
+ np.array
365
+ 3D Cartesian position vector(s).
366
+ """
367
+ # Handle a1vect and a2vect
368
+ if a1vect is None:
369
+ a1vect = self.a1vect
370
+ a1vect = np.asarray(a1vect)
371
+
372
+ if a2vect is None:
373
+ a2vect = self.a2vect
374
+ a2vect = np.asarray(a2vect)
375
+
376
+ # Convert a1vect and a2vect from crystal to Cartesian coordinates
377
+ a1vect = np.dot(a1vect, self.box.vects)
378
+ a2vect = np.dot(a2vect, self.box.vects)
379
+
380
+ # Transform a1, a2 to Cartesian pos
381
+ return np.outer(a1, a1vect) + np.outer(a2, a2vect)
382
+
383
+ def pos_to_xy(self,
384
+ pos: npt.ArrayLike,
385
+ xvect: Optional[npt.ArrayLike] = None
386
+ ) -> Union[Tuple[float, float],
387
+ Tuple[np.ndarray, np.ndarray]]:
388
+ """
389
+ Conversion function from Cartesian positions to plotting x, y
390
+ coordinates.
391
+
392
+ Parameters
393
+ ----------
394
+ pos: array-like object
395
+ 3D Cartesian position vector(s).
396
+ xvect : array-like object, optional
397
+ Cartesian vector corresponding to the plotting x-axis. If None (default), this is
398
+ taken as the Cartesian of a1vect.
399
+
400
+ Returns
401
+ -------
402
+ x : float or numpy.ndarray
403
+ Plotting x coordinate(s).
404
+ y : float or numpy.ndarray
405
+ Plotting y coordinate(s).
406
+ """
407
+ # Handle xvect
408
+ if xvect is None:
409
+ xvect = np.dot(self.a1vect, self.box.vects)
410
+ xvect = np.asarray(xvect)
411
+ if not np.isclose(np.dot(xvect, self.planenormal), 0.0):
412
+ raise ValueError('xvect must be in plane defined by a1vect and a2vect')
413
+
414
+ # Build transformation tensor
415
+ yvect = np.cross(self.planenormal, xvect)
416
+ transform = np.array([xvect, yvect, self.planenormal])
417
+ transform = (transform.T / np.linalg.norm(transform, axis=1)).T
418
+
419
+ # Transform coordinates to x,y,z orientation
420
+ pos = transform.dot(pos.T).T
421
+
422
+ # Return x, y coordinates
423
+ return pos[...,0], pos[...,1]
424
+
425
+ def a12_to_xy(self,
426
+ a1: npt.ArrayLike,
427
+ a2: npt.ArrayLike,
428
+ a1vect: Optional[npt.ArrayLike] = None,
429
+ a2vect: Optional[npt.ArrayLike] = None,
430
+ xvect: Optional[npt.ArrayLike] = None
431
+ ) -> Union[Tuple[float, float],
432
+ Tuple[np.ndarray, np.ndarray]]:
433
+ """
434
+ Conversion function from normalized a1, a2 coordinates to plotting x, y
435
+ coordinates.
436
+
437
+ Parameters
438
+ ----------
439
+ a1 : array-like object
440
+ Fractional distance(s) along a1 vector.
441
+ a2 : array-like object
442
+ Fractional distance(s) along a2 vector.
443
+ a1vect : array-like object, optional
444
+ Crystal vector for the a1 vector. Default value of None uses the
445
+ saved a1vect.
446
+ a2vect : array-like object, optional
447
+ Crystal vector for the a2 vector. Default value of None uses the
448
+ saved a2vect.
449
+ xvect : array-like object, optional
450
+ Cartesian vector corresponding to the plotting x-axis. If None (default), this is
451
+ taken as the Cartesian of a1vect.
452
+
453
+ Returns
454
+ -------
455
+ x : float or numpy.ndarray
456
+ Plotting x coordinate(s).
457
+ y : float or numpy.ndarray
458
+ Plotting y coordinate(s).
459
+ """
460
+ # Set xvect as given a1vect if needed
461
+ if a1vect is not None and xvect is None:
462
+ xvect = np.dot(a1vect, self.box.vects)
463
+
464
+ # Transform from a1, a2 to pos
465
+ pos = self.a12_to_pos(a1, a2, a1vect=a1vect, a2vect=a2vect)
466
+
467
+ # Transform from pos to x, y
468
+ return self.pos_to_xy(pos, xvect=xvect)
469
+
470
+ def pos_to_a12(self,
471
+ pos: npt.ArrayLike,
472
+ a1vect: Optional[npt.ArrayLike] = None,
473
+ a2vect: Optional[npt.ArrayLike] = None
474
+ ) -> Union[Tuple[float, float],
475
+ Tuple[np.ndarray, np.ndarray]]:
476
+ """
477
+ Conversion function from Cartesian positions to normalized a1, a2
478
+ coordinates.
479
+
480
+ Parameters
481
+ ----------
482
+ pos : array-like object
483
+ 3D Cartesian position vector(s).
484
+ a1vect : array-like object, optional
485
+ Crystal vector for the a1 vector. Default value of None uses the
486
+ saved a1vect.
487
+ a2vect : array-like object, optional
488
+ Crystal vector for the a2 vector. Default value of None uses the
489
+ saved a2vect.
490
+
491
+ Returns
492
+ -------
493
+ a1 : float(s)
494
+ Fractional distance(s) along a1 vector.
495
+ a2 : float(s)
496
+ Fractional distance(s) along a2 vector.
497
+ """
498
+
499
+ # Handle a1vect and a2vect
500
+ if a1vect is None:
501
+ a1vect = self.a1vect
502
+ a1vect = np.asarray(a1vect)
503
+
504
+ if a2vect is None:
505
+ a2vect = self.a2vect
506
+ a2vect = np.asarray(a2vect)
507
+
508
+ # Convert a1vect and a2vect from crystal to Cartesian coordinates
509
+ a1vect = np.dot(a1vect, self.box.vects)
510
+ a2vect = np.dot(a2vect, self.box.vects)
511
+
512
+ # Solve for a1, a2, a3
513
+ a3vect = np.cross(a1vect, a2vect)
514
+ coeffs = np.array([a1vect, a2vect, a3vect]).T
515
+ if pos.ndim == 2:
516
+ coeffs = np.array([coeffs])
517
+ pos = pos.reshape(pos.shape + (1,))
518
+
519
+ a123 = np.linalg.solve(coeffs, pos).reshape(pos.shape[:-1])
520
+ else:
521
+ a123 = np.linalg.solve(coeffs, pos)
522
+
523
+ assert np.allclose(a123[...,2], 0.0, atol=1e-6), np.abs(a123[...,2]).max()
524
+
525
+ # Return a1, a2
526
+ return a123[...,0], a123[...,1]
527
+
528
+ def xy_to_pos(self,
529
+ x: npt.ArrayLike,
530
+ y: npt.ArrayLike,
531
+ xvect: Optional[npt.ArrayLike] = None) -> np.ndarray:
532
+ """
533
+ Conversion function from plotting x, y coordinates to Cartesian
534
+ positions.
535
+
536
+ Parameters
537
+ ----------
538
+ x : array-like object
539
+ Plotting x coordinate(s).
540
+ y : array-like object
541
+ Plotting y coordinate(s).
542
+ xvect : array-like object, optional
543
+ Cartesian vector corresponding to the plotting x-axis. If None
544
+ (default), this is taken as the Cartesian of a1vect.
545
+
546
+ Returns
547
+ -------
548
+ pos: np.array
549
+ 3D Cartesian position vector(s).
550
+ """
551
+ # Assign default xvect if needed
552
+ if xvect is None:
553
+ xvect = np.dot(self.a1vect, self.box.vects)
554
+ xvect = np.asarray(xvect)
555
+ if not np.isclose(np.dot(xvect, self.planenormal), 0.0):
556
+ raise ValueError('xvect must be in plane defined by a1vect and a2vect')
557
+
558
+ # Build transformation tensor
559
+ yvect = np.cross(self.planenormal, xvect)
560
+ transform = np.array([xvect, yvect, self.planenormal])
561
+ transform = (transform.T / np.linalg.norm(transform, axis=1)).T
562
+ transform = np.linalg.inv(transform)
563
+
564
+ # Transform coords
565
+ pos = np.outer(x, [1,0,0]) + np.outer(y, [0,1,0])
566
+ pos = transform.dot(pos.T).T
567
+
568
+ # Return Cartesian pos
569
+ return pos
570
+
571
+ def xy_to_a12(self,
572
+ x: npt.ArrayLike,
573
+ y: npt.ArrayLike,
574
+ a1vect: Optional[npt.ArrayLike] = None,
575
+ a2vect: Optional[npt.ArrayLike] = None,
576
+ xvect: Optional[npt.ArrayLike] = None
577
+ ) -> Union[Tuple[float, float],
578
+ Tuple[np.ndarray, np.ndarray]]:
579
+ """
580
+ Conversion function from plotting x, y coordinates to normalized a1, a2
581
+ coordinates.
582
+
583
+ Parameters
584
+ ----------
585
+ x : array-like object
586
+ Plotting x coordinate(s).
587
+ y : array-like object
588
+ Plotting y coordinate(s).
589
+ a1vect : array-like object, optional
590
+ Crystal vector for the a1 vector. Default value of None uses the
591
+ saved a1vect.
592
+ a2vect : array-like object, optional
593
+ Crystal vector for the a2 vector. Default value of None uses the
594
+ saved a2vect.
595
+ xvect : array-like object, optional
596
+ Cartesian vector corresponding to the plotting x-axis. If None
597
+ (default), this is taken as the Cartesian of a1vect.
598
+
599
+ Returns
600
+ -------
601
+ a1 : float(s)
602
+ Fractional distance(s) along a1 vector.
603
+ a2 : float(s)
604
+ Fractional distance(s) along a2 vector.
605
+ """
606
+
607
+ # Set xvect to given a1vect if needed
608
+ if a1vect is not None and xvect is None:
609
+ xvect = np.dot(a1vect, self.box.vects)
610
+
611
+ # Convert x, y to pos
612
+ pos = self.xy_to_pos(x, y, xvect=xvect)
613
+
614
+ # Convert pos to a1, a2
615
+ return self.pos_to_a12(pos, a1vect=a1vect, a2vect=a2vect)
616
+
617
+ def E_gsf(self, **kwargs) -> Union[float, np.ndarray]:
618
+ """
619
+ Returns values for generalized stacking fault energy interpolated from
620
+ the raw data. Values can be obtained relative to a1, a2 fractional
621
+ coordinates, x, y plotting coordinates, or pos Cartesian coordinates.
622
+
623
+ Parameters
624
+ ----------
625
+ a1 : array-like object, optional
626
+ Fractional coordinate(s) along a1vect.
627
+ a2 : array-like object, optional
628
+ Fractional coordinate(s) along a2vect.
629
+ pos : array-like object, optional
630
+ 3D Cartesian position vector(s).
631
+ x : array-like object, optional
632
+ Plotting x coordinate(s).
633
+ y : array-like object, optional
634
+ Plotting y coordinate(s).
635
+ a1vect : array-like object, optional
636
+ Vector for the a1 fractional coordinates. Default value of None
637
+ uses the saved a1vect.
638
+ a2vect : array-like object, optional
639
+ Vector for the a2 fractional coordinates. Default value of None
640
+ uses the saved a2vect.
641
+ xvect : array-like object, optional
642
+ Cartesian vector corresponding to the plotting x-axis. If None
643
+ (default), this is taken as the Cartesian of a1vect.
644
+ smooth : bool, optional
645
+ If True (default) the returned values are smoothed using a RBF fit.
646
+ If False, the closest measured values are returned.
647
+
648
+ Returns
649
+ -------
650
+ float or np.ndarray
651
+ The generalized stacking fault energy value(s).
652
+ """
653
+ if not self.__hasdata:
654
+ raise AttributeError('gamma surface data not set')
655
+
656
+ smooth = kwargs.pop('smooth', True)
657
+
658
+ # Convert x, y to a1, a2
659
+ if 'x' in kwargs:
660
+ x = kwargs.pop('x')
661
+ y = kwargs.pop('y')
662
+ a1vect = kwargs.pop('a1vect', None)
663
+ a2vect = kwargs.pop('a2vect', None)
664
+ xvect = kwargs.pop('xvect', None)
665
+ assert len(kwargs) == 0, 'Unknown/incompatible arguments given'
666
+ a1, a2 = self.xy_to_a12(x, y, a1vect=a1vect, a2vect=a2vect, xvect=xvect)
667
+
668
+ # Convert pos to a1, a2
669
+ elif 'pos' in kwargs:
670
+ pos = kwargs.pop('pos')
671
+ a1vect = kwargs.pop('a1vect', None)
672
+ a2vect = kwargs.pop('a2vect', None)
673
+ assert len(kwargs) == 0, 'Unknown/incompatible arguments given'
674
+ a1, a2 = self.pos_to_a12(pos, a1vect=a1vect, a2vect=a2vect)
675
+
676
+ # Get a1, a2 from kwargs
677
+ else:
678
+ a1 = np.array(kwargs.pop('a1'))
679
+ a2 = np.array(kwargs.pop('a2'))
680
+ a1vect = kwargs.pop('a1vect', None)
681
+ a2vect = kwargs.pop('a2vect', None)
682
+ assert len(kwargs) == 0, 'Unknown/incompatible arguments given'
683
+ if a1vect is not None or a2vect is not None:
684
+ shape = a1.shape
685
+ # Convert into pos using given a1vect, a2vect, then back into a1, a2
686
+ pos = self.a12_to_pos(a1, a2, a1vect=a1vect, a2vect=a2vect)
687
+ a1, a2 = self.pos_to_a12(pos)
688
+ a1 = a1.reshape(shape)
689
+ a2 = a2.reshape(shape)
690
+
691
+ # Return interpolated values
692
+ if smooth:
693
+ cushion = (1 - self.data.a1.max()) / 2
694
+
695
+ # Wrap all a1, a2 values within [-cushion, 1.0 - cushion)
696
+ while np.any(a1 >= 1.0 - cushion):
697
+ a1[a1 >= 1.0 - cushion] -= 1.0
698
+ while np.any(a1 < -cushion):
699
+ a1[a1 < -cushion] += 1.0
700
+ while np.any(a2 >= 1.0 - cushion):
701
+ a2[a2 >= 1.0 - cushion] -= 1.0
702
+ while np.any(a2 < -cushion):
703
+ a2[a2 < -cushion] += 1.0
704
+
705
+ # Compute weighting factors
706
+ def zone1(x):
707
+ return (x + cushion) / (2 * cushion)
708
+ def zone2(x):
709
+ return np.ones_like(x)
710
+ x = np.piecewise(a1, [a1 < cushion, a1>= cushion], [zone1, zone2])
711
+ y = np.piecewise(a2, [a2 < cushion, a2>= cushion], [zone1, zone2])
712
+
713
+ # Linear smoothing across boundaries
714
+ return ( x * y * self.__E_gsf_fit(a1, a2)
715
+ + x * (1 - y) * self.__E_gsf_fit(a1, a2 + 1)
716
+ + (1 - x) * y * self.__E_gsf_fit(a1 + 1, a2)
717
+ + (1 - x) * (1 - y) * self.__E_gsf_fit(a1 + 1, a2 + 1))
718
+
719
+ # Return nearest values
720
+ else:
721
+
722
+ # Wrap all values within 0.0 < a1, a2 < 1.0
723
+ while np.any(a1 > 1.0):
724
+ a1[a1 > 1.0] -= 1.0
725
+ while np.any(a1 < 0.0):
726
+ a1[a1 < 0.0] += 1.0
727
+ while np.any(a2 > 1.0):
728
+ a2[a2 > 1.0] -= 1.0
729
+ while np.any(a2 < 0.0):
730
+ a2[a2 < 0.0] += 1.0
731
+
732
+ return self.__E_gsf_nearest(np.array([a1.flatten(), a2.flatten()]).T).reshape(a1.shape)
733
+
734
+ def delta(self, **kwargs) -> Union[float, np.ndarray]:
735
+ """
736
+ Returns values for generalized stacking fault relaxation distance
737
+ interpolated from the raw data. Values can be obtained relative to
738
+ a1, a2 fractional coordinates, x, y plotting coordinates, or pos
739
+ Cartesian coordinates.
740
+
741
+ Parameters
742
+ ----------
743
+ a1 : array-like object, optional
744
+ Fractional coordinate(s) along a1vect.
745
+ a2 : array-like object, optional
746
+ Fractional coordinate(s) along a2vect.
747
+ pos : array-like object, optional
748
+ 3D Cartesian position vector(s).
749
+ x : array-like object, optional
750
+ Plotting x coordinate(s).
751
+ y : array-like object, optional
752
+ Plotting y coordinate(s).
753
+ a1vect : array-like object, optional
754
+ Vector for the a1 fractional coordinates. Default value of None
755
+ uses the saved a1vect.
756
+ a2vect : array-like object, optional
757
+ Vector for the a2 fractional coordinates. Default value of None
758
+ uses the saved a2vect.
759
+ xvect : array-like object, optional
760
+ Cartesian vector corresponding to the plotting x-axis. If None
761
+ (default), this is taken as the Cartesian of a1vect.
762
+ smooth : bool, optional
763
+ If True (default) the returned values are smoothed using a RBF fit.
764
+ If False, the closest measured values are returned.
765
+
766
+ Returns
767
+ -------
768
+ float or np.ndarray
769
+ The generalized stacking fault planar shift value(s).
770
+ """
771
+ if not self.__hasdata:
772
+ raise AttributeError('gamma surface data not set')
773
+ if 'delta' not in self.data:
774
+ raise AttributeError('delta data not set')
775
+
776
+ smooth = kwargs.pop('smooth', True)
777
+
778
+ # Convert x, y to a1, a2
779
+ if 'x' in kwargs:
780
+ x = kwargs.pop('x')
781
+ y = kwargs.pop('y')
782
+ a1vect = kwargs.pop('a1vect', None)
783
+ a2vect = kwargs.pop('a2vect', None)
784
+ xvect = kwargs.pop('xvect', None)
785
+ assert len(kwargs) == 0, 'Unknown/incompatible arguments given'
786
+ a1, a2 = self.xy_to_a12(x, y, a1vect=a1vect, a2vect=a2vect, xvect=xvect)
787
+
788
+ # Convert pos to a1, a2
789
+ elif 'pos' in kwargs:
790
+ pos = kwargs.pop('pos')
791
+ a1vect = kwargs.pop('a1vect', None)
792
+ a2vect = kwargs.pop('a2vect', None)
793
+ assert len(kwargs) == 0, 'Unknown/incompatible arguments given'
794
+ a1, a2 = self.pos_to_a12(pos, a1vect=a1vect, a2vect=a2vect)
795
+
796
+ # Get a1, a2 from kwargs
797
+ else:
798
+ a1 = np.array(kwargs.pop('a1'))
799
+ a2 = np.array(kwargs.pop('a2'))
800
+ a1vect = kwargs.pop('a1vect', None)
801
+ a2vect = kwargs.pop('a2vect', None)
802
+ assert len(kwargs) == 0, 'Unknown/incompatible arguments given'
803
+ if a1vect is not None or a2vect is not None:
804
+ shape = a1.shape
805
+ # Convert into pos using given a1vect, a2vect, then back into a1, a2
806
+ pos = self.a12_to_pos(a1, a2, a1vect=a1vect, a2vect=a2vect)
807
+ a1, a2 = self.pos_to_a12(pos)
808
+ a1 = a1.reshape(shape)
809
+ a2 = a2.reshape(shape)
810
+
811
+ # Wrap all values within 0.0 < a1, a2 < 1.0
812
+ while np.any(a1 > 1.0):
813
+ a1[a1 > 1.0] -= 1.0
814
+ while np.any(a1 < 0.0):
815
+ a1[a1 < 0.0] += 1.0
816
+ while np.any(a2 > 1.0):
817
+ a2[a2 > 1.0] -= 1.0
818
+ while np.any(a2 < 0.0):
819
+ a2[a2 < 0.0] += 1.0
820
+
821
+ if smooth:
822
+ return self.__delta_fit(a1, a2)
823
+ else:
824
+ return self.__delta_nearest(np.array([a1.flatten(), a2.flatten()]).T).reshape(a1.shape)
825
+
826
+ def E_gsf_surface_plot(self,
827
+ normalize: bool = False,
828
+ smooth: bool = True,
829
+ a1vect: Optional[npt.ArrayLike] = None,
830
+ a2vect: Optional[npt.ArrayLike] = None,
831
+ xvect: Optional[npt.ArrayLike] = None,
832
+ length_unit: str = 'Å',
833
+ energyperarea_unit: str = 'eV/Å^2',
834
+ numx: int = 100,
835
+ numy: int = 100,
836
+ figsize: Optional[tuple] = None,
837
+ **kwargs) -> plt.figure:
838
+ """
839
+ Creates a 2D surface plot from the stacking fault energy values.
840
+
841
+ Parameters
842
+ ----------
843
+ normalize : bool, optional
844
+ Flag indicating if axes are Cartesian (False, default) or
845
+ normalized by a1, a2 vectors (True).
846
+ smooth : bool, optional
847
+ If True (default), then plot shows smooth interpolated values.
848
+ If False, plot shows nearest raw data values.
849
+ a1vect : np.array, optional
850
+ Crystal vector for the a1 vector to use for plotting. Default
851
+ value of None uses the saved a1vect.
852
+ a2vect : np.array, optional
853
+ Crystal vector for the a2 vector to use for plotting. Default
854
+ value of None uses the saved a2vect.
855
+ xvect : numpy.array, optional
856
+ Crystal vector to align with the plotting x-axis for
857
+ non-normalized plots. If not given, this is taken as the Cartesian
858
+ of a1vect.
859
+ length_unit : str, optional
860
+ The unit of length to display non-normalized axes values in.
861
+ Default value is 'Å'.
862
+ energyperarea_unit : str, optional
863
+ The unit of energy per area to display the stacking fault energies
864
+ in. Default value is 'eV/Å^2'.
865
+ numx : int, optional
866
+ The number of plotting points to use along the x-axis. Default
867
+ value is 100.
868
+ numy : int, optional
869
+ The number of plotting points to use along the y-axis. Default
870
+ value is 100.
871
+ figsize : tuple or None, optional
872
+ The figure's x,y dimensions. If None (default), the values are
873
+ scaled such that the x,y spacings are approximately equal, and the
874
+ larger of the two values is set to 10.
875
+ **kwargs : dict, optional
876
+ Additional keywords are passed into the underlying
877
+ matplotlib.pyplot.pcolormesh(). This allows control of such things
878
+ like the colormap (cmap).
879
+
880
+ Returns
881
+ -------
882
+ matplotlib.figure
883
+ """
884
+ if not self.__hasdata:
885
+ raise AttributeError('gamma surface data not set')
886
+
887
+ # Extract data
888
+ if a1vect is None:
889
+ a1vect = self.a1vect
890
+ a1vect = np.asarray(a1vect)
891
+ if a2vect is None:
892
+ a2vect = self.a2vect
893
+ a2vect = np.asarray(a2vect)
894
+
895
+ # Generate grids of a1, a2 values from numx, numy
896
+ xvals = np.linspace(0, 1, numx)
897
+ yvals = np.linspace(0, 1, numy)
898
+ x_grid, y_grid = np.meshgrid(xvals, yvals)
899
+ x_gridc, y_gridc = np.meshgrid(xvals[:-1], yvals[:-1])
900
+
901
+ # Generate grid of values either with or without interpolation
902
+ C = self.E_gsf(a1=x_gridc, a2=y_gridc, a1vect=a1vect, a2vect=a2vect, smooth=smooth)
903
+
904
+ # Convert units of C using energyperarea_unit
905
+ C = uc.get_in_units(C, energyperarea_unit)
906
+
907
+ # Set parameters for normalized plots
908
+ if normalize is True:
909
+ yscale = 1
910
+ xlabel = f'$a_1$ = {a1vect}'
911
+ ylabel = f'$a_2$ = {a2vect}'
912
+
913
+ # Set parameters for absolute plots
914
+ else:
915
+ shape = x_grid.shape
916
+ x_grid, y_grid = self.a12_to_xy(x_grid.flatten(), y_grid.flatten(),
917
+ a1vect=a1vect, a2vect=a2vect, xvect=xvect)
918
+ x_grid.shape = shape
919
+ y_grid.shape = shape
920
+ x_grid = uc.get_in_units(x_grid, length_unit)
921
+ y_grid = uc.get_in_units(y_grid, length_unit)
922
+ yscale = (y_grid.max()-y_grid.min()) / (x_grid.max() - x_grid.min())
923
+ xlabel = f'$x$ along {a1vect} (${length_unit}$)'
924
+ ylabel = f'$y$ along {a2vect} (${length_unit}$)'
925
+
926
+ # Set default figsize if needed
927
+ if figsize is None:
928
+ xscale = 1.175
929
+ if yscale < 1:
930
+ figsize = (xscale * 10, 10 * yscale)
931
+ else:
932
+ figsize = (xscale * 10 / yscale, 10)
933
+
934
+ # Generate plot
935
+ fig = plt.figure(figsize=figsize)
936
+ plt.pcolormesh(x_grid, y_grid, C, **kwargs)
937
+ plt.xlabel(xlabel, fontsize='xx-large')
938
+ plt.ylabel(ylabel, fontsize='xx-large')
939
+ cbar = plt.colorbar(aspect=40, fraction=0.1)
940
+ cbar.ax.set_ylabel(f'$γ_{{gsf}}$ (${energyperarea_unit}$)', fontsize='x-large')
941
+
942
+ return fig
943
+
944
+ def E_gsf_line_plot(self,
945
+ vect: Optional[npt.ArrayLike] = None,
946
+ num: int = None,
947
+ smooth: bool = True,
948
+ length_unit: str = 'Å',
949
+ energyperarea_unit: str = 'eV/Å^2',
950
+ figsize: Optional[tuple] = None,
951
+ fig: Optional[plt.figure] = None,
952
+ **kwargs) -> plt.figure:
953
+ """
954
+ Generates a line plot for the interpolated generalized stacking fault
955
+ energy along a specified crystallographic vector in the (a1, a2) plane.
956
+
957
+ Parameters
958
+ ----------
959
+ vect : numpy.array, optional
960
+ Vector to plot the gsf along. If box is set, this vect will be a
961
+ lattice vector, otherwise it will be a Cartesian vector. Must be
962
+ in the plane defined by the GammaSurface object's a1vect and
963
+ a2vect vectors. Default value will use the set a1vect.
964
+ num : int, optional
965
+ The number of points to evaluate the generalized stacking fault
966
+ energy for. Default value is 100 if smooth is True, otherwise is
967
+ number of unique a1 values from 0 to 1.
968
+ smooth : bool, optional
969
+ If True (default), then plot shows smooth interpolated values.
970
+ If False, plot shows nearest raw data values.
971
+ length_unit : str, optional
972
+ The unit of length to display the x-axis coordinates in.
973
+ Default value is 'Å'.
974
+ energyperarea_unit : str, optional
975
+ The unit of energy per area to display the stacking fault energies
976
+ in. Default value is 'eV/Å^2'.
977
+ figsize : tuple, optional
978
+ The x,y size of the figure to return. Default value is (10, 6).
979
+ fig : matplotlib.figure, optional
980
+ An existing figure object to add the new plot to. If not given, a
981
+ new figure is generated.
982
+ **kwargs : dict, optional
983
+ Additional keywords are passed into the underlying
984
+ matplotlib.pyplot.plot(). This allows control of such things
985
+ like line color, style, etc.
986
+
987
+ Returns
988
+ -------
989
+ matplotlib.figure
990
+ """
991
+ if not self.__hasdata:
992
+ raise AttributeError('gamma surface data not set')
993
+
994
+ if num is None:
995
+ if smooth:
996
+ num = 100
997
+ else:
998
+ unique_a1 = np.unique([self.data.a1 - 1, self.data.a1, self.data.a1 + 1])
999
+ num = len(unique_a1[(unique_a1 >=-0.000001) & (unique_a1 <=1.000001)])
1000
+
1001
+ # Generate coordinates
1002
+ a1 = np.linspace(0, 1, num)
1003
+ a2 = np.zeros(num)
1004
+ pos = self.a12_to_pos(a1, a2, a1vect=vect)
1005
+
1006
+ # Evaluate interpolated energy and distance along x
1007
+ E = uc.get_in_units(self.E_gsf(pos=pos, smooth=smooth), energyperarea_unit)
1008
+ x = uc.get_in_units(np.linalg.norm(pos, axis=1), length_unit)
1009
+
1010
+ # Create plot
1011
+ xmax = x.max()
1012
+ emin = E.min() * 1.05
1013
+ emax = E.max() * 1.05
1014
+ if fig is None:
1015
+ if figsize is None:
1016
+ figsize = (10, 6)
1017
+ fig = plt.figure(figsize=figsize)
1018
+ else:
1019
+ old_xmax = fig.axes[0].get_xlim()[-1]
1020
+ old_emin = fig.axes[0].get_ylim()[0]
1021
+ old_emax = fig.axes[0].get_ylim()[-1]
1022
+ if old_xmax > xmax:
1023
+ xmax = old_xmax
1024
+ if old_emin < emin:
1025
+ emin = old_emin
1026
+ if old_emax > emax:
1027
+ emax = old_emax
1028
+ if 'fmt' in kwargs:
1029
+ fmt = kwargs.pop('fmt')
1030
+ plt.plot(x, E, fmt, **kwargs)
1031
+ else:
1032
+ plt.plot(x, E, **kwargs)
1033
+
1034
+ if vect is None:
1035
+ vect = self.a1vect
1036
+
1037
+ plt.xlabel(f'$x$ along {vect} (${length_unit}$)', fontsize='x-large')
1038
+ plt.ylabel(f'$γ_{{gsf}}$ (${energyperarea_unit}$)', fontsize='x-large')
1039
+ plt.xlim(0, xmax)
1040
+ plt.ylim(emin, emax)
1041
+
1042
+ return fig
1043
+
1044
+ def delta_surface_plot(self,
1045
+ normalize: bool = False,
1046
+ smooth: bool = True,
1047
+ a1vect: Optional[npt.ArrayLike] = None,
1048
+ a2vect: Optional[npt.ArrayLike] = None,
1049
+ xvect: Optional[npt.ArrayLike] = None,
1050
+ length_unit: str = 'Å',
1051
+ numx: int = 100,
1052
+ numy: int = 100,
1053
+ figsize: Optional[tuple] = None,
1054
+ **kwargs) -> plt.figure:
1055
+ """
1056
+ Creates a 2D surface plot from the delta planar displacement values.
1057
+
1058
+ Parameters
1059
+ ----------
1060
+ normalize : bool, optional
1061
+ Flag indicating if axes are Cartesian (False, default) or
1062
+ normalized by a1, a2 vectors (True).
1063
+ smooth : bool, optional
1064
+ If True (default), then plot shows smooth interpolated values.
1065
+ If False, plot shows nearest raw data values.
1066
+ a1vect : np.array, optional
1067
+ Crystal vector for the a1 vector to use for plotting. Default
1068
+ value of None uses the saved a1vect.
1069
+ a2vect : np.array, optional
1070
+ Crystal vector for the a2 vector to use for plotting. Default
1071
+ value of None uses the saved a2vect.
1072
+ xvect : numpy.array, optional
1073
+ Crystal vector to align with the plotting x-axis for
1074
+ non-normalized plots. If not given, this is taken as the Cartesian
1075
+ of a1vect.
1076
+ length_unit : str, optional
1077
+ The unit of length to display delta and non-normalized axes values
1078
+ in. Default value is 'Å'.
1079
+ numx : int, optional
1080
+ The number of plotting points to use along the x-axis. Default
1081
+ value is 100.
1082
+ numy : int, optional
1083
+ The number of plotting points to use along the y-axis. Default
1084
+ value is 100.
1085
+ figsize : tuple or None, optional
1086
+ The figure's x,y dimensions. If None (default), the values are
1087
+ scaled such that the x,y spacings are approximately equal, and the
1088
+ larger of the two values is set to 10.
1089
+ **kwargs : dict, optional
1090
+ Additional keywords are passed into the underlying
1091
+ matplotlib.pyplot.pcolormesh(). This allows control of such things
1092
+ like the colormap (cmap).
1093
+
1094
+ Returns
1095
+ -------
1096
+ matplotlib.figure
1097
+ """
1098
+ if not self.__hasdata:
1099
+ raise AttributeError('gamma surface data not set')
1100
+ if 'delta' not in self.data:
1101
+ raise AttributeError('delta data not set')
1102
+
1103
+ # Extract data
1104
+ if a1vect is None:
1105
+ a1vect = self.a1vect
1106
+ a1vect = np.asarray(a1vect)
1107
+ if a2vect is None:
1108
+ a2vect = self.a2vect
1109
+ a2vect = np.asarray(a2vect)
1110
+
1111
+ # Generate grids of a1, a2 values from numx, numy
1112
+ x_grid, y_grid = np.meshgrid(np.linspace(0, 1, numx),
1113
+ np.linspace(0, 1, numy))
1114
+
1115
+ # Generate grid of values either with or without interpolation
1116
+ C = self.delta(a1=x_grid, a2=y_grid, a1vect=a1vect, a2vect=a2vect, smooth=smooth)
1117
+
1118
+ # Convert units of C using length_unit
1119
+ C = uc.get_in_units(C, length_unit)
1120
+
1121
+ # Set parameters for normalized plots
1122
+ if normalize is True:
1123
+ yscale = 1
1124
+ xlabel = f'$a_1$ = {a1vect}'
1125
+ ylabel = f'$a_2$ = {a2vect}'
1126
+
1127
+ # Set parameters for absolute plots
1128
+ else:
1129
+ shape = x_grid.shape
1130
+ x_grid, y_grid = self.a12_to_xy(x_grid.flatten(), y_grid.flatten(),
1131
+ a1vect=a1vect, a2vect=a2vect, xvect=xvect)
1132
+ x_grid.shape = shape
1133
+ y_grid.shape = shape
1134
+ x_grid = uc.get_in_units(x_grid, length_unit)
1135
+ y_grid = uc.get_in_units(y_grid, length_unit)
1136
+ yscale = (y_grid.max()-y_grid.min()) / (x_grid.max() - x_grid.min())
1137
+ xlabel = f'x (${length_unit}$)'
1138
+ ylabel = f'y (${length_unit}$)'
1139
+
1140
+ # Set default figsize if needed
1141
+ if figsize is None:
1142
+ xscale = 1.175
1143
+ if yscale < 1:
1144
+ figsize = (xscale * 10, 10 * yscale)
1145
+ else:
1146
+ figsize = (xscale * 10 / yscale, 10)
1147
+
1148
+ # Generate plot
1149
+ fig = plt.figure(figsize=figsize)
1150
+ plt.pcolormesh(x_grid, y_grid, C, **kwargs)
1151
+ plt.xlabel(xlabel, fontsize='x-large')
1152
+ plt.ylabel(ylabel, fontsize='x-large')
1153
+ cbar = plt.colorbar(aspect=40, fraction=0.1)
1154
+ cbar.ax.set_ylabel(f'$δ_{{gsf}}$ (${length_unit}$)', fontsize='x-large')
1155
+
1156
+ return fig
1157
+
1158
+ def delta_line_plot(self,
1159
+ vect: Optional[npt.ArrayLike] = None,
1160
+ num: Optional[int] = None,
1161
+ smooth: bool = True,
1162
+ length_unit: str = 'Å',
1163
+ figsize: Optional[tuple] = None,
1164
+ fig: Optional[plt.figure] = None,
1165
+ **kwargs) -> plt.figure:
1166
+ """
1167
+ Generates a line plot for the interpolated delta planar shift values
1168
+ along a specified crystallographic vector in the (a1, a2) plane.
1169
+
1170
+ Parameters
1171
+ ----------
1172
+ vect : numpy.array, optional
1173
+ Vector to plot the gsf along. If box is set, this vect will be a
1174
+ lattice vector, otherwise it will be a Cartesian vector. Must be
1175
+ in the plane defined by the GammaSurface object's a1vect and
1176
+ a2vect vectors. Default value will use the set a1vect.
1177
+ num : int, optional
1178
+ The number of points to evaluate the generalized stacking fault
1179
+ energy for. Default value is 100 if smooth is True, otherwise is
1180
+ number of unique a1 values from 0 to 1.
1181
+ smooth : bool, optional
1182
+ If True (default), then plot shows smooth interpolated values.
1183
+ If False, plot shows nearest raw data values.
1184
+ length_unit : str, optional
1185
+ The unit of length to display the x-axis coordinates in.
1186
+ Default value is 'Å'.
1187
+ figsize : tuple, optional
1188
+ The x,y size of the figure to return. Default value is (10, 6).
1189
+ fig : matplotlib.figure, optional
1190
+ An existing figure object to add the new plot to. If not given, a
1191
+ new figure is generated.
1192
+ **kwargs : dict, optional
1193
+ Additional keywords are passed into the underlying
1194
+ matplotlib.pyplot.plot(). This allows control of such things
1195
+ like line color, style, etc.
1196
+
1197
+ Returns
1198
+ -------
1199
+ matplotlib.figure
1200
+ """
1201
+ if not self.__hasdata:
1202
+ raise AttributeError('gamma surface data not set')
1203
+ if 'delta' not in self.data:
1204
+ raise AttributeError('delta data not set')
1205
+
1206
+ if num is None:
1207
+ if smooth:
1208
+ num = 100
1209
+ else:
1210
+ unique_a1 = np.unique([self.data.a1-1, self.data.a1, self.data.a1+1])
1211
+ num = len(unique_a1[(unique_a1 >=-0.000001) & (unique_a1 <=1.000001)])
1212
+
1213
+ # Generate coordinates
1214
+ a1 = np.linspace(0, 1, num)
1215
+ a2 = np.zeros(num)
1216
+ pos = self.a12_to_pos(a1, a2, a1vect=vect)
1217
+
1218
+ # Evaluate interpolated distance along x
1219
+ d = uc.get_in_units(self.delta(pos=pos, smooth=smooth), length_unit)
1220
+ x = uc.get_in_units(np.linalg.norm(pos, axis=1), length_unit)
1221
+
1222
+ # Create plot
1223
+ xmax = x.max()
1224
+ dmin = d.min() * 1.05
1225
+ dmax = d.max() * 1.05
1226
+ if fig is None:
1227
+ if figsize is None:
1228
+ figsize = (10, 6)
1229
+ fig = plt.figure(figsize=figsize)
1230
+ else:
1231
+ old_xmax = fig.axes[0].get_xlim()[-1]
1232
+ old_dmin = fig.axes[0].get_ylim()[0]
1233
+ old_dmax = fig.axes[0].get_ylim()[-1]
1234
+ if old_xmax > xmax:
1235
+ xmax = old_xmax
1236
+ if old_dmin < dmin:
1237
+ dmin = old_dmin
1238
+ if old_dmax > dmax:
1239
+ dmax = old_dmax
1240
+ if 'fmt' in kwargs:
1241
+ fmt = kwargs.pop('fmt')
1242
+ plt.plot(x, d, fmt, **kwargs)
1243
+ else:
1244
+ plt.plot(x, d, **kwargs)
1245
+
1246
+ if vect is None:
1247
+ vect = self.a1vect
1248
+
1249
+ plt.xlabel(f'$x$ along {vect} (${length_unit}$)', fontsize='x-large')
1250
+ plt.ylabel(f'$δ_{{gsf}}$ (${length_unit}$)', fontsize='x-large')
1251
+ plt.xlim(0, xmax)
1252
+ plt.ylim(dmin, dmax)
1253
+
1254
+ return fig
1255
+
1256
+ def path(self,
1257
+ coord: npt.ArrayLike,
1258
+ style: str = 'ISM',
1259
+ gradientfxn: str = 'cdiff',
1260
+ gradientkwargs: Optional[dict] = None,
1261
+ integratorfxn: str ='rk') -> BasePath:
1262
+ """
1263
+ Creates an mep Path object mapping for the gamma surface based on
1264
+ supplied xy coordinates along the path line.
1265
+
1266
+ Parameters
1267
+ ----------
1268
+ coord : array-like object
1269
+ The xy coordinates of the points along the path.
1270
+ style : str
1271
+ The path/relaxer style to use. Default value of 'ISM' will use improved string method.
1272
+ gradientfxn : function, optional
1273
+ The function to use to estimate the gradient of the energy. Default
1274
+ value of 'cdiff' will use atomman.mep.gradient.central_difference
1275
+ gradientkwargs : dict or None, optional
1276
+ The keyword arguments (i.e. settings) to use with the gradientfxn.
1277
+ Default value of None will use {'shift':1e-7}.
1278
+ integratorfxn : str or function, optional
1279
+ The function to use to integrate relaxation steps. Default value of
1280
+ 'rk' will use atomman.mep.integrator.rungekutta.
1281
+
1282
+ Returns
1283
+ -------
1284
+ subclass of atomman.mep.BasePath
1285
+ Specific class dictated by style: style=='ISM' -> ISMPath (only style currently).
1286
+ """
1287
+ # Handle default values
1288
+ if gradientkwargs is None:
1289
+ gradientkwargs = {'shift':1e-7}
1290
+
1291
+ # Define energyfxn for the path
1292
+ def energyfxn(xy):
1293
+ return self.E_gsf(x=xy[..., 0], y=xy[..., 1])
1294
+
1295
+ return create_path(coord, energyfxn, style=style, gradientfxn=gradientfxn,
1296
+ gradientkwargs=gradientkwargs, integratorfxn=integratorfxn)
1297
+
1298
+ def build_path(self,
1299
+ pos: npt.ArrayLike,
1300
+ npoints: int = 31,
1301
+ style: str = 'ISM',
1302
+ gradientfxn: str = 'cdiff',
1303
+ gradientkwargs: Optional[dict] = None,
1304
+ integratorfxn: str = 'rk') -> BasePath:
1305
+ """
1306
+ Builds a subclass of atomman.mep.BasePath as one or two line segments
1307
+ along a gamma surface. The energy function along the path will be
1308
+ properly set using E_gsf.
1309
+
1310
+ Parameters
1311
+ ----------
1312
+ pos : array-like object
1313
+ 2x3 or 3x3 array of Miller vector points that defines the end points of the path's
1314
+ line segment(s).
1315
+ npoints : int, optional
1316
+ The number of points to include along the path. Must be odd if three pos are used.
1317
+ Default value is 31.
1318
+ style : str
1319
+ The path/relaxer style to use. Default value of 'ISM' will use improved string method.
1320
+ gradientfxn : function, optional
1321
+ The function to use to estimate the gradient of the energy. Default
1322
+ value of 'cdiff' will use atomman.mep.gradient.central_difference
1323
+ gradientkwargs : dict or None, optional
1324
+ The keyword arguments (i.e. settings) to use with the gradientfxn.
1325
+ Default value of None will use {'shift':1e-7}.
1326
+ integratorfxn : str or function, optional
1327
+ The function to use to integrate relaxation steps. Default value of
1328
+ 'rk' will use atomman.mep.integrator.rungekutta.
1329
+
1330
+ Returns
1331
+ -------
1332
+ subclass of atomman.mep.BasePath
1333
+ Specific class dictated by style: style=='ISM' -> ISMPath (only style currently).
1334
+ """
1335
+
1336
+ # Define path coordinate builder
1337
+ def build_coord(startpos, endpos, npoints):
1338
+
1339
+ box = self.box
1340
+ x, y = self.pos_to_xy(miller.vector_crystal_to_cartesian(startpos, box))
1341
+ start_xy = np.array([x, y])
1342
+
1343
+ x, y = self.pos_to_xy(miller.vector_crystal_to_cartesian(endpos, box))
1344
+ end_xy = np.array([x, y])
1345
+
1346
+ return np.vstack([np.linspace(start_xy[0], end_xy[0], npoints),
1347
+ np.linspace(start_xy[1], end_xy[1], npoints)]).T
1348
+
1349
+ # Construct single segment path coordinates
1350
+ if len(pos) == 2:
1351
+ coord = build_coord(pos[0], pos[1], npoints)
1352
+
1353
+ # Construct double segment path coordinates
1354
+ elif len(pos) == 3:
1355
+ assert npoints % 2 == 1, 'npoints must be odd'
1356
+ npoints_2 = int(npoints + 1 / 2)
1357
+ coorda = build_coord(pos[0], pos[1], npoints_2)
1358
+ coordb = build_coord(pos[1], pos[2], npoints_2)
1359
+ coord = np.vstack([coorda, coordb[1:]])
1360
+
1361
+ else:
1362
+ raise ValueError('pos must have 2 or 3 coordinates')
1363
+
1364
+ return self.path(coord, style=style, gradientfxn=gradientfxn,
1365
+ gradientkwargs=gradientkwargs, integratorfxn=integratorfxn)
1366
+
atomman/source/atomman/defect/GrainBoundary.py ADDED
@@ -0,0 +1,236 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python imports
3
+ from typing import Optional, Tuple
4
+
5
+ from scipy.spatial.transform import Rotation
6
+
7
+ # http://www.numpy.org/
8
+ import numpy as np
9
+ import numpy.typing as npt
10
+
11
+ from DataModelDict import DataModelDict as DM
12
+
13
+ # Local imports
14
+ from .Boundary import Boundary
15
+ from .TiltGrainBoundaryHelper import TiltGrainBoundaryHelper
16
+ from .. import System, Box
17
+ from ..tools import approx_rational
18
+ from ..library.record.GrainBoundary import GrainBoundary as GBRecord
19
+
20
+
21
+ class GrainBoundary(Boundary):
22
+ """
23
+ Class for generating systems to investigate grain boundaries. In comparison
24
+ to the more generic Boundary class, GrainBoundary only accepts one unit cell,
25
+ it limits configurations to zero-strain states, and it includes extra methods
26
+ and attributes specific to grain boundary configurations.
27
+ """
28
+
29
+ def __init__(self,
30
+ ucell: System,
31
+ uvws1,
32
+ uvws2,
33
+ conventional_setting: str = 'p',
34
+ cutboxvector='c',
35
+ maxmult: int = 10):
36
+ """
37
+ Initialize a grain boundary configuration.
38
+
39
+ Parameters
40
+ ----------
41
+ ucell : atomman.System
42
+ The reference unit cell to use for both grains.
43
+ uvws1 : array-like object
44
+ The three Miller(-Bravais) crystal vectors of ucell to use for
45
+ orienting the first grain such that each crystal vector will be
46
+ aligned with one of the box vectors of the final configuration.
47
+ uvws2 : array-like object
48
+ The three Miller(-Bravais) crystal vectors of ucell to use for
49
+ orienting the second grain such that each crystal vector will be
50
+ aligned with one of the box vectors of the final configuration.
51
+ conventional_setting : str, optional
52
+ Specifies which conventional lattice setting that ucell is in.
53
+ The default value of 'p' takes ucell to be primitive, in which case
54
+ the uvws1 and uvws2 values must be integers. This must be specified
55
+ in order to access non-integer lattice vectors for the uvws.
56
+ cutboxvector : str, optional
57
+ Indicates which of the three box vectors that the boundary will
58
+ be placed along. Default value is 'c'.
59
+ maxmult : int, optional
60
+ The max integer multiplier to use for the zero strain check.
61
+ """
62
+ # Call Boundary's init using the single ucell
63
+ super().__init__(ucell, ucell, uvws1, uvws2,
64
+ conventional_setting1=conventional_setting,
65
+ conventional_setting2=conventional_setting,
66
+ cutboxvector=cutboxvector,
67
+ zerostrain=True, maxmult=maxmult)
68
+
69
+ # Find the transformation for rcell1 to rcell2
70
+ rvects1 = self.transform_p1_to_r1.apply(self.ucell.box.vects)
71
+ rvects2 = self.transform_p2_to_r2.apply(self.ucell.box.vects)
72
+ self.transform = np.linalg.lstsq(rvects1, rvects2, rcond=None)[0].T
73
+ self.__transform_r1_to_r2 = Rotation.from_matrix(np.linalg.lstsq(rvects1, rvects2, rcond=None)[0].T)
74
+
75
+ @classmethod
76
+ def from_model(cls,
77
+ ucell: System,
78
+ record: GBRecord,
79
+ maxmult: int = 10):
80
+ """
81
+ Allows for the grain boundary settings to be loaded from a grain_boundary
82
+ record rather than manually specifying the inputs.
83
+
84
+ Parameters
85
+ ----------
86
+ ucell : atomman.System
87
+ The reference unit cell to use for both grains.
88
+ record : str, path, DataModelDict.DataModelDict or atomman.library.record.GrainBoundary
89
+ A grain_boundary-style record containing the input settings to load.
90
+ Can be given as a file path, str file contents, DataModelDict contents,
91
+ or a GrainBoundary record object.
92
+ maxmult : int, optional
93
+ The max integer multiplier to use for the zero strain check.
94
+
95
+ Returns
96
+ -------
97
+ atomman.defect.GrainBoundary
98
+ """
99
+ # Extract inputs from the grain_boundary record
100
+ if not isinstance(record, GBRecord):
101
+ record = GBRecord(model = record)
102
+ obj = cls(ucell, maxmult=maxmult, **record.parameters)
103
+
104
+ return obj
105
+
106
+ @classmethod
107
+ def symmetric_tilt(cls,
108
+ ucell: System,
109
+ axis_uvw: npt.ArrayLike,
110
+ plane1_hkl: Optional[npt.ArrayLike] = None,
111
+ in1_uvw: Optional[npt.ArrayLike] = None,
112
+ conventional_setting: str = 'p',
113
+ ref_hkl: Optional[npt.ArrayLike] = None,
114
+ ref_uvw: Optional[npt.ArrayLike] = None,
115
+ cutboxvector: str = 'c',
116
+ maxindex: int = 20,
117
+ maxmult: int = 10,
118
+ tol: float = 1e-8):
119
+ """
120
+ Initialize for symmetric tilt grain boundary configurations.
121
+
122
+ Parameters
123
+ ----------
124
+ ucell : atomman.System
125
+ The unit cell to use for both top and bottom grains.
126
+ axis_uvw : array-like
127
+ The Miller [uvw] or Miller-Bravais [uvtw] crystal vector of ucell
128
+ that serves as the tilt axis. The tilt axis is a common vector in
129
+ the grain boundary plane that is shared by both grains.
130
+ in1_uvw : array-like object
131
+ The first grain's in-plane vector given as a Miller or
132
+ Miller-Bravais crystal vector.
133
+ ref_uvw : array-like object or None, optional
134
+ A reference in-plane vector that when crossed with the tilt axis
135
+ identifies the grain boundary plane associated with the 0 degree
136
+ misorientation configuration. If given, in2 is found such
137
+ that the two grain boundary planes have the same angle wrt the
138
+ reference plane. If None (default), then in2 is selected based on
139
+ producing the smallest misorientation angle, which usually but not
140
+ always corresponds to a symmetric tilt boundary.
141
+ cutboxvector : str, optional
142
+ Sets the alignment of the uvws to the final configuration's box
143
+ vectors by specifying which of the box vectors is out-of-plane
144
+ with respect to the grain boundary plane. Default value is 'c',
145
+ which aligns the tilt axis with a, and in1 (and in2) with b.
146
+ maxindex : int, optional
147
+ The maximum absolute vector index to search over for the u,v,w
148
+ values: u, v, and w can all independently vary from
149
+ -maxindex to maxindex. Default value is 20.
150
+ maxmult : int, optional
151
+ The max integer multiplier to use for the zero strain check if
152
+ zerostrain is True.
153
+ tol : float, optional
154
+ A floating point tolerance used in finding the compatible primitive
155
+ unit cell. Available as a parameter in case issues are encountered.
156
+ Default value is 1e-8.
157
+
158
+ Returns
159
+ -------
160
+ atomman.defect.GrainBoundary
161
+ """
162
+ # Initialize helper
163
+ helper = TiltGrainBoundaryHelper(ucell, axis_uvw,
164
+ conventional_setting=conventional_setting,
165
+ ref_uvw=ref_uvw, ref_hkl=ref_hkl, tol=tol)
166
+
167
+ # Identify rotation uvws
168
+ uvws1, uvws2 = helper.symmetric_uvws(plane1_hkl=plane1_hkl, in1_uvw=in1_uvw,
169
+ cutboxvector=cutboxvector,
170
+ maxindex=maxindex)
171
+ obj = cls(ucell, uvws1, uvws2, conventional_setting=conventional_setting,
172
+ cutboxvector=cutboxvector)
173
+
174
+ return obj
175
+
176
+ @property
177
+ def ucell(self) -> System:
178
+ """atomman.System: The conventional reference unit cell for both grains. Alias for ucell1 and ucell2"""
179
+ return self.ucell1
180
+
181
+ @property
182
+ def ucell_prim(self) -> System:
183
+ """atomman.System: The primitive reference unit cell for both grains. Alias for ucell_prim1 and ucell_prim2"""
184
+ return self.ucell_prim1
185
+
186
+ @property
187
+ def transform_r1_to_r2(self) -> Rotation:
188
+ """scipy.spatial.transform.Rotation: The Cartesian rotation associated with rcell1 to rcell2"""
189
+ return self.__transform_r1_to_r2
190
+
191
+ @property
192
+ def misorientation(self) -> float:
193
+ """float : The misorientation angle for the grain boundary"""
194
+ return np.linalg.norm(self.transform_r1_to_r2.as_rotvec(degrees=True))
195
+
196
+ def sigma(self,
197
+ tol: float = 1e-6) -> int:
198
+ """Compute the sigma factor for the grain boundary"""
199
+
200
+ t1 = self.transform_r1_to_r2.as_matrix()
201
+ t2 = self.transform_r1_to_r2.inv().as_matrix()
202
+
203
+ denominators = approx_rational(np.vstack([t1, t2]).flatten(), tol=tol)[1]
204
+
205
+ # Sigma is the least common multiplier of the denominators
206
+ return np.lcm.reduce(denominators)
207
+
208
+ def dlat(self,
209
+ precision: int = 6) -> float:
210
+ """
211
+ Computes the lattice thickness for the grain boundary by building a
212
+ lattice system for the primitive cell and finding the distance between
213
+ atoms perpendicular to the grain boundary plane.
214
+
215
+ Parameters
216
+ ----------
217
+ precision : int, optional
218
+ The rounding precision to use in identifying unique atom
219
+ coordinates. Default value is 6.
220
+
221
+ Returns
222
+ -------
223
+ float
224
+ The distance between two lattice atom positions in the direction
225
+ perpendicular to the grain boundary plane.
226
+ """
227
+ # Construct a lattice system from the primitive unit cell and 1 atom
228
+ latticesystem = System(box=Box(vects=self.ucell_prim.box.vects))
229
+
230
+ # Rotate and supersize
231
+ testsystem = latticesystem.rotate(self.uvws_prim1).supersize(2, 2, 2)
232
+
233
+ # Find the difference between the two nearest atom positions perpendicular to the cut
234
+ unique_z = sorted(list(set(testsystem.atoms.pos[:, self.cutindex].round(precision))))
235
+ return unique_z[1] - unique_z[0]
236
+
atomman/source/atomman/defect/InterstitialSite.py ADDED
@@ -0,0 +1,168 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+
3
+ from scipy.spatial import Voronoi, ConvexHull
4
+
5
+ import numpy as np
6
+
7
+ from .. import Atoms
8
+
9
+ class InterstitialSite():
10
+
11
+ def __init__(self,
12
+ pos: np.ndarray,
13
+ neighbor_atoms: Atoms):
14
+ """
15
+ Initializes an InterstitialSite object
16
+
17
+ Parameters
18
+ ----------
19
+ pos : numpy.ndarray
20
+ The coordinates for the interstitial position
21
+ neighbor_pos : numpy.ndarray
22
+ The coordinates for the atoms that neighbor the interstitial.
23
+ These should correspond to either direct or replica atoms from
24
+ the source atomic system.
25
+ """
26
+ self.__pos = pos
27
+ self.__neighbor_atoms = neighbor_atoms
28
+
29
+ def __eq__(self, other):
30
+ """
31
+ Compare InterstitialSites using is_similar() with the default settings.
32
+ """
33
+ return self.is_similar(other)
34
+
35
+
36
+ def is_similar(self,
37
+ other,
38
+ decimals: int = 6,
39
+ use_dmag: bool = False) -> bool:
40
+ """
41
+ Compares agains another InterstitialSite based on the neighbor atoms'
42
+ atype and dvect or dmag values.
43
+
44
+
45
+ Parameters
46
+ ----------
47
+ other : InterstitialSite
48
+ The other InterstitialSite to compare against.
49
+ decimals : int, optional
50
+ The number of decimal points to round the dvect or dmag values to
51
+ before comparing. Default value is 6.
52
+ use_dmag : bool
53
+ If True then the comparison will use atype and dmag. If False
54
+ (default) then the comparison will use atype and dvect. Roughly,
55
+ this means that setting this to True will perform a rotation
56
+ invariant comparison while leaving it False will not.
57
+ """
58
+ # First check number of atoms
59
+ if self.neighbor_atoms.natoms != other.neighbor_atoms.natoms:
60
+ return False
61
+
62
+ atype0 = self.neighbor_atoms.atype
63
+ atype1 = other.neighbor_atoms.atype
64
+
65
+ if use_dmag:
66
+ # Extract and round dmag values
67
+ d0 = np.round(self.neighbor_dmag, decimals=decimals)
68
+ d1 = np.round(other.neighbor_dmag, decimals=decimals)
69
+
70
+ # Get sorting indices
71
+ sorted_indices0 = np.lexsort((d0, atype0))
72
+ sorted_indices1 = np.lexsort((d1, atype1))
73
+
74
+ else:
75
+ # Extract and round dvect values
76
+ d0 = np.round(self.neighbor_dvect, decimals=decimals)
77
+ d1 = np.round(other.neighbor_dvect, decimals=decimals)
78
+
79
+ # Get sorting indices
80
+ sorted_indices0 = np.lexsort((d0[:, 2], d0[:, 1], d0[:, 0], atype0))
81
+ sorted_indices1 = np.lexsort((d1[:, 2], d1[:, 1], d1[:, 0], atype1))
82
+
83
+ # Sort the arrays
84
+ atype0 = atype0[sorted_indices0]
85
+ atype1 = atype1[sorted_indices1]
86
+ d0 = d0[sorted_indices0]
87
+ d1 = d1[sorted_indices1]
88
+
89
+ # Compare
90
+ return np.allclose(atype0, atype1) and np.allclose(d0, d1)
91
+
92
+ @property
93
+ def pos(self):
94
+ return self.__pos
95
+
96
+ @property
97
+ def neighbor_atoms(self):
98
+ return self.__neighbor_atoms
99
+
100
+ @property
101
+ def volume(self):
102
+ return ConvexHull(self.neighbor_atoms.pos).volume
103
+
104
+ @property
105
+ def neighbor_dvect(self):
106
+ return self.neighbor_atoms.pos - self.pos
107
+
108
+ @property
109
+ def neighbor_dmag(self):
110
+ return np.linalg.norm(self.neighbor_dvect, axis=1)
111
+
112
+ def is_strained(self, rtol=1e-05, atol=1e-08):
113
+ return not np.allclose(self.neighbor_dmag, self.neighbor_dmag[0])
114
+
115
+ def asdict(self, rtol=1e-05, atol=1e-08):
116
+ d = {
117
+ 'pos[0]': self.pos[0],
118
+ 'pos[1]': self.pos[1],
119
+ 'pos[2]': self.pos[2],
120
+ '#neighbors': self.neighbor_atoms.natoms,
121
+ 'volume': self.volume,
122
+ 'strained': self.is_strained(rtol=rtol, atol=atol)
123
+ }
124
+ return d
125
+
126
+ def interstitial_site_finder(system):
127
+ """
128
+ Generates a list of interstitial sites for an atomic configuration using
129
+ a Voronoi analysis.
130
+
131
+ Parameters
132
+ ----------
133
+ system : atomman.System
134
+ The atomic configuration to search for interstitial sites.
135
+
136
+ Returns
137
+ -------
138
+ list of atomman.defect.InterstitialSite
139
+ The identified interstitial sites.
140
+ """
141
+
142
+ # Supersize the system in all directions
143
+ bigsystem = system.supersize((-1,2), (-1,2), (-1,2))
144
+
145
+ # Compute the Voronoi analysis
146
+ vor = Voronoi(bigsystem.atoms.pos)
147
+
148
+ # Filter out the Voronoi vertices that are not in the middle replica
149
+ isin_ucell = system.box.inside(vor.vertices + .0005)
150
+ vertices_pos = vor.vertices[isin_ucell]
151
+ vertices_ids = np.where(isin_ucell)[0] # ids of vor.vertices that correspond to vertices
152
+
153
+ # Initialize neighbors lists
154
+ neighborlists = [ [] for nix in range(np.sum(isin_ucell))]
155
+
156
+ # Search for all atoms that neighbor the vertices
157
+ for atom_id, region_id in enumerate(vor.point_region):
158
+ region_vertices_ids = vor.regions[region_id]
159
+ for vertex_index, vertex_id in enumerate(vertices_ids):
160
+ if vertex_id in region_vertices_ids:
161
+ neighborlists[vertex_index].append(atom_id)
162
+
163
+ interstitialsites = []
164
+ for vertex_pos, neighborlist in zip(vertices_pos, neighborlists):
165
+ neighbor_atoms = bigsystem.atoms[neighborlist]
166
+ interstitialsites.append(InterstitialSite(vertex_pos, neighbor_atoms))
167
+
168
+ return interstitialsites
atomman/source/atomman/defect/IsotropicVolterraDislocation.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ import warnings
4
+ from typing import Optional, Union
5
+
6
+ # http://www.numpy.org/
7
+ import numpy as np
8
+ import numpy.typing as npt
9
+
10
+ # atomman imports
11
+ from . import VolterraDislocation
12
+ from .. import Box, ElasticConstants
13
+
14
+ class IsotropicVolterraDislocation(VolterraDislocation):
15
+ """
16
+ Class for representing the isotropic Volterra solution for a straight dislocation.
17
+ """
18
+
19
+ def solve(self,
20
+ C: ElasticConstants,
21
+ burgers: npt.ArrayLike,
22
+ ξ_uvw: Optional[npt.ArrayLike] = None,
23
+ slip_hkl: Optional[npt.ArrayLike] = None,
24
+ transform: Optional[npt.ArrayLike] = None,
25
+ axes: Optional[npt.ArrayLike] = None,
26
+ box: Optional[Box] = None,
27
+ m: Union[str, npt.ArrayLike] = 'x',
28
+ n: Union[str, npt.ArrayLike] = 'y',
29
+ cart_axes: bool = False,
30
+ tol: float = 1e-8):
31
+ """
32
+ Computes the elastic solution for an isotropic volterra dislocation.
33
+
34
+ Parameters
35
+ ----------
36
+ C : atomman.ElasticConstants
37
+ The medium's elastic constants.
38
+ burgers : array-like object
39
+ The dislocation's Burgers vector.
40
+ ξ_uvw : array-like object
41
+ The Miller crystal vector associated with the dislocation's line
42
+ direction. Must be given with slip_hkl to identify the
43
+ transformation matrix to use on C and burgers.
44
+ slip_hkl : array-like object
45
+ The Miller plane indices associated with the dislocation's slip
46
+ plane. Must be given with slip_hkl to identify the
47
+ transformation matrix to use on C and burgers.
48
+ transform : array-like object, optional
49
+ A 3x3 set of orthogonal Cartesian vectors that define the
50
+ transformation matrix to use on C and burgers to convert from the
51
+ standard (unit cell) and dislocation orientations. The 3 vectors
52
+ will automatically be converted into unit vectors. Using this is
53
+ an alternative to using ξ_uvw and slip_hkl.
54
+ axes : array-like object, optional
55
+ Same as transform. Retained for backwards compatibility.
56
+ box : atomman.Box, optional
57
+ The unit cell's box that crystal vectors are taken with respect to.
58
+ If not given, will use a cubic box with a=1 meaning that burgers,
59
+ ξ_uvw and slip_hkl will be interpreted as Cartesian vectors.
60
+ m : str or array-like object, optional
61
+ The 3D Cartesian unit vector to align with the dislocation solution's m-axis,
62
+ i.e. the in-plane direction perpendicular to the dislocation line. Also
63
+ accepts str values of 'x', 'y', or 'z', in which case the dislocation axis will
64
+ be aligned with the corresponding Cartesian axis. Default value is 'x'.
65
+ n : str or array-like object, optional
66
+ The 3D Cartesian unit vector to align with the dislocation solution's n-axis,
67
+ i.e. the slip plane normal. Also accepts str values of 'x', 'y', or 'z', in
68
+ which case the dislocation axis will be aligned with the corresponding Cartesian
69
+ axis. Default value is 'y'.
70
+ cart_axes : bool, optional
71
+ Setting this to True will also perform an assertion check that the m- and n-axes
72
+ are both aligned with Cartesian axes. This is a requirement for some of the
73
+ atomic configuration generators. Default value is False as the elastic solution
74
+ by itself does not require the limitation.
75
+ tol : float
76
+ Tolerance parameter used to round off near-zero values. Default
77
+ value is 1e-8.
78
+ """
79
+ # Check that C is isotropic
80
+ if not C.is_normal('isotropic', atol=0.0, rtol=1e-4):
81
+ raise ValueError('C must be isotropic elastic constants')
82
+ C = C.normalized_as('isotropic')
83
+
84
+ VolterraDislocation.solve(self, C, burgers, ξ_uvw=ξ_uvw, slip_hkl=slip_hkl,
85
+ transform=transform, axes=axes, box=box,
86
+ m=m, n=n, cart_axes=cart_axes, tol=tol)
87
+
88
+ # Save shear modulus and Poissons ratio
89
+ bulk = self.C.bulk()
90
+ self.__mu = self.C.shear()
91
+ self.__nu = (3 * bulk - 2 * self.mu) / (2 * (3 * bulk + self.mu))
92
+
93
+ @property
94
+ def mu(self) -> float:
95
+ """float: The isotropic shear modulus"""
96
+ return self.__mu
97
+
98
+ @property
99
+ def nu(self) -> float:
100
+ """float: The isotropic Poisson's ratio"""
101
+ return self.__nu
102
+
103
+ @property
104
+ def K_tensor(self):
105
+ """numpy.ndarray : The energy coefficient tensor"""
106
+
107
+ # Construct K_tensor in standard setting
108
+ K_e = self.mu / (1 - self.nu)
109
+ K_s = self.mu
110
+ K = np.array([[K_e, 0.0, 0.0],
111
+ [0.0, K_e, 0.0],
112
+ [0.0, 0.0, K_s]])
113
+
114
+ # Transform tensor from m, n, ξ system
115
+ trans = np.array([self.m, self.n, self.ξ])
116
+ K = trans.T.dot(K.dot(trans))
117
+
118
+ # Round away near-zero terms
119
+ K[np.isclose(K / K.max(), 0.0, atol=self.tol)] = 0.0
120
+ return K
121
+
122
+ def theta(self, pos: npt.ArrayLike) -> np.ndarray:
123
+ """
124
+ Computes arctan(y / x) ranging from -π to π.
125
+
126
+ Parameters
127
+ ----------
128
+ pos : array-like object
129
+ 3D vector position(s).
130
+
131
+ Returns
132
+ -------
133
+ numpy.ndarray
134
+ The theta angles for each 3D position.
135
+ """
136
+ pos = np.asarray(pos, dtype=float)
137
+ x = pos.dot(self.m)
138
+ y = pos.dot(self.n)
139
+
140
+ # Compute arctan(y/x) for all values
141
+ with warnings.catch_warnings():
142
+ warnings.simplefilter("ignore")
143
+ theta = np.arctan(y / x)
144
+
145
+ # Handle special cases and ensure value range -π to π
146
+ theta[(x == 0) & (y > 0)] = np.pi / 2
147
+ theta[(x == 0) & (y < 0)] = -np.pi / 2
148
+ theta[(x < 0)] += np.pi
149
+ theta[(theta >= np.pi)] -= 2 * np.pi
150
+
151
+ return theta
152
+
153
+ def displacement(self, pos: npt.ArrayLike) -> np.ndarray:
154
+ """
155
+ Compute the position-dependent isotropic displacements.
156
+
157
+ Parameters
158
+ ----------
159
+ pos : array-like object
160
+ 3D vector position(s).
161
+
162
+ Returns
163
+ -------
164
+ numpy.ndarray
165
+ The computed 3D vector displacements at all given points.
166
+ """
167
+ pos = np.asarray(pos)
168
+ if pos.shape == (3,):
169
+ pos = pos.reshape(1,3)
170
+
171
+ # Split pos, burgers into components
172
+ x = pos.dot(self.m)
173
+ y = pos.dot(self.n)
174
+ b_s = self.burgers.dot(self.ξ)
175
+ b_e = self.burgers.dot(self.m)
176
+ nu = self.nu
177
+
178
+ # Compute displacement components in m, n, ξ directions
179
+ disp_m = b_e / (2 * np.pi) * (self.theta(pos) + (x * y) / (2 * (1 - nu) * (x**2 + y**2)))
180
+
181
+ disp_n = b_e / (2 * np.pi) * (-(1 - 2 * nu) / (4 * (1 - nu)) * np.log(x**2 + y**2)
182
+ + (y**2) / (2 * (1 - nu) * (x**2 + y**2)))
183
+
184
+ disp_ξ = b_s / (2 * np.pi) * (self.theta(pos))
185
+
186
+ # Combine into array
187
+ disp = np.outer(disp_ξ, self.ξ) + np.outer(disp_m, self.m) + np.outer(disp_n, self.n)
188
+
189
+ if disp.shape[0] == 1:
190
+ return disp[0]
191
+ else:
192
+ return disp
193
+
194
+ def strain(self, pos: npt.ArrayLike) -> np.ndarray:
195
+ """
196
+ Compute the position-dependent isotropic strains. The equations used are derived
197
+ from ϵ_ij = S_ijkl σ_kl.
198
+
199
+ Parameters
200
+ ----------
201
+ pos : array-like object
202
+ 3D vector position(s).
203
+
204
+ Returns
205
+ -------
206
+ numpy.ndarray
207
+ The computed 3x3 strain states at all given points.
208
+ """
209
+ pos = np.asarray(pos)
210
+ if pos.shape == (3,):
211
+ pos = pos.reshape(1,3)
212
+
213
+ # Split pos, burgers into components
214
+ x = pos.dot(self.m)
215
+ y = pos.dot(self.n)
216
+ b_s = self.burgers.dot(self.ξ)
217
+ b_e = self.burgers.dot(self.m)
218
+ nu = self.nu
219
+
220
+ # Initialize empty strain array
221
+ strain = np.empty(pos.shape[:-1] + (3,3))
222
+
223
+ # Strain components due to b_s
224
+ strain[..., 0, 2] = strain[..., 2, 0] = -b_s * y / (4 * np.pi * (x**2 + y**2))
225
+ strain[..., 1, 2] = strain[..., 2, 1] = b_s * x / (4 * np.pi * (x**2 + y**2))
226
+
227
+ # Shear strain components due to b_e
228
+ strain[..., 0, 1] = strain[..., 1, 0] = b_e * (x * (x**2 - y**2)) / (4 * np.pi * (1 - nu) * (x**2 + y**2)**2)
229
+
230
+ # Normal strain components due to b_e
231
+ common = b_e * y / (4 * np.pi * (1 - nu**2) * (x**2 + y**2)**2)
232
+ strain[..., 0, 0] = common * (x**2 * (-3 - nu + 2 * nu**2) + y**2 * (-1 + nu + 2 * nu**2))
233
+ strain[..., 1, 1] = common * (x**2 * (1 + 3 * nu + 2 * nu**2) + y**2 * (-1 + nu + 2 * nu**2))
234
+
235
+ # Set zero values
236
+ strain[..., 2, 2] = 0.0
237
+
238
+ # Get the reverse transformation matrix
239
+ transform = np.array([self.m, self.n, self.ξ]).T
240
+
241
+ # Transform strains
242
+ strain = np.einsum('mi, nj, ...ij -> ...mn', transform, transform, strain)
243
+
244
+ if strain.shape[0] == 1:
245
+ return strain[0]
246
+ else:
247
+ return strain
248
+
249
+ def stress(self, pos: npt.ArrayLike) -> np.ndarray:
250
+ """
251
+ Compute the position-dependent isotropic stresses.
252
+
253
+ Parameters
254
+ ----------
255
+ pos : array-like object
256
+ 3D vector position(s).
257
+
258
+ Returns
259
+ -------
260
+ numpy.ndarray
261
+ The computed 3x3 stress states at all given points.
262
+ """
263
+ pos = np.asarray(pos)
264
+ if pos.shape == (3,):
265
+ pos = pos.reshape(1,3)
266
+
267
+ # Split pos, burgers into components
268
+ x = pos.dot(self.m)
269
+ y = pos.dot(self.n)
270
+ b_s = self.burgers.dot(self.ξ)
271
+ b_e = self.burgers.dot(self.m)
272
+ nu = self.nu
273
+ mu = self.mu
274
+
275
+ # Initialize empty stress array
276
+ stress = np.empty(pos.shape[:-1] + (3,3))
277
+
278
+ # Stress components due to b_s
279
+ pre_s = mu * b_s / (2 * np.pi)
280
+ stress[..., 0, 2] = stress[..., 2, 0] =-pre_s * y / (x**2 + y**2)
281
+ stress[..., 1, 2] = stress[..., 2, 1] = pre_s * x / (x**2 + y**2)
282
+
283
+ # Stress components due to b_e
284
+ pre_e = mu * b_e / (2 * np.pi * (1 - nu))
285
+ stress[..., 0, 0] =-pre_e * (y * (3 * x**2 + y**2)) / (x**2 + y**2)**2
286
+ stress[..., 1, 1] = pre_e * (y * (x**2 - y**2)) / (x**2 + y**2)**2
287
+ stress[..., 2, 2] = nu * (stress[..., 0, 0] + stress[..., 1, 1])
288
+ stress[..., 0, 1] = stress[..., 1, 0] = pre_e * (x * (x**2 - y**2)) / (x**2 + y**2)**2
289
+
290
+ # Get the reverse transformation matrix
291
+ transform = np.array([self.m, self.n, self.ξ]).T
292
+
293
+ # Transform stresses
294
+ stress = np.einsum('mi, nj, ...ij -> ...mn', transform, transform, stress)
295
+
296
+ if stress.shape[0] == 1:
297
+ return stress[0]
298
+ else:
299
+ return stress
atomman/source/atomman/defect/SDVPN.py ADDED
@@ -0,0 +1,1243 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+
3
+ # Standard Python libraries
4
+ import io
5
+ import warnings
6
+ from typing import Optional, Tuple, Union
7
+
8
+ # http://www.numpy.org/
9
+ import numpy as np
10
+ import numpy.typing as npt
11
+
12
+ # https://www.scipy.org/
13
+ from scipy.optimize import minimize, OptimizeResult
14
+
15
+ # https://matplotlib.org/
16
+ import matplotlib.pyplot as plt
17
+
18
+ # https://github.com/usnistgov/DataModelDict
19
+ from DataModelDict import DataModelDict as DM
20
+
21
+ # atomman imports
22
+ import atomman.unitconvert as uc
23
+ from . import GammaSurface, VolterraDislocation
24
+
25
+ class SDVPN(object):
26
+ """
27
+ Class representation of the semidiscrete variational Peierls-Nabarro
28
+ dislocation model.
29
+ """
30
+
31
+ def __init__(self,
32
+ volterra: Optional[VolterraDislocation] = None,
33
+ gamma: Optional[GammaSurface] = None,
34
+ model: Union[str, io.IOBase, DM, None] = None,
35
+ tau: npt.ArrayLike = np.zeros((3,3)),
36
+ alpha: float = 0.0,
37
+ beta: npt.ArrayLike = np.zeros((3,3)),
38
+ cutofflongrange: Optional[float] = None,
39
+ fullstress: bool = True,
40
+ cdiffelastic: bool = False,
41
+ cdiffsurface: bool = True,
42
+ cdiffstress: bool = False,
43
+ min_method: str = 'Powell',
44
+ min_kwargs: Optional[dict] = None,
45
+ min_options: Optional[dict] = None):
46
+ """
47
+ Initializes an SDVPN object.
48
+
49
+ Parameters
50
+ ----------
51
+ volterra : atomman.defect.VolterraDislocation, optional
52
+ The elastic solution for a Volterra dislocation to use as the basis
53
+ of the model. Either volterra or model are required, and both cannot
54
+ be given at the same time.
55
+ gamma : atomman.defect.GammaSurface, optional
56
+ The gamma surface to use for the solution. Required unless model
57
+ is given and the model content contains gamma surface data.
58
+ model : str or DataModelDict, optional
59
+ Saved data from previous SDVPN runs to load. Either volterra or
60
+ model are required, and both cannot be given at the same time.
61
+ tau : numpy.ndarray, optional
62
+ A (3,3) array giving the stress tensor to apply to the system
63
+ using the stress energy term. Only the xy, yy, and yz components
64
+ are used. Default value is all zeros.
65
+ alpha : list of float, optional
66
+ The alpha coefficient(s) used by the nonlocal energy term. Default
67
+ value is [0.0].
68
+ beta : numpy.ndarray, optional
69
+ The (3,3) array of beta coefficient(s) used by the surface energy
70
+ term. Default value is all zeros.
71
+ cutofflongrange : float, optional
72
+ The cutoff distance to use for computing the long-range energy.
73
+ Default value is 1000 angstroms.
74
+ fullstress : bool, optional
75
+ Flag indicating which stress energy algorithm to use. Default
76
+ value is True.
77
+ cdiffelastic : bool, optional
78
+ Flag indicating if the dislocation density for the elastic energy
79
+ component is computed with central difference (True) or simply
80
+ neighboring values (False). Default value is False.
81
+ cdiffsurface : bool, optional
82
+ Flag indicating if the dislocation density for the surface energy
83
+ component is computed with central difference (True) or simply
84
+ neighboring values (False). Default value is True.
85
+ cdiffstress : bool, optional
86
+ Flag indicating if the dislocation density for the stress energy
87
+ component is computed with central difference (True) or simply
88
+ neighboring values (False). Only matters if fullstress is True.
89
+ Default value is False.
90
+ min_method : str, optional
91
+ The scipy.optimize.minimize method to use. Default value is
92
+ 'Powell'.
93
+ min_kwargs : dict, optional
94
+ Any keyword arguments to pass on to scipy.optimize.minimize besides
95
+ the coordinates, method and options.
96
+ min_options : dict, optional
97
+ Any options to pass on to scipy.optimize.minimize. Default value
98
+ is {}.
99
+ """
100
+
101
+ # Load solution from existing model
102
+ if model is not None:
103
+ if volterra is not None:
104
+ raise ValueError('model cannot be given with volterra')
105
+
106
+ self.load(model, gamma=gamma)
107
+
108
+ # Extract parameters and check solution compatibility
109
+ elif volterra is not None:
110
+
111
+ # Check that gamma is given
112
+ if gamma is None:
113
+ raise ValueError('gamma is required if volterra is given')
114
+
115
+ # Check that burgers is in the slip plane
116
+ if not np.isclose(np.dot(volterra.n, volterra.burgers), 0.0):
117
+ raise ValueError('dislocation burgers vector must be in the slip plane')
118
+
119
+ # Get m, n, ξ, K_tensor, burgers, and transform from volterra
120
+ m, n, ξ = volterra.m, volterra.n, volterra.ξ
121
+ K_tensor = volterra.K_tensor
122
+ burgers = volterra.burgers
123
+ transform = volterra.transform
124
+
125
+ # Transform K_tensor, burgers and transform to [m,n,ξ] orientation
126
+ mnξ = np.array([m, n, ξ]) # This is transformation matrix to [m,n,ξ] setting
127
+ K_tensor = mnξ.dot(K_tensor.dot(mnξ.T))
128
+ burgers = mnξ.dot(burgers)
129
+ transform = np.matmul(mnξ, transform)
130
+
131
+ # Check if dislocation system is compatible with gamma surface
132
+ planenormal = transform.dot(gamma.planenormal)
133
+ if not np.isclose(planenormal[0], 0.0) or not np.isclose(planenormal[2], 0.0):
134
+ raise ValueError('different slip planes for gamma and volterra found')
135
+
136
+ # Set basic solution definition
137
+ self.__K_tensor = K_tensor
138
+ self.__burgers = burgers
139
+ self.__transform = transform
140
+ self.__gamma = gamma
141
+
142
+ # Set options
143
+ self.tau = tau
144
+ self.alpha = alpha
145
+ self.beta = beta
146
+ if cutofflongrange is None:
147
+ self.cutofflongrange = uc.set_in_units(1000, 'angstrom')
148
+ else:
149
+ self.cutofflongrange = cutofflongrange
150
+ self.fullstress = fullstress
151
+ self.cdiffelastic = cdiffelastic
152
+ self.cdiffsurface = cdiffsurface
153
+ self.cdiffstress = cdiffstress
154
+ self.min_method = min_method
155
+ self.min_options = min_options
156
+ self.min_kwargs = min_kwargs
157
+
158
+ else:
159
+ raise ValueError('either dislsol or model must be given')
160
+
161
+ @property
162
+ def x(self) -> np.ndarray:
163
+ """numpy.ndarray : The x coordinates."""
164
+ try:
165
+ return self.__x
166
+ except:
167
+ raise AttributeError('x values not set yet')
168
+
169
+ @x.setter
170
+ def x(self, value: npt.ArrayLike):
171
+ value = np.asarray(value, dtype=float)
172
+ assert value.ndim == 1
173
+ diff = value[1:] - value[:-1]
174
+ assert np.allclose(diff[0], diff), 'x values must be evenly spaced'
175
+ assert diff[0] > 0, 'x values must be in increasing order'
176
+ self.__x = value
177
+
178
+ @property
179
+ def disregistry(self) -> np.ndarray:
180
+ """numpy.ndarray : The disregistry vector for each x coordinate."""
181
+ try:
182
+ return self.__disregistry
183
+ except:
184
+ raise AttributeError('disregistry values not set yet')
185
+
186
+ @disregistry.setter
187
+ def disregistry(self, value: npt.ArrayLike):
188
+ value = np.asarray(value)
189
+ assert value.ndim == 2 and value.shape[1] == 3, 'invalid disregistry dimensions'
190
+ assert np.allclose(value[:,1], 0.0), 'y (i.e. out-of-plane) component of disregistry not supported'
191
+ self.__disregistry = value
192
+
193
+ @property
194
+ def K_tensor(self) -> np.ndarray:
195
+ """numpy.ndarray : Dislocation energy coefficient tensor."""
196
+ return self.__K_tensor
197
+
198
+ @property
199
+ def burgers(self) -> np.ndarray:
200
+ """numpy.ndarray : Burgers vector."""
201
+ return self.__burgers
202
+
203
+ @property
204
+ def transform(self) -> np.ndarray:
205
+ """numpy.ndarray : Transformation matrix from standard crystal setting to dislocation solution setting."""
206
+ return self.__transform
207
+
208
+ @property
209
+ def gamma(self) -> GammaSurface:
210
+ """atomman.defect.GammaSurface : The stacking fault map."""
211
+ return self.__gamma
212
+
213
+ @property
214
+ def tau(self) -> np.ndarray:
215
+ """numpy.ndarray : The applied 3x3 stress tensor."""
216
+ return self.__tau
217
+
218
+ @tau.setter
219
+ def tau(self, value: npt.ArrayLike):
220
+ value = np.asarray(value, dtype=float)
221
+ assert value.shape == (3,3)
222
+ self.__tau = value
223
+
224
+ @property
225
+ def alpha(self) -> tuple:
226
+ """tuple of float : Coefficients for nonlocal energy correction."""
227
+ return self.__alpha
228
+
229
+ @alpha.setter
230
+ def alpha(self, value: Union[float, tuple]):
231
+ try:
232
+ self.__alpha = tuple(value)
233
+ except:
234
+ self.__alpha = (value,)
235
+
236
+ @property
237
+ def beta(self) -> np.ndarray:
238
+ """numpy.ndarray : 3x3 coefficients for gradient energy correction."""
239
+ return self.__beta
240
+
241
+ @beta.setter
242
+ def beta(self, value: npt.ArrayLike):
243
+ value = np.asarray(value, dtype=float)
244
+ assert value.shape == (3,3)
245
+ self.__beta = value
246
+
247
+ @property
248
+ def cutofflongrange(self) -> float:
249
+ """float : Cutoff distance for long-range elastic energy."""
250
+ return self.__cutofflongrange
251
+
252
+ @cutofflongrange.setter
253
+ def cutofflongrange(self, value: float):
254
+ self.__cutofflongrange = float(value)
255
+
256
+ @property
257
+ def fullstress(self) -> bool:
258
+ """bool : Flag indicating which stress algorithm was used."""
259
+ return self.__fullstress
260
+
261
+ @fullstress.setter
262
+ def fullstress(self, value: bool):
263
+ assert isinstance(value, bool)
264
+ self.__fullstress = value
265
+
266
+ @property
267
+ def cdiffelastic(self) -> bool:
268
+ """bool : Flag indicating if elastic energy used central difference for computing the dislocation density."""
269
+ return self.__cdiffelastic
270
+
271
+ @cdiffelastic.setter
272
+ def cdiffelastic(self, value: bool):
273
+ assert isinstance(value, bool)
274
+ self.__cdiffelastic = value
275
+
276
+ @property
277
+ def cdiffsurface(self) -> bool:
278
+ """bool : Flag indicating if surface energy used central difference for computing the dislocation density."""
279
+ return self.__cdiffsurface
280
+
281
+ @cdiffsurface.setter
282
+ def cdiffsurface(self, value: bool):
283
+ assert isinstance(value, bool)
284
+ self.__cdiffsurface = value
285
+
286
+ @property
287
+ def cdiffstress(self) -> bool:
288
+ """bool : Flag indicating if stress energy used central difference for computing the dislocation density."""
289
+ return self.__cdiffstress
290
+
291
+ @cdiffstress.setter
292
+ def cdiffstress(self, value: bool):
293
+ assert isinstance(value, bool)
294
+ self.__cdiffstress = value
295
+
296
+ @property
297
+ def min_method(self) -> str:
298
+ """str : scipy.optimize.minimize method used."""
299
+ return self.__min_method
300
+
301
+ @min_method.setter
302
+ def min_method(self, value: str):
303
+ self.__min_method = str(value)
304
+
305
+ @property
306
+ def min_options(self) -> dict:
307
+ """dict : scipy.optimize.minimize options used."""
308
+ return self.__min_options
309
+
310
+ @min_options.setter
311
+ def min_options(self, value: Optional[dict]):
312
+ if value is None:
313
+ self.__min_options = {}
314
+ elif isinstance(value, dict):
315
+ self.__min_options = value
316
+ else:
317
+ raise TypeError('min_options must be a dict')
318
+
319
+ @property
320
+ def min_kwargs(self) -> dict:
321
+ """dict : scipy.optimize.minimize keywords used."""
322
+ return self.__min_kwargs
323
+
324
+ @min_kwargs.setter
325
+ def min_kwargs(self, value: Optional[dict]):
326
+ if value is None:
327
+ self.__min_kwargs = {}
328
+ elif isinstance(value, dict):
329
+ self.__min_kwargs = value
330
+ else:
331
+ raise TypeError('min_kwargs must be a dict')
332
+
333
+ @property
334
+ def res(self) -> OptimizeResult:
335
+ """OptimizeResult : scipy.optimize.minimize result."""
336
+ try:
337
+ return self.__res
338
+ except:
339
+ return None
340
+
341
+ def solve(self,
342
+ x: Optional[npt.ArrayLike] = None,
343
+ disregistry: Optional[npt.ArrayLike] = None,
344
+ tau: Optional[npt.ArrayLike] = None,
345
+ alpha: Optional[list] = None,
346
+ beta: Optional[npt.ArrayLike] = None,
347
+ cutofflongrange: Optional[float] = None,
348
+ fullstress: Optional[bool] = None,
349
+ cdiffelastic: Optional[bool] = None,
350
+ cdiffsurface: Optional[bool] = None,
351
+ cdiffstress: Optional[bool] = None,
352
+ min_method: Optional[str] = None,
353
+ min_kwargs: Optional[dict] = None,
354
+ min_options: Optional[dict] = None):
355
+ """
356
+ Solves the semidiscrete variational Peierls-Nabarro dislocation
357
+ disregistry through energy minimization using the set class
358
+ properties. All parameters are optional keyword arguments that
359
+ can be used to change any of the previous settings.
360
+
361
+ Parameters
362
+ ----------
363
+ x : numpy.ndarray, optional
364
+ An array of shape (N) giving the x coordinates corresponding to
365
+ the disregistry solution.
366
+ disregistry : numpy.ndarray, optional
367
+ A (N,3) array giving the initial disregistry vector guess at each
368
+ x coordinate.
369
+ tau : numpy.ndarray, optional
370
+ A (3,3) array giving the stress tensor to apply to the system
371
+ using the stress energy term. Only the xy, yy, and yz components
372
+ are used.
373
+ alpha : list of float, optional
374
+ The alpha coefficient(s) used by the nonlocal energy term.
375
+ beta : numpy.ndarray, optional
376
+ The (3,3) array of beta coefficient(s) used by the surface energy
377
+ term.
378
+ cutofflongrange : float, optional
379
+ The cutoff distance to use for computing the long-range energy.
380
+ fullstress : bool, optional
381
+ Flag indicating which stress energy algorithm to use.
382
+ cdiffelastic : bool, optional
383
+ Flag indicating if the dislocation density for the elastic energy
384
+ component is computed with central difference (True) or simply
385
+ neighboring values (False).
386
+ cdiffsurface : bool, optional
387
+ Flag indicating if the dislocation density for the surface energy
388
+ component is computed with central difference (True) or simply
389
+ neighboring values (False).
390
+ cdiffstress : bool, optional
391
+ Flag indicating if the dislocation density for the stress energy
392
+ component is computed with central difference (True) or simply
393
+ neighboring values (False). Only matters if fullstress is True.
394
+ min_method : str, optional
395
+ The scipy.optimize.minimize method to use.
396
+ min_kwargs : dict, optional
397
+ Any keyword arguments to pass on to scipy.optimize.minimize besides
398
+ the coordinates, method and options.
399
+ min_options : dict, optional
400
+ Any options to pass on to scipy.optimize.minimize.
401
+ """
402
+
403
+ # Change attribute values if given
404
+ if x is not None:
405
+ self.x = x
406
+ if disregistry is not None:
407
+ self.disregistry = disregistry
408
+ if tau is not None:
409
+ self.tau = tau
410
+ if alpha is not None:
411
+ self.alpha = alpha
412
+ if beta is not None:
413
+ self.beta = beta
414
+ if cutofflongrange is not None:
415
+ self.cutofflongrange = cutofflongrange
416
+ if fullstress is not None:
417
+ self.fullstress = fullstress
418
+ if cdiffelastic is not None:
419
+ self.cdiffelastic = cdiffelastic
420
+ if cdiffsurface is not None:
421
+ self.cdiffsurface = cdiffsurface
422
+ if cdiffstress is not None:
423
+ self.cdiffstress = cdiffstress
424
+ if min_method is not None:
425
+ self.min_method = min_method
426
+ if min_options is not None:
427
+ self.min_options = min_options
428
+ if min_kwargs is not None:
429
+ self.min_kwargs = min_kwargs
430
+
431
+ # Check that x and disregistry exist and are of the same length
432
+ if len(self.x) != len(self.disregistry):
433
+ raise ValueError('x and disregistry are not of the same length')
434
+
435
+ # Define subfunctions
436
+ def decompose(d):
437
+ """Breaks disregistry into components for minimization"""
438
+ d13 = np.concatenate([d[1:-1, 0], d[1:-1, 2]])
439
+ first = d[0]
440
+ last = d[-1]
441
+ return d13, first, last
442
+
443
+ def recompose(d13, first, last):
444
+ """Reassembles the disregistry components"""
445
+ half = int(len(d13)/2)
446
+ d = np.zeros((half+2, 3))
447
+ d[0] = first
448
+ d[-1] = last
449
+ d[1:-1, 0] = d13[:half]
450
+ d[1:-1, 2] = d13[half:]
451
+ return d
452
+
453
+ def min_func(d13, first, last):
454
+ """Function for minimizing"""
455
+ disregistry = recompose(d13, first, last)
456
+ return self.total_energy(disregistry=disregistry)
457
+
458
+ # Solve disregistry
459
+ d13, first, last = decompose(self.disregistry)
460
+ res = minimize(min_func, d13, args=(first, last),
461
+ method=self.min_method, options=self.min_options, **self.min_kwargs)
462
+ self.disregistry = recompose(res.x, first, last)
463
+
464
+ self.__res = res
465
+
466
+ def disldensity(self,
467
+ x: Optional[npt.ArrayLike] = None,
468
+ disregistry: Optional[npt.ArrayLike] = None,
469
+ cdiff: bool = False
470
+ ) -> Tuple[np.ndarray, np.ndarray]:
471
+ """
472
+ Computes the dislocation density as the numerical derivative of
473
+ disregistry with respect to x. Uses either neighboring values
474
+
475
+ ρ[i] = (δ[i] - δ[i-1]) / (x[i] - x[i-1])
476
+
477
+ or central difference
478
+
479
+ ρ[i] = (δ[i+1] - δ[i-1]) / (x[i+1] - x[i-1])
480
+
481
+ Parameters
482
+ ----------
483
+ x : array-like object, optional
484
+ x-coordinates. Default value is the stored x-coordinates.
485
+ disregistry : array-like object, optional
486
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
487
+ Default value is the stored disregistry values.
488
+ cdiff : bool, optional
489
+ Flag indicating how to compute the derivative. A value of False
490
+ (default) compares nearest values while a value of True compares
491
+ next-nearest (i.e., central difference).
492
+
493
+ Returns
494
+ -------
495
+ newx : numpy.array
496
+ The x positions corresponding to the dislocation density values.
497
+ rho : numpy.array
498
+ The computed dislocation density.
499
+ """
500
+ # Default values are class properties
501
+ if x is None:
502
+ x = self.x
503
+ if disregistry is None:
504
+ disregistry = self.disregistry
505
+
506
+ # Extract values
507
+ δ = disregistry
508
+
509
+ if cdiff is False:
510
+ # ρ[i] = (δ[i] - δ[i-1]) / (x[i] - x[i-1])
511
+ ρ = ((δ[1:] - δ[:-1]).T / (x[1:] - x[:-1])).T
512
+
513
+ # newx is all x except the first
514
+ newx = x[1:]
515
+
516
+ elif cdiff is True:
517
+ # ρ[i] = (δ[i+1] - δ[i-1]) / (x[i+1] - x[i-1])
518
+ ρ = ((δ[2:] - δ[:-2]).T / (x[2:] - x[:-2])).T
519
+
520
+ # newx is all x except the first and last
521
+ newx = x[1:-1]
522
+ else:
523
+ raise TypeError('cdiff must be bool')
524
+
525
+ return (newx, ρ)
526
+
527
+ def misfit_energy(self,
528
+ x: Optional[npt.ArrayLike] = None,
529
+ disregistry: Optional[npt.ArrayLike] = None) -> float:
530
+ """
531
+ Computes the misfit energy for the disregistry using the stored gamma
532
+ surface
533
+
534
+ E_misfit = Σ γ(δ)Δx
535
+
536
+ Parameters
537
+ ----------
538
+ x : array-like object, optional
539
+ x-coordinates. Default value is the stored x-coordinates.
540
+ disregistry : array-like object, optional
541
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
542
+ Default value is the stored disregistry values.
543
+
544
+ Returns
545
+ -------
546
+ float
547
+ The misfit energy for the dislocation.
548
+ """
549
+ # Default values are class properties
550
+ if x is None:
551
+ x = self.x
552
+ if disregistry is None:
553
+ disregistry = self.disregistry
554
+
555
+ # Extract values
556
+ δ = disregistry
557
+ Δx = x[1] - x[0]
558
+ transform = self.transform
559
+ gamma = self.gamma
560
+
561
+ # Strip out y-component of disregistry and transform for gamma
562
+ disreg = np.vstack([δ[:,0], np.zeros(len(δ)), δ[:,2]]).T
563
+ pos = np.inner(disreg, transform.T)
564
+
565
+ # Σ γ(δ)Δx
566
+ return Δx * gamma.E_gsf(pos=pos).sum()
567
+
568
+ def elastic_energy(self,
569
+ x: Optional[npt.ArrayLike] = None,
570
+ disregistry: Optional[npt.ArrayLike] = None) -> float:
571
+ r"""
572
+ Computes the short-range configuration-dependent elastic energy term
573
+ for the dislocation based on the dislocation density and K_tensor.
574
+
575
+ E_elastic = 1/(4π) Σ_i Σ_j χ(i,j,Δx) K_lm ρ_l[i] ρ_m[j]
576
+
577
+ χ(i,j,Δx) = (3/2) Δx² + ψ(i-1,j-1,Δx) + ψ(i,j,Δx) - ψ(i,j-1,Δx) - ψ(j,i-1,Δx)
578
+
579
+ ψ(i,j,Δx) = (1/2) (i-j)² Δx² ln(\|i-j\|Δx)
580
+
581
+ Parameters
582
+ ----------
583
+ x : array-like object, optional
584
+ x-coordinates. Default value is the stored x-coordinates.
585
+ disregistry : array-like object, optional
586
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
587
+ Default value is the stored disregistry values.
588
+
589
+ Returns
590
+ -------
591
+ float
592
+ The elastic energy for the dislocation.
593
+ """
594
+ # Define subfunctions
595
+ def χ(i, j, Δx):
596
+ """
597
+ Computes the chi subfunction:
598
+ χ(i,j,Δx) = (3/2) Δx² + ψ(i-1,j-1,Δx) + ψ(i,j,Δx)
599
+ - ψ(i,j-1,Δx) - ψ(j,i-1,Δx)
600
+ """
601
+ return 3./2. * Δx**2 + (ψ(i-1, j-1, Δx) + ψ(i, j, Δx)
602
+ - ψ(i, j-1, Δx) - ψ(j, i-1, Δx))
603
+
604
+ def ψ(i, j, Δx):
605
+ """
606
+ Computes the psi subfunction:
607
+ ψ(i,j,Δx) = (1/2) (i-j)² Δx² ln(|i-j|Δx)
608
+ """
609
+ # Suppress NaN runtime warnings
610
+ with warnings.catch_warnings():
611
+ warnings.simplefilter("ignore", category=RuntimeWarning)
612
+
613
+ # ψ(i,j,Δx) = (1/2) (i-j)² Δx² ln(|i-j|Δx)
614
+ p = 0.5 * (i - j)**2 * Δx**2 * np.log(np.abs(i - j) * Δx)
615
+
616
+ # Replace NaN values with 0.0
617
+ p[np.isnan(p)] = 0.0
618
+
619
+ return p
620
+
621
+ # Default values are class properties
622
+ if x is None:
623
+ x = self.x
624
+ if disregistry is None:
625
+ disregistry = self.disregistry
626
+
627
+ # Extract values
628
+ δ = disregistry
629
+ Δx = x[1] - x[0]
630
+ cdiff = self.cdiffelastic
631
+ Kij = self.K_tensor
632
+
633
+ ρ = self.disldensity(x=x, disregistry=δ, cdiff=cdiff)[1]
634
+ j = np.arange(len(ρ), dtype=int)
635
+
636
+ energy = 0.0
637
+
638
+ # Compute elastic energy (looping over i, vectorization over j)
639
+ # 1/(4π) Σ_i Σ_j χ(i,j,Δx) K_lm ρ_l[i] ρ_m[j]
640
+ for i in j:
641
+ energy += np.sum( χ(i, j, Δx) * np.inner(ρ[i].dot(Kij), ρ) ) / (4 * np.pi)
642
+
643
+ return energy
644
+
645
+ def longrange_energy(self) -> float:
646
+ """
647
+ Computes the long-range elastic energy term for the dislocation using
648
+ the K_tensor, Burgers vector and long-range cutoff. This term is
649
+ configuration-independent and thus the method takes no parameters.
650
+
651
+ E_longrange = 1/(2π) K_lm b_l b_m ln(L)
652
+
653
+ Returns
654
+ -------
655
+ float
656
+ The long-range energy for the dislocation.
657
+ """
658
+ # Extract values
659
+ Kij = self.K_tensor
660
+ b = self.burgers
661
+ L = self.cutofflongrange
662
+
663
+ # Compute long-range energy
664
+ # 1/(2π) K_lm b_l b_m ln(L)
665
+ return np.inner(b.dot(Kij), b) * np.log(L) / (2 * np.pi)
666
+
667
+ def stress_energy(self,
668
+ x: Optional[npt.ArrayLike] = None,
669
+ disregistry: Optional[npt.ArrayLike] = None) -> float:
670
+ """
671
+ Computes the stress energy due to the applied stress, tau.
672
+ If fullstress is True, the original stress expression by
673
+ Bulatov and Kaxiras will be used:
674
+
675
+ E_stress = -1/2 Σ_i (x[i]² - x[i-1]²) ρ_l τ_2l
676
+
677
+ If fullstress is False, the alternate stress expression by
678
+ Shen and Cheng 10.1016/j.scriptamat.2009.04.047 will be used:
679
+
680
+ E_stress = -1/2 Σ_i τ_2l (δ_l[i] + δ_l[i+1]) Δx
681
+
682
+ Note that the Shen and Cheng expression will have a constant
683
+ error associated with it giving an incorrect overall energy, but
684
+ should apply a similar force on the dislocation.
685
+
686
+ Parameters
687
+ ----------
688
+ x : array-like object, optional
689
+ x-coordinates. Default value is the stored x-coordinates.
690
+ disregistry : array-like object, optional
691
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
692
+ Default value is the stored disregistry values.
693
+
694
+ Returns
695
+ -------
696
+ float
697
+ The stress energy for the dislocation.
698
+ """
699
+ # Default values are class properties
700
+ if x is None:
701
+ x = self.x
702
+ if disregistry is None:
703
+ disregistry = self.disregistry
704
+
705
+ # Extract values
706
+ δ = disregistry
707
+ Δx = x[1] - x[0]
708
+ τ = self.tau
709
+ full = self.fullstress
710
+ cdiff = self.cdiffstress
711
+
712
+ if full is True:
713
+ ρ = self.disldensity(x=x, disregistry=δ, cdiff=cdiff)[1]
714
+ # -1/2 Σ_i (x[i]² - x[i-1]²) ρ_l τ_2l
715
+ return -0.5 * np.sum((x[1:]**2 - x[:-1]**2) * np.inner(ρ, τ[1,:]))
716
+
717
+ else:
718
+ # Flip sign on tau so energies match full=True
719
+ τ = -τ
720
+
721
+ # -1/2 Σ_i τ_2l (δ_l[i] + δ_l[i+1]) Δx
722
+ return -0.5 * np.sum(np.inner(τ[1,:], (δ[:-1] + δ[1:]) * Δx))
723
+
724
+ def surface_energy(self,
725
+ x: Optional[npt.ArrayLike] = None,
726
+ disregistry: Optional[npt.ArrayLike] = None) -> float:
727
+ """
728
+ Computes the gradient surface energy correction using beta
729
+ coefficients.
730
+
731
+ E_surface = Σ_j β_lj / 4 Σ_i ρ_l[i]² Δx
732
+
733
+ Parameters
734
+ ----------
735
+ x : array-like object, optional
736
+ x-coordinates. Default value is the stored x-coordinates.
737
+ disregistry : array-like object, optional
738
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
739
+ Default value is the stored disregistry values.
740
+
741
+ Returns
742
+ -------
743
+ float
744
+ The surface energy for the dislocation.
745
+ """
746
+ # Default values are class properties
747
+ if x is None:
748
+ x = self.x
749
+ if disregistry is None:
750
+ disregistry = self.disregistry
751
+
752
+ # Extract values
753
+ δ = disregistry
754
+ Δx = x[1] - x[0]
755
+ β = self.beta
756
+ cdiff = self.cdiffsurface
757
+
758
+ ρ = self.disldensity(x=x, disregistry=δ, cdiff=cdiff)[1]
759
+
760
+ # Σ_j β_lj / 4 Σ_i ρ_l[i]² Δx
761
+ return np.sum( np.inner(ρ**2 * Δx, β) ) / 4
762
+
763
+ def nonlocal_energy(self,
764
+ x: Optional[npt.ArrayLike] = None,
765
+ disregistry: Optional[npt.ArrayLike] = None) -> float:
766
+ """
767
+ Computes the nonlocal energy correction using alpha coefficient(s).
768
+
769
+ E_nonlocal = Σ_m α_m Σ_i δ[i] (δ[i] - (δ[i+m] + δ[i-m]) / 2) Δx
770
+
771
+ Parameters
772
+ ----------
773
+ x : array-like object, optional
774
+ x-coordinates. Default value is the stored x-coordinates.
775
+ disregistry : array-like object, optional
776
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
777
+ Default value is the stored disregistry values.
778
+
779
+ Returns
780
+ -------
781
+ float
782
+ The nonlocal energy for the dislocation.
783
+ """
784
+ # Default values are class properties
785
+ if x is None:
786
+ x = self.x
787
+ if disregistry is None:
788
+ disregistry = self.disregistry
789
+
790
+ # Extract values
791
+ δ = disregistry
792
+ Δx = x[1] - x[0]
793
+ αs = self.alpha
794
+
795
+ energy = 0.0
796
+
797
+ # Σ_m α_m Σ_i δ[i] (δ[i] - (δ[i+m] + δ[i-m]) / 2) Δx
798
+ for num, α in enumerate(αs):
799
+ m = num + 1
800
+ dd = δ[m:-m] - 0.5 * (δ[2*m:] + δ[:-2*m])
801
+ energy += α * np.sum(δ[m:-m] * dd * Δx)
802
+
803
+ return energy
804
+
805
+ def total_energy(self,
806
+ x: Optional[npt.ArrayLike] = None,
807
+ disregistry: Optional[npt.ArrayLike] = None) -> float:
808
+ """
809
+ Computes the total energy for the dislocation.
810
+
811
+ E_total = E_elastic + E_misfit + E_stress + E_surface + E_nonlocal
812
+
813
+ Parameters
814
+ ----------
815
+ x : array-like object, optional
816
+ x-coordinates. Default value is the stored x-coordinates.
817
+ disregistry : array-like object, optional
818
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
819
+ Default value is the stored disregistry values.
820
+
821
+ Returns
822
+ -------
823
+ float
824
+ The total energy for the dislocation.
825
+ """
826
+ # Default values are class properties
827
+ if x is None:
828
+ x = self.x
829
+ if disregistry is None:
830
+ disregistry = self.disregistry
831
+
832
+ return (self.misfit_energy(x, disregistry)
833
+ + self.elastic_energy(x, disregistry)
834
+ + self.longrange_energy()
835
+ + self.stress_energy(x, disregistry)
836
+ + self.nonlocal_energy(x, disregistry)
837
+ + self.surface_energy(x, disregistry))
838
+
839
+ def check_energies(self,
840
+ x: Optional[npt.ArrayLike] = None,
841
+ disregistry: Optional[npt.ArrayLike] = None,
842
+ energyperlength_unit: str = 'eV/Å'):
843
+ """
844
+ Prints a summary string of all computed energy components.
845
+
846
+ Parameters
847
+ ----------
848
+ x : array-like object, optional
849
+ x-coordinates. Default value is the stored x-coordinates.
850
+ disregistry : array-like object, optional
851
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
852
+ Default value is the stored disregistry values.
853
+ energyperlength_unit : str, optional
854
+ The units of energy per length to report the dislocation line
855
+ energies in. Default value is 'eV/Å'.
856
+ """
857
+ hasvals = True
858
+ # Default values are class properties
859
+ if x is None:
860
+ try:
861
+ x = self.x
862
+ except:
863
+ hasvals = False
864
+ if disregistry is None:
865
+ try:
866
+ disregistry = self.disregistry
867
+ except:
868
+ hasvals = False
869
+
870
+ if hasvals:
871
+ print(f'Dislocation energy terms in {energyperlength_unit}:')
872
+ print('Misfit energy = ', uc.get_in_units(self.misfit_energy(x, disregistry), energyperlength_unit))
873
+ print('Elastic energy = ', uc.get_in_units(self.elastic_energy(x, disregistry), energyperlength_unit))
874
+ print('Long-range energy =', uc.get_in_units(self.longrange_energy(), energyperlength_unit))
875
+ print('Stress energy = ', uc.get_in_units(self.stress_energy(x, disregistry), energyperlength_unit))
876
+ print('Surface energy = ', uc.get_in_units(self.surface_energy(x, disregistry), energyperlength_unit))
877
+ print('Nonlocal energy = ', uc.get_in_units(self.nonlocal_energy(x, disregistry), energyperlength_unit))
878
+ print('Total energy = ', uc.get_in_units(self.total_energy(x, disregistry), energyperlength_unit))
879
+ else:
880
+ print('x and disregistry must be set/given to check energies')
881
+
882
+ def disregistry_plot(self,
883
+ x: Optional[npt.ArrayLike] = None,
884
+ disregistry: Optional[npt.ArrayLike] = None,
885
+ figsize: Optional[tuple] = None,
886
+ length_unit: str = 'Å') -> plt.figure:
887
+ """
888
+ Creates a simple matplotlib figure showing the disregistry profiles.
889
+
890
+ Parameters
891
+ ----------
892
+ x : array-like object, optional
893
+ x-coordinates. Default value is the stored x-coordinates.
894
+ disregistry : array-like object, optional
895
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
896
+ Default value is the stored disregistry values.
897
+ figsize : tuple, optional
898
+ matplotlib figure figsize parameter. Default value is (10, 6).
899
+ length_unit : str, optional
900
+ The unit of length to display positions and disregistry values in.
901
+ Default value is 'Å'.
902
+
903
+ Returns
904
+ -------
905
+ matplotlib.pyplot.figure
906
+ The generated figure allowing users to perform additional
907
+ modifications.
908
+ """
909
+ hasvals = True
910
+ # Default values are class properties
911
+ if x is None:
912
+ try:
913
+ x = self.x
914
+ except:
915
+ hasvals = False
916
+ if disregistry is None:
917
+ try:
918
+ disregistry = self.disregistry
919
+ except:
920
+ hasvals = False
921
+ if hasvals:
922
+
923
+ if figsize is None:
924
+ figsize = (10, 6)
925
+
926
+ fig = plt.figure(figsize=figsize)
927
+
928
+ x = uc.get_in_units(x, length_unit)
929
+ disregistry = uc.get_in_units(disregistry, length_unit)
930
+ plt.plot(x, disregistry[:, 0], label='edge disregistry')
931
+ plt.plot(x, disregistry[:, 1], label='normal disregistry')
932
+ plt.plot(x, disregistry[:, 2], label='screw disregistry')
933
+ plt.legend(fontsize='xx-large')
934
+ plt.xlabel(f'x (${length_unit}$)', size='xx-large')
935
+ plt.ylabel(f'disregistry (${length_unit}$', size='xx-large')
936
+ return fig
937
+
938
+ else:
939
+ print('x and disregistry must be set/given to plot')
940
+
941
+ def E_gsf_surface_plot(self,
942
+ x: Optional[npt.ArrayLike] = None,
943
+ disregistry: Optional[npt.ArrayLike] = None,
944
+ fmt: str = 'ro-',
945
+ normalize: Optional[bool] = False,
946
+ smooth: bool = True,
947
+ a1vect: Optional[npt.ArrayLike] = None,
948
+ a2vect: Optional[npt.ArrayLike] = None,
949
+ xvect: Optional[npt.ArrayLike] = None,
950
+ length_unit: str = 'Å',
951
+ energyperarea_unit: str = 'eV/Å^2',
952
+ numx: int = 100,
953
+ numy: int = 100,
954
+ figsize: Optional[tuple] = None,
955
+ **kwargs) -> plt.figure:
956
+ """
957
+ Extends the GammaSurface.E_gsf_surface_plot() method to plot the
958
+ disregistry path on top of it.
959
+
960
+ Parameters
961
+ ----------
962
+ x : array-like object, optional
963
+ x-coordinates. Default value is the stored x-coordinates. If x
964
+ or disregistry are not set/given, then the disregistry path will
965
+ not be added.
966
+ disregistry : array-like object, optional
967
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
968
+ Default value is the stored disregistry values. If x
969
+ or disregistry are not set/given, then the disregistry path will
970
+ not be added.
971
+ fmt : str, optional
972
+ The matplotlib.pyplot.plot fmt parameter for the disregistry path
973
+ line, i.e. color, marker and line style options. Default value is
974
+ 'ro-': red with circle markers and solid line.
975
+ normalize : bool, optional
976
+ Flag indicating if axes are Cartesian (False, default) or
977
+ normalized by a1, a2 vectors (True).
978
+ smooth : bool, optional
979
+ If True (default), then plot shows smooth interpolated values.
980
+ If False, plot shows nearest raw data values.
981
+ a1vect : array-like object, optional
982
+ Crystal vector for the a1 vector to use for plotting. Default
983
+ value of None uses the saved a1vect.
984
+ a2vect : array-like object, optional
985
+ Crystal vector for the a2 vector to use for plotting. Default
986
+ value of None uses the saved a2vect.
987
+ xvect : array-like object, optional
988
+ Crystal vector to align with the plotting x-axis for
989
+ non-normalized plots. If not given, this is taken as the Cartesian
990
+ of a1vect.
991
+ length_unit : str, optional
992
+ The unit of length to display non-normalized axes values in.
993
+ Default value is 'Å'.
994
+ energyperarea_unit : str, optional
995
+ The unit of energy per area to display the stacking fault energies
996
+ in. Default value is 'eV/Å^2'.
997
+ numx : int, optional
998
+ The number of plotting points to use along the x-axis. Default
999
+ value is 100.
1000
+ numy : int, optional
1001
+ The number of plotting points to use along the y-axis. Default
1002
+ value is 100.
1003
+ figsize : tuple or None, optional
1004
+ The figure's x,y dimensions. If None (default), the values are
1005
+ scaled such that the x,y spacings are approximately equal, and the
1006
+ larger of the two values is set to 10.
1007
+ **kwargs : dict, optional
1008
+ Additional keywords are passed into the underlying
1009
+ matplotlib.pyplot.pcolormesh(). This allows control of such things
1010
+ like the colormap (cmap).
1011
+
1012
+ Returns
1013
+ -------
1014
+ matplotlib.pyplot.figure
1015
+ The generated figure allowing users to perform additional
1016
+ modifications.
1017
+ """
1018
+
1019
+ # Generate the surface plot
1020
+ fig = self.gamma.E_gsf_surface_plot(normalize=normalize, smooth=smooth,
1021
+ a1vect=a1vect, a2vect=a2vect, xvect=xvect,
1022
+ length_unit=length_unit, energyperarea_unit=energyperarea_unit,
1023
+ numx=numx, numy=numy, figsize=figsize, **kwargs)
1024
+
1025
+ hasvals = True
1026
+ # Default values are class properties
1027
+ if x is None:
1028
+ try:
1029
+ x = self.x
1030
+ except:
1031
+ hasvals = False
1032
+ if disregistry is None:
1033
+ try:
1034
+ disregistry = self.disregistry
1035
+ except:
1036
+ hasvals = False
1037
+ if hasvals:
1038
+
1039
+ # Get xvect direction
1040
+ if xvect is None:
1041
+ if a1vect is None:
1042
+ a1vect = self.gamma.a1vect
1043
+ xvect = np.dot(a1vect, self.gamma.box.vects)
1044
+
1045
+ # Transform disregistry to gamma surface pos
1046
+ pos = disregistry.dot(self.transform)
1047
+
1048
+ # Transform to x, y plotting coordinates and plot
1049
+ x, y = self.gamma.pos_to_xy(pos, xvect=xvect)
1050
+ x = uc.get_in_units(x, length_unit)
1051
+ y = uc.get_in_units(y, length_unit)
1052
+ plt.plot(x, y, fmt)
1053
+
1054
+ return fig
1055
+
1056
+ def E_gsf_vs_x_plot(self,
1057
+ x: Optional[npt.ArrayLike] = None,
1058
+ disregistry: Optional[npt.ArrayLike] = None,
1059
+ figsize: Optional[tuple] = None,
1060
+ length_unit: str = 'Å',
1061
+ energyperarea_unit: str = 'eV/Å^2') -> plt.figure:
1062
+ """
1063
+ Generates a plot of the stacking fault energy, i.e. misfit energy,
1064
+ associated with the disregistry values for each x coordinate.
1065
+
1066
+ Parameters
1067
+ ----------
1068
+ x : array-like object, optional
1069
+ x-coordinates. Default value is the stored x-coordinates. If x
1070
+ or disregistry are not set/given, then the disregistry path will
1071
+ not be added.
1072
+ disregistry : array-like object, optional
1073
+ (N, 3) shaped array of disregistry vectors at each x-coordinate.
1074
+ Default value is the stored disregistry values. If x
1075
+ or disregistry are not set/given, then the disregistry path will
1076
+ not be added.
1077
+ figsize : tuple or None, optional
1078
+ The figure's x,y dimensions. If None (default), then a figure
1079
+ size of (10, 6) will be generated.
1080
+ length_unit : str, optional
1081
+ The unit of length to display x coordinates in. Default value is
1082
+ 'Å'.
1083
+ energyperarea_unit : str, optional
1084
+ The unit of energy per area to display the stacking fault energies
1085
+ in. Default value is 'eV/Å^2'.
1086
+
1087
+ Returns
1088
+ -------
1089
+ matplotlib.pyplot.figure
1090
+ The generated figure allowing users to perform additional
1091
+ modifications.
1092
+ """
1093
+ hasvals = True
1094
+ # Default values are class properties
1095
+ if x is None:
1096
+ try:
1097
+ x = self.x
1098
+ except:
1099
+ hasvals = False
1100
+ if disregistry is None:
1101
+ try:
1102
+ disregistry = self.disregistry
1103
+ except:
1104
+ hasvals = False
1105
+ if hasvals:
1106
+
1107
+ if figsize is None:
1108
+ figsize=(10,6)
1109
+
1110
+ gsf = self.gamma.E_gsf(pos=disregistry.dot(self.transform))
1111
+
1112
+ fig = plt.figure(figsize=figsize)
1113
+ plt.plot(uc.get_in_units(x, length_unit),
1114
+ uc.get_in_units(gsf, energyperarea_unit))
1115
+ plt.xlabel(f'x-coordinate (${length_unit}$)', size='xx-large')
1116
+ plt.ylabel(f'Stacking fault energy (${energyperarea_unit}$)', size='xx-large')
1117
+ return fig
1118
+
1119
+ else:
1120
+ print('x and disregistry must be set/given to check stacking fault energies')
1121
+
1122
+
1123
+ def load(self,
1124
+ model: Union[str, io.IOBase, DM],
1125
+ gamma: Optional[GammaSurface] = None):
1126
+ """
1127
+ Load solution from a data model.
1128
+
1129
+ Parameters
1130
+ ----------
1131
+ model : str, file-like object or DataModelDict
1132
+ The semi-discrete-Peierls-Nabarro data model to load.
1133
+ gamma : atomman.defect.GammaSurface, optional
1134
+ The gamma surface to use. If not given, will check to see if the
1135
+ content is inside model.
1136
+
1137
+ Raises
1138
+ ------
1139
+ ValueError
1140
+ If the gamma surface information is not given and it is not found
1141
+ in model.
1142
+ """
1143
+
1144
+ # Identify model element
1145
+ try:
1146
+ sdpn = DM(model).find('semidiscrete-variational-Peierls-Nabarro')
1147
+ except:
1148
+ sdpn = DM(model).find('semi-discrete-Peierls-Nabarro')
1149
+
1150
+ # Load calculation parameters
1151
+ params = sdpn['parameter']
1152
+ try:
1153
+ self.__transform = uc.value_unit(params['transform'])
1154
+ except:
1155
+ self.__transform = uc.value_unit(params['axes'])
1156
+ self.__K_tensor = uc.value_unit(params['K_tensor'])
1157
+ self.__burgers = uc.value_unit(params['burgers'])
1158
+ self.tau = uc.value_unit(params['tau'])
1159
+ self.alpha = uc.value_unit(params['alpha'])
1160
+ self.beta = uc.value_unit(params['beta'])
1161
+ self.cutofflongrange = uc.value_unit(params['cutofflongrange'])
1162
+ self.fullstress = params['fullstress']
1163
+ self.cdiffelastic = params['cdiffelastic']
1164
+ self.cdiffsurface = params['cdiffsurface']
1165
+ self.cdiffstress = params['cdiffstress']
1166
+ self.min_method = params['min_method']
1167
+ self.min_options = params['min_options']
1168
+ self.min_kwargs = params.get('min_kwargs', None)
1169
+
1170
+ # Load gamma
1171
+ if gamma is None:
1172
+ try:
1173
+ gamma = GammaSurface(model)
1174
+ except:
1175
+ raise ValueError('No gamma surface in model or given!')
1176
+ elif not isinstance(gamma, GammaSurface):
1177
+ gamma = GammaSurface(gamma)
1178
+ self.__gamma = gamma
1179
+
1180
+ # Load calculation solution
1181
+ solution = sdpn['solution']
1182
+ self.x = uc.value_unit(solution['x'])
1183
+ self.disregistry = uc.value_unit(solution['disregistry'])
1184
+
1185
+ def model(self,
1186
+ length_unit: str = 'Å',
1187
+ energyperarea_unit: str = 'eV/Å^2',
1188
+ pressure_unit: str = 'GPa',
1189
+ include_gamma: bool = False) -> DM:
1190
+ """
1191
+ Generate a data model for the object.
1192
+
1193
+ Parameters
1194
+ ----------
1195
+ length_unit : str, optional
1196
+ The unit of length to save values as. Default is 'Å'.
1197
+ energyperarea_unit : str, optional
1198
+ The unit of energy per area to save fault energy values as. Only
1199
+ used if the gamma surface information is included. Default value
1200
+ is 'eV/Å^2'.
1201
+ pressure_unit : str, optional
1202
+ The unit of pressure to save values as. Default is 'GPa'.
1203
+ include_gamma : bool, optional
1204
+ Flag indicating if the gamma surface data is to be included.
1205
+ Default value is False.
1206
+
1207
+ Returns
1208
+ -------
1209
+ DataModelDict
1210
+ The data model containing all input parameters and the current
1211
+ disregistry vectors.
1212
+ """
1213
+ model = DM()
1214
+ model['semidiscrete-variational-Peierls-Nabarro'] = sdpn = DM()
1215
+
1216
+ sdpn['parameter'] = params = DM()
1217
+ params['transform'] = uc.model(self.transform, None)
1218
+ params['K_tensor'] = uc.model(self.K_tensor, pressure_unit)
1219
+ params['tau'] = uc.model(self.tau, pressure_unit)
1220
+ params['alpha'] = uc.model(self.alpha, pressure_unit+'/'+length_unit)
1221
+
1222
+ params['beta'] = uc.model(self.beta, pressure_unit+'*'+length_unit)
1223
+ params['cdiffelastic'] = self.cdiffelastic
1224
+ params['cdiffsurface'] = self.cdiffsurface
1225
+ params['cdiffstress'] = self.cdiffstress
1226
+ params['cutofflongrange'] = uc.model(self.cutofflongrange, length_unit)
1227
+ params['burgers'] = uc.model(self.burgers, length_unit)
1228
+ params['fullstress'] = self.fullstress
1229
+ params['min_method'] = self.min_method
1230
+ params['min_options'] = self.min_options
1231
+ if len(self.min_kwargs) > 0:
1232
+ params['min_kwargs'] = self.min_kwargs
1233
+
1234
+ if include_gamma is True:
1235
+ sdpn['generalized-stacking-fault'] = self.gamma.model(
1236
+ length_unit=length_unit,
1237
+ energyperarea_unit=energyperarea_unit)
1238
+
1239
+ sdpn['solution'] = solution = DM()
1240
+ solution['x'] = uc.model(self.x, length_unit)
1241
+ solution['disregistry'] = uc.model(self.disregistry, length_unit)
1242
+
1243
+ return model
atomman/source/atomman/defect/StackingFault.py ADDED
@@ -0,0 +1,667 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Standard Python libraries
3
+ import io
4
+ from copy import deepcopy
5
+ from typing import Generator, Optional, Tuple, Union
6
+
7
+ from DataModelDict import DataModelDict as DM
8
+
9
+ # http://www.numpy.org/
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+
13
+ from yabadaba.record import Record
14
+
15
+ # atomman imports
16
+ from ..tools import miller
17
+ from . import FreeSurface
18
+ from .. import System, load
19
+ from ..library import load_record, Database
20
+
21
+ class StackingFault(FreeSurface):
22
+ """
23
+ Class for generating stacking fault atomic configurations.
24
+ """
25
+ def __init__(self,
26
+ hkl: npt.ArrayLike,
27
+ ucell: System,
28
+ cutboxvector: str = 'c',
29
+ maxindex: Optional[int] = None,
30
+ a1vect_uvw: Optional[npt.ArrayLike] = None,
31
+ a2vect_uvw: Optional[npt.ArrayLike] = None,
32
+ conventional_setting: str = 'p',
33
+ shift: Optional[npt.ArrayLike] = None,
34
+ shiftindex: Optional[int] = None,
35
+ shiftscale: bool = False,
36
+ tol: float = 1e-8):
37
+ """
38
+ Class initializer. Identifies the proper rotations for the given hkl plane
39
+ and cutboxvector, and creates the rotated cell.
40
+
41
+ Parameters
42
+ ----------
43
+ hkl : array-like object
44
+ The free surface plane to generate expressed in either 3 indices
45
+ Miller (hkl) format or 4 indices Miller-Bravais (hkil) format.
46
+ ucell : atomman.System
47
+ The unit cell to use in generating the system.
48
+ cutboxvector : str, optional
49
+ Specifies which of the three box vectors corresponds to the
50
+ out-of-plane vector. Default value is c.
51
+ maxindex : int, optional
52
+ Max uvw index value to use in identifying the best uvw set for the
53
+ out-of-plane vector. If not given, will use the largest absolute
54
+ index between the given hkl and the initial in-plane vector guesses.
55
+ a1vect_uvw : array-like object, optional
56
+ The crystal vector to use for one of the two shifting vectors. If
57
+ not given, will be set to the shortest in-plane lattice vector.
58
+ a2vect_uvw : array-like object, optional
59
+ The crystal vector to use for one of the two shifting vectors. If
60
+ not given, will be set to the shortest in-plane lattice vector not
61
+ parallel to a1vect_uvw.
62
+ conventional_setting : str, optional
63
+ Allows for rotations of a primitive unit cell to be determined from
64
+ (hkl) indices specified relative to a conventional unit cell. Allowed
65
+ settings: 'p' for primitive (no conversion), 'f' for face-centered,
66
+ 'i' for body-centered, and 'a', 'b', or 'c' for side-centered. Default
67
+ behavior is to perform no conversion, i.e. take (hkl) relative to the
68
+ given ucell.
69
+ shift : array-like object, optional
70
+ Applies a shift to all atoms. Different values allow for free surfaces with
71
+ different termination planes to be selected. shift is taken as absolute
72
+ if shiftscale is False, or relative to the rotated cell's box vectors
73
+ if shiftscale is True. Cannot be given with shiftindex. If
74
+ neither shift nor shiftindex is given then shiftindex = 0 is used.
75
+ shiftindex : float, optional
76
+ The index of the identified shifts based on the rotated
77
+ cell to use. Different values allow for the selection of different
78
+ atomic planes neighboring the slip plane. Cannot be given with shift.
79
+ If neither shift nor shiftindex is given then shiftindex = 0 is used.
80
+ shiftscale : bool, optional
81
+ If False (default), a given shift value will be taken as absolute
82
+ Cartesian. If True, a given shift will be taken relative to the
83
+ rotated cell's box vectors.
84
+ tol : float, optional
85
+ Tolerance parameter used to round off near-zero values. Default
86
+ value is 1e-8.
87
+ """
88
+ super().__init__(hkl, ucell, cutboxvector=cutboxvector, maxindex=maxindex,
89
+ conventional_setting=conventional_setting,
90
+ shift=shift, shiftindex=shiftindex, shiftscale=shiftscale, tol=tol)
91
+
92
+ # Extract a1vect and a2vect values from uvws and rcell.box
93
+ if a1vect_uvw is None and a2vect_uvw is None:
94
+
95
+ # Set a1index and a2index based on cutboxvector
96
+ if self.cutboxvector == 'a':
97
+ a1index, a2index = 1, 2
98
+ elif self.cutboxvector == 'b':
99
+ a1index, a2index = 2, 0
100
+ elif self.cutboxvector == 'c':
101
+ a1index, a2index = 0, 1
102
+
103
+ self.a1vect_uvw = self.uvws[a1index]
104
+ self.a2vect_uvw = self.uvws[a2index]
105
+
106
+ # Set given a1vect_uvw and a2vect_uvw values
107
+ elif a1vect_uvw is not None and a2vect_uvw is not None:
108
+ self.a1vect_uvw = a1vect_uvw
109
+ self.a2vect_uvw = a2vect_uvw
110
+
111
+ else:
112
+ raise ValueError('a1vect_uvw and a2vect_uvw either both need to be given or not given')
113
+
114
+ # Set empty attributes
115
+ self.__faultpos_cart = None
116
+ self.__faultpos_rel = None
117
+ self.__abovefault = None
118
+
119
+ @classmethod
120
+ def fromrecord(cls,
121
+ record: Union[str, io.IOBase, DM, Record],
122
+ ucell: Union[System, str, io.IOBase],
123
+ maxindex: Optional[int] = None,
124
+ tol: float = 1e-7):
125
+ """
126
+ Construct a StackingFault object based on parameters in a stacking_fault
127
+ record and unit cell information.
128
+
129
+ Parameters
130
+ ----------
131
+ record : atomman.library.record.StackingFault, str, file-like object or DataModelDict
132
+ A StackingFault record object or the model contents for one.
133
+ ucell : atomman.System
134
+ The unit cell to use in generating the system.
135
+ maxindex : int, optional
136
+ Max uvw index value to use in identifying the best uvw set for the
137
+ out-of-plane vector. If not given, will use the largest absolute
138
+ index between the given hkl and the initial in-plane vector guesses.
139
+ tol : float, optional
140
+ Tolerance parameter used to round off near-zero values. Default
141
+ value is 1e-8.
142
+ """
143
+ # Create record object if needed
144
+ if not isinstance(record, Record):
145
+ record = load_record('stacking_fault', model=record)
146
+
147
+ # Extract parameters in the record
148
+ hkl = miller.fromstring(record.parameters['hkl'])
149
+ shiftindex = int(record.parameters.get('shiftindex', 0))
150
+ cutboxvector = record.parameters['cutboxvector']
151
+ conventional_setting = record.parameters.get('conventional_setting', 'p')
152
+ a1vect_uvw = miller.fromstring(record.parameters['a1vect_uvw'])
153
+ a2vect_uvw = miller.fromstring(record.parameters['a2vect_uvw'])
154
+
155
+ return cls(hkl=hkl, ucell=ucell, cutboxvector=cutboxvector,
156
+ maxindex=maxindex, shiftindex=shiftindex,
157
+ a1vect_uvw=a1vect_uvw, a2vect_uvw=a2vect_uvw,
158
+ conventional_setting=conventional_setting, tol=tol)
159
+
160
+ @classmethod
161
+ def fromdatabase(cls,
162
+ name: Optional[str] = None,
163
+ ucell: Optional[System] = None,
164
+ database: Optional[Database] = None,
165
+ prompt: bool = True,
166
+ maxindex: Optional[int] = None,
167
+ tol: float = 1e-7,
168
+ **kwargs):
169
+ """
170
+ Construct a StackingFault object based on record(s) retrieved from the
171
+ reference database.
172
+
173
+ Parameters
174
+ ----------
175
+ name : str or None, optional
176
+ The name of the stacking_fault record to retrieve from the database.
177
+ Alternatively, you can use any other query keyword arguments supported
178
+ by the stacking_fault record style (see **kwargs below for more info).
179
+ ucell : atomman.System or None, optional
180
+ The unit cell to use in generating the system. If None (default), then
181
+ the crystal_prototype record that matches the defect's family setting
182
+ will be loaded from the database. Note that if None then the
183
+ crystal-specific info (lattice constants and symbols) should be given
184
+ here as kwargs (see below).
185
+ database : atomman.library.Database or None, optional
186
+ A Database object to use to fetch the records. If None (default), then
187
+ a new Database instance will be created.
188
+ prompt : bool
189
+ If prompt=True (default) then a screen input will ask for a selection
190
+ if multiple matching stacking_fault (or crystal_prototype) records are
191
+ found. If prompt=False, then an error will be thrown if multiple
192
+ matches are found.
193
+ maxindex : int, optional
194
+ Max uvw index value to use in identifying the best uvw set for the
195
+ out-of-plane vector. If not given, will use the largest absolute
196
+ index between the given hkl and the initial in-plane vector guesses.
197
+ tol : float, optional
198
+ Tolerance parameter used to round off near-zero values. Default
199
+ value is 1e-8.
200
+ **kwargs : any
201
+ The recognized kwargs include the query keywords for free_surface
202
+ records (key, id, family, hkl, shiftindex, cutboxvector), and the
203
+ crystal-specific parameters recognized by the prototype load style
204
+ (a, b, c, alpha, beta, gamma, symbols). The non-trivial
205
+ crystal-specific parameters should be for the crystal if ucell is
206
+ not given above as the crystal prototype lacks this information.
207
+ """
208
+ # Initialize a database if needed
209
+ if database is None:
210
+ database = Database()
211
+
212
+ # Extract ucell parameters from kwargs
213
+ prototype_kwargs = {}
214
+ prototype_kwargs_names = ['a', 'b', 'c', 'alpha', 'beta', 'gamma', 'symbols']
215
+ for prototype_kwargs_name in prototype_kwargs_names:
216
+ if prototype_kwargs_name in kwargs:
217
+ prototype_kwargs[prototype_kwargs_name] = kwargs.pop(prototype_kwargs_name)
218
+
219
+ # Fetch matching defect record
220
+ record = database.get_record('stacking_fault', name=name, prompt=prompt, **kwargs)
221
+
222
+ # Fetch crystal prototype unit cell if needed
223
+ if ucell is None:
224
+ ucell = load('prototype', name=record.family, **prototype_kwargs)
225
+ else:
226
+ if len(prototype_kwargs) > 0:
227
+ raise ValueError('crystal-specific kwargs cannot be given with ucell')
228
+
229
+ return cls.fromrecord(record=record, ucell=ucell, maxindex=maxindex, tol=tol)
230
+
231
+ @property
232
+ def a1vect_uvw(self) -> np.ndarray:
233
+ """
234
+ numpy.ndarray : One of the two conventional lattice shift vectors in Miller
235
+ or Miller-Bravais indices.
236
+ """
237
+ return self.__a1vect_uvw
238
+
239
+ @property
240
+ def a2vect_uvw(self) -> np.ndarray:
241
+ """
242
+ numpy.ndarray : One of the two conventional lattice shift vectors in Miller
243
+ or Miller-Bravais indices.
244
+ """
245
+ return self.__a2vect_uvw
246
+
247
+ @a1vect_uvw.setter
248
+ def a1vect_uvw(self, value: npt.ArrayLike):
249
+
250
+ value = np.asarray(value)
251
+
252
+ # Check shape and convert [uvtw] to [uvw] if needed
253
+ if value.shape == (4,):
254
+ uvw = miller.vector4to3(value)
255
+ elif value.shape == (3,):
256
+ uvw = value
257
+ if self.hkl.shape == (4,):
258
+ value = miller.vector3to4(value)
259
+ else:
260
+ raise ValueError('Invalid uvw shape: must have 3 or 4 values.')
261
+
262
+ # Convert to primitive cell
263
+ uvw = miller.vector_conventional_to_primitive(uvw, self.conventional_setting)
264
+
265
+ # Convert to Cartesian wrt rotated system
266
+ cart = self.transform.dot(miller.vector_crystal_to_cartesian(uvw, self.ucell.box))
267
+
268
+ # Check that Cartesian vector is in slip plane
269
+ if not np.isclose(cart[self.cutindex], 0.0):
270
+ raise ValueError(f'shift vector {value} not in fault plane {self.hkl}')
271
+
272
+ # Save uvw and cart
273
+ self.__a1vect_uvw = value
274
+ self.__a1vect_cart = cart
275
+
276
+ @a2vect_uvw.setter
277
+ def a2vect_uvw(self, value: npt.ArrayLike):
278
+
279
+ value = np.asarray(value)
280
+
281
+ # Check shape and convert [uvtw] to [uvw] if needed
282
+ if value.shape == (4,):
283
+ uvw = miller.vector4to3(value)
284
+ elif value.shape == (3,):
285
+ uvw = value
286
+ if self.hkl.shape == (4,):
287
+ value = miller.vector3to4(value)
288
+ else:
289
+ raise ValueError('Invalid uvw shape: must have 3 or 4 values.')
290
+
291
+ # Convert to primitive cell
292
+ uvw = miller.vector_conventional_to_primitive(uvw, self.conventional_setting)
293
+
294
+ # Convert to Cartesian wrt rotated system
295
+ cart = self.transform.dot(miller.vector_crystal_to_cartesian(uvw, self.ucell.box))
296
+
297
+ # Check that Cartesian vector is in slip plane
298
+ if not np.isclose(cart[self.cutindex], 0.0):
299
+ raise ValueError(f'shift vector {value} not in fault plane {self.hkl}')
300
+
301
+ # Save uvw and cart
302
+ self.__a2vect_uvw = value
303
+ self.__a2vect_cart = cart
304
+
305
+ @property
306
+ def a1vect_cart(self) -> np.ndarray:
307
+ """numpy.ndarray : One of the two shift vectors in Cartesian relative to system."""
308
+ return self.__a1vect_cart
309
+
310
+ @property
311
+ def a2vect_cart(self) -> np.ndarray:
312
+ """numpy.ndarray : One of the two shift vectors in Cartesian relative to system."""
313
+ return self.__a2vect_cart
314
+
315
+ @property
316
+ def faultpos_cart(self) -> float:
317
+ """float : The Cartesian position of the slip plane."""
318
+ if self.__faultpos_cart is not None:
319
+ return self.__faultpos_cart
320
+ else:
321
+ raise AttributeError('system not yet built. Use build_system() or surface().')
322
+
323
+ @property
324
+ def faultpos_rel(self) -> float:
325
+ """float : The fractional position of the slip plane."""
326
+ if self.__faultpos_rel is not None:
327
+ return self.__faultpos_rel
328
+ else:
329
+ raise AttributeError('system not yet built. Use build_system() or surface().')
330
+
331
+ @property
332
+ def abovefault(self) -> list:
333
+ """list : Indices of all atoms in system above the slip plane."""
334
+ if self.__abovefault is not None:
335
+ return self.__abovefault
336
+ else:
337
+ raise AttributeError('system not yet built. Use build_system() or surface().')
338
+
339
+ @faultpos_cart.setter
340
+ def faultpos_cart(self, value: float):
341
+
342
+ # faultpos_rel = (faultpos_cart - origin) / width (for origin, width || cutindex)
343
+ faultpos_rel = ((value - self.system.box.origin[self.cutindex])
344
+ / self.system.box.vects[self.cutindex, self.cutindex])
345
+ if faultpos_rel < 0.0 or faultpos_rel > 1.0:
346
+ raise ValueError('faultpos is outside system')
347
+
348
+ self.__faultpos_rel = faultpos_rel
349
+ self.__faultpos_cart = value
350
+
351
+ # Identify atoms above fault plane position
352
+ self.__abovefault = self.system.atoms.pos[:, self.cutindex] > (self.faultpos_cart)
353
+
354
+ @faultpos_rel.setter
355
+ def faultpos_rel(self, value: float):
356
+
357
+ if value < 0.0 or value > 1.0:
358
+ raise ValueError('faultpos is outside system')
359
+
360
+ self.__faultpos_rel = value
361
+
362
+ # faultpos_cart = origin + faultpos_rel * width (for origin, width || cutindex)
363
+ self.__faultpos_cart = (self.system.box.origin[self.cutindex]
364
+ + self.faultpos_rel
365
+ * self.system.box.vects[self.cutindex, self.cutindex])
366
+
367
+ # Identify atoms above fault plane position
368
+ self.__abovefault = self.system.atoms.pos[:, self.cutindex] > (self.faultpos_cart)
369
+
370
+ def surface(self,
371
+ shift: Optional[npt.ArrayLike] = None,
372
+ shiftindex: Optional[int] = None,
373
+ shiftscale: bool = None,
374
+ vacuumwidth: Optional[float] = None,
375
+ minwidth: Optional[float] = None,
376
+ sizemults: Optional[list] = None,
377
+ even: bool = False,
378
+ faultpos_rel: Optional[float] = None,
379
+ faultpos_cart: Optional[float] = None) -> System:
380
+ """
381
+ Generates the free surface atomic system, which is used as the basis for generating
382
+ the stacking fault configuration(s).
383
+
384
+ Parameters
385
+ ----------
386
+ shift : array-like object, optional
387
+ Applies a shift to all atoms. Different values allow for free surfaces with
388
+ different termination planes to be selected. shift is taken as absolute
389
+ if shiftscale is False, or relative to the rotated cell's box vectors
390
+ if shiftscale is True. Cannot be given with shiftindex. If
391
+ neither shift nor shiftindex is given then the current value set to the
392
+ shift attribute will be used.
393
+ shiftindex : float, optional
394
+ The index of the identified shifts based on the rotated
395
+ cell to use. Different values allow for the selection of different
396
+ atomic planes neighboring the slip plane. Cannot be given with shift.
397
+ If neither shift nor shiftindex is given then the current value set to
398
+ the shift attribute will be used.
399
+ shiftscale : bool, optional
400
+ If False (default), a given shift value will be taken as absolute
401
+ Cartesian. If True, a given shift will be taken relative to the
402
+ rotated cell's box vectors.
403
+ vacuumwidth : float, optional
404
+ If given, the free surface is created by modifying the system's box to insert
405
+ a region of vacuum with this width. This is typically used for DFT calculations
406
+ where it is computationally preferable to insert a vacuum region and keep all
407
+ dimensions periodic.
408
+ sizemults : list or tuple, optional
409
+ The three System.supersize multipliers [a_mult, b_mult, c_mult] to use on the
410
+ rotated cell to build the final system. Note that the cutboxvector sizemult
411
+ must be an integer and not a tuple. Default value is [1, 1, 1].
412
+ minwidth : float, optional
413
+ If given, the sizemult along the cutboxvector will be selected such that the
414
+ width of the resulting final system in that direction will be at least this
415
+ value. If both sizemults and minwidth are given, then the larger of the two
416
+ in the cutboxvector direction will be used.
417
+ even : bool, optional
418
+ A True value means that the sizemult for cutboxvector will be made an even
419
+ number by adding 1 if it is odd. Default value is False.
420
+ faultpos_rel : float, optional
421
+ The position to place the slip plane within the system given as a
422
+ relative coordinate along the out-of-plane direction. faultpos_rel
423
+ and faultpos_cart cannot both be given. Default value is 0.5 if
424
+ faultpos_cart is also not given.
425
+ faultpos_cart : float, optional
426
+ The position to place the slip plane within the system given as a
427
+ Cartesian coordinate along the out-of-plane direction. faultpos_rel
428
+ and faultpos_cart cannot both be given.
429
+
430
+ Returns
431
+ -------
432
+ atomman.System
433
+ The free surface atomic system.
434
+ """
435
+ super().surface(shift=shift, shiftindex=shiftindex, shiftscale=shiftscale,
436
+ vacuumwidth=vacuumwidth, minwidth=minwidth,
437
+ sizemults=sizemults, even=even)
438
+
439
+ # Set Cartesian fault position
440
+ if faultpos_cart is not None:
441
+ if faultpos_rel is not None:
442
+ raise ValueError('faultpos_rel and faultpos_cart cannot both be given')
443
+ self.faultpos_cart = faultpos_cart
444
+
445
+ # Set relative fault position
446
+ elif faultpos_rel is not None:
447
+ self.faultpos_rel = faultpos_rel
448
+
449
+ # Set default fault position
450
+ else:
451
+ self.faultpos_rel = 0.5
452
+
453
+ return self.system
454
+
455
+ def fault(self,
456
+ a1: Optional[float] = None,
457
+ a2: Optional[float] = None,
458
+ outofplane: Optional[float] = None,
459
+ faultshift: Optional[npt.ArrayLike] = None,
460
+ minimum_r: Optional[float] = None,
461
+ a1vect_uvw: Optional[npt.ArrayLike] = None,
462
+ a2vect_uvw: Optional[npt.ArrayLike] = None,
463
+ faultpos_cart: Optional[float] = None,
464
+ faultpos_rel: Optional[float] = None) -> System:
465
+ """
466
+ Generates a fault configuration by displacing all atoms above the slip
467
+ plane.
468
+
469
+ Parameters
470
+ ----------
471
+ a1 : float, optional
472
+ The fractional coordinate of a1vect to shift by.
473
+ Default value is 0.0.
474
+ a2 : float, optional
475
+ The fractional coordinate of a2vect to shift by.
476
+ Default value is 0.0.
477
+ outofplane : float, optional
478
+ An out-of-plane shift, given in absolute units.
479
+ Default value is 0.0.
480
+ faultshift : array-like object, optional
481
+ The full shifting vector to displace the atoms above the slip
482
+ plane by. Cannot be given with a1, a2, or outofplane.
483
+ minimum_r : float, optional
484
+ Specifies the minimum allowed interatomic spacing across the slip
485
+ plane. If any sets of atoms are closer than this value then the
486
+ outofplane shift is increased. Default value is None, which
487
+ performs no adjustment.
488
+ a1vect_uvw : array-like object, optional
489
+ The crystal vector to use for one of the two shifting vectors.
490
+ Included here for those wishing to override the values set during
491
+ class initialization.
492
+ a2vect_uvw : array-like object, optional
493
+ The crystal vector to use for one of the two shifting vectors.
494
+ Included here for those wishing to override the values set during
495
+ class initialization.
496
+ faultpos_rel : float, optional
497
+ The position to place the slip plane within the system given as a
498
+ relative coordinate along the out-of-plane direction. Included
499
+ here for those wishing to override the value set when surface()
500
+ was called. faultpos_rel and faultpos_cart cannot both be given.
501
+ faultpos_cart : float, optional
502
+ The position to place the slip plane within the system given as a
503
+ Cartesian coordinate along the out-of-plane direction. Included
504
+ here for those wishing to override the value set when surface()
505
+ was called. faultpos_rel and faultpos_cart cannot both be given.
506
+
507
+ Returns
508
+ -------
509
+ atomman.System
510
+ The atomic configuration with stacking fault shift
511
+ """
512
+ # Update uvws and faultpos if given
513
+ if a1vect_uvw is not None:
514
+ self.a1vect_uvw = a1vect_uvw
515
+ if a2vect_uvw is not None:
516
+ self.a2vect_uvw = a2vect_uvw
517
+ if faultpos_cart is not None:
518
+ if faultpos_rel is not None:
519
+ raise ValueError('faultpos_rel and faultpos_cart cannot both be given')
520
+ self.faultpos_cart = faultpos_cart
521
+ elif faultpos_rel is not None:
522
+ self.faultpos_rel = faultpos_rel
523
+
524
+ # Define out of plane unit vector
525
+ ovect = np.zeros(3)
526
+ ovect[self.cutindex] = 1.0
527
+
528
+ # Identify the two non-cut indices
529
+ inindex = []
530
+ for i in range(3):
531
+ if i != self.cutindex:
532
+ inindex.append(i)
533
+
534
+ # Calculate faultshift
535
+ if a1 is not None or a2 is not None or outofplane is not None:
536
+ if faultshift is not None:
537
+ raise ValueError('a1, a2, outofplane cannot be given with faultshift')
538
+ if a1 is None:
539
+ a1 = 0.0
540
+ if a2 is None:
541
+ a2 = 0.0
542
+ if outofplane is None:
543
+ outofplane = 0.0
544
+ faultshift = a1 * self.a1vect_cart + a2 * self.a2vect_cart + outofplane * ovect
545
+
546
+ # Set default faultshift
547
+ elif faultshift is None:
548
+ faultshift = np.array([0.0, 0.0, 0.0])
549
+
550
+ # Shift atoms above the fault by faultshift
551
+ sfsystem = deepcopy(self.system)
552
+ sfsystem.atoms.pos[self.abovefault] += faultshift
553
+ sfsystem.wrap()
554
+
555
+ # Add additional outofplane shift if necessary
556
+ if minimum_r is not None:
557
+ # Get all atoms within minimum_r of fault position
558
+ top_pos = sfsystem.atoms.pos[self.abovefault]
559
+ top_pos = top_pos[top_pos[:, self.cutindex] <= self.faultpos_cart + minimum_r]
560
+ bot_pos = sfsystem.atoms.pos[~self.abovefault] #
561
+ bot_pos = bot_pos[bot_pos[:, self.cutindex] >= self.faultpos_cart - minimum_r]
562
+ if top_pos.shape[0] > 0 and bot_pos.shape[0] > 0:
563
+ dmag_min = minimum_r
564
+ dvect_min = None
565
+
566
+ for i in range(top_pos.shape[0]):
567
+ dvect = sfsystem.dvect(bot_pos, top_pos[i])
568
+ if dvect.shape == (3,):
569
+ dvect = dvect.reshape(1,3)
570
+ dmag = np.linalg.norm(dvect, axis=1)
571
+ i = np.argmin(dmag)
572
+ if dmag[i] < dmag_min:
573
+ dmag_min = dmag[i]
574
+ dvect_min = dvect[i]
575
+
576
+ if dvect_min is not None:
577
+
578
+ new = (minimum_r**2 - dvect_min[inindex[0]]**2 - dvect_min[inindex[1]]**2)**0.5
579
+ outofplane = new - dvect_min[self.cutindex]
580
+ faultshift = outofplane * ovect
581
+ sfsystem.atoms.pos[self.abovefault] += faultshift
582
+ sfsystem.wrap()
583
+
584
+ return sfsystem
585
+
586
+ def iterfaultmap(self,
587
+ num_a1: Optional[int] = None,
588
+ num_a2: Optional[int] = None,
589
+ outofplane: Optional[float] = None,
590
+ minimum_r: Optional[float] = None,
591
+ a1vect_uvw: Optional[npt.ArrayLike] = None,
592
+ a2vect_uvw: Optional[npt.ArrayLike] = None,
593
+ faultpos_cart: Optional[float] = None,
594
+ faultpos_rel: Optional[float] = None
595
+ ) -> Generator[Tuple[float, float, System], None, None]:
596
+ """
597
+ Iterates over generalized stacking fault configurations associated
598
+ with a 2D map of equally spaced a1, a2 coordinates.
599
+
600
+ Parameters
601
+ ----------
602
+ num_a1 : int
603
+ The number of a1 values to generate systems for.
604
+ Default value is 1 (only generate for a1=0.0).
605
+ num_a2 : int
606
+ The number of a2 values to generate systems for.
607
+ Default value is 1 (only generate for a2=0.0).
608
+ outofplane : float, optional
609
+ An out-of-plane shift, given in absolute units.
610
+ Default value is 0.0.
611
+ minimum_r : float, optional
612
+ Specifies the minimum allowed interatomic spacing across the slip
613
+ plane. If any sets of atoms are closer than this value then the
614
+ outofplane shift is increased. Default value is None, which
615
+ performs no adjustment.
616
+ a1vect_uvw : array-like object, optional
617
+ The crystal vector to use for one of the two shifting vectors.
618
+ Included here for those wishing to override the values set during
619
+ class initialization.
620
+ a2vect_uvw : array-like object, optional
621
+ The crystal vector to use for one of the two shifting vectors.
622
+ Included here for those wishing to override the values set during
623
+ class initialization.
624
+ faultpos_rel : float, optional
625
+ The position to place the slip plane within the system given as a
626
+ relative coordinate along the out-of-plane direction. Included
627
+ here for those wishing to override the value set when surface()
628
+ was called. faultpos_rel and faultpos_cart cannot both be given.
629
+ faultpos_cart : float, optional
630
+ The position to place the slip plane within the system given as a
631
+ Cartesian coordinate along the out-of-plane direction. Included
632
+ here for those wishing to override the value set when surface()
633
+ was called. faultpos_rel and faultpos_cart cannot both be given.
634
+
635
+ Yields
636
+ ------
637
+ a1 : float
638
+ The a1 fractional coordinate of a1vect.
639
+ a2 : float
640
+ The a2 fractional coordinate of a2vect.
641
+ atomman.System
642
+ The fault configuration associated with the a1, a2 shift.
643
+ """
644
+ # Update uvws and faultpos if given
645
+ if a1vect_uvw is not None:
646
+ self.a1vect_uvw = a1vect_uvw
647
+ if a2vect_uvw is not None:
648
+ self.a2vect_uvw = a2vect_uvw
649
+ if faultpos_cart is not None:
650
+ if faultpos_rel is not None:
651
+ raise ValueError('faultpos_rel and faultpos_cart cannot both be given')
652
+ self.faultpos_cart = faultpos_cart
653
+ elif faultpos_rel is not None:
654
+ self.faultpos_rel = faultpos_rel
655
+
656
+ if num_a1 is None:
657
+ num_a1 = 1
658
+ if num_a2 is None:
659
+ num_a2 = 1
660
+
661
+ # Construct mesh of regular points
662
+ a1s, a2s = np.meshgrid(np.linspace(0, 1, num_a1, endpoint=False),
663
+ np.linspace(0, 1, num_a2, endpoint=False))
664
+
665
+ for a1, a2 in zip(a1s.flat, a2s.flat):
666
+ yield a1, a2, self.fault(a1=a1, a2=a2, outofplane=outofplane,
667
+ minimum_r=minimum_r)