Spaces:
Sleeping
Sleeping
Upload 877 files
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +69 -0
- Dockerfile +18 -0
- README.md +27 -5
- app.py +45 -0
- atomman/mcp_output/README_MCP.md +77 -0
- atomman/mcp_output/analysis.json +871 -0
- atomman/mcp_output/diff_report.md +63 -0
- atomman/mcp_output/mcp_plugin/__init__.py +0 -0
- atomman/mcp_output/mcp_plugin/adapter.py +183 -0
- atomman/mcp_output/mcp_plugin/main.py +13 -0
- atomman/mcp_output/mcp_plugin/mcp_service.py +165 -0
- atomman/mcp_output/requirements.txt +16 -0
- atomman/mcp_output/start_mcp.py +30 -0
- atomman/mcp_output/workflow_summary.json +207 -0
- atomman/source/LICENSE.TXT +17 -0
- atomman/source/MANIFEST.in +9 -0
- atomman/source/README.rst +112 -0
- atomman/source/UPDATES.rst +682 -0
- atomman/source/__init__.py +4 -0
- atomman/source/atomman/VERSION +1 -0
- atomman/source/atomman/__init__.py +45 -0
- atomman/source/atomman/cluster/BondAngleMap.py +752 -0
- atomman/source/atomman/cluster/__init__.py +3 -0
- atomman/source/atomman/core/Atoms.py +572 -0
- atomman/source/atomman/core/Box.py +1112 -0
- atomman/source/atomman/core/ElasticConstants.py +1037 -0
- atomman/source/atomman/core/ElasticConstants2.py +1017 -0
- atomman/source/atomman/core/NeighborList.py +263 -0
- atomman/source/atomman/core/System.py +1267 -0
- atomman/source/atomman/core/__init__.py +14 -0
- atomman/source/atomman/core/displacement.py +49 -0
- atomman/source/atomman/core/dmag.pxd +3 -0
- atomman/source/atomman/core/dmag.pyx +150 -0
- atomman/source/atomman/core/dvect.pxd +3 -0
- atomman/source/atomman/core/dvect.pyx +153 -0
- atomman/source/atomman/core/nlist.pyx +334 -0
- atomman/source/atomman/defect/Boundary.py +803 -0
- atomman/source/atomman/defect/DifferentialDisplacement.py +733 -0
- atomman/source/atomman/defect/Dislocation/__init__.py +535 -0
- atomman/source/atomman/defect/Dislocation/_dipole.py +300 -0
- atomman/source/atomman/defect/Dislocation/_monopole.py +335 -0
- atomman/source/atomman/defect/Dislocation/_periodicarray.py +450 -0
- atomman/source/atomman/defect/FreeSurface.py +634 -0
- atomman/source/atomman/defect/GRIP.py +545 -0
- atomman/source/atomman/defect/GammaSurface.py +1366 -0
- atomman/source/atomman/defect/GrainBoundary.py +236 -0
- atomman/source/atomman/defect/InterstitialSite.py +168 -0
- atomman/source/atomman/defect/IsotropicVolterraDislocation.py +299 -0
- atomman/source/atomman/defect/SDVPN.py +1243 -0
- 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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
|
|
|
|
|
|
| 7 |
pinned: false
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|