#!/usr/bin/env python
# -*- coding: utf-8 -*-
# The MIT License (MIT)
# Copyright (c) 2020 Daniel Schick
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
# DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE
# OR OTHER DEALINGS IN THE SOFTWARE.
__all__ = ['Structure']
__docformat__ = 'restructuredtext'
from .layers import AmorphousLayer, UnitCell
from .. import u, Q_
from ..helpers import make_hash_md5, finderb
import itertools
import numpy as np
[docs]
class Structure:
"""Structure
Structure representation which holds various sub_structures.
Each sub_structure can be either a layer of :math:`N` UnitCell or
AmorphousLayer instances or a structure by itself.
It is possible to recursively build up 1D structures.
Args:
name (str): name of the sample.
Attributes:
name (str): name of sample.
sub_structures (list[AmorphousLayer, UnitCell, Structure]): list of
structures in sample.
substrate (Structure): structure of the substrate.
num_sub_systems (int): number of subsystems for heat and phonons
(electronic, lattice, spins, ...).
"""
def __init__(self, name):
self.name = name
self.num_sub_systems = 1
self.sub_structures = []
self.substrate = []
self.roughness = 0*u.nm
def __str__(self, tabs=0):
"""String representation of this class"""
tab_str = tabs*'\t'
class_str = tab_str + 'Structure properties:\n\n'
class_str += tab_str + 'Name : {:s}\n'.format(self.name)
class_str += tab_str + 'Thickness : {:0.2f}\n'.format(self.get_thickness().to('nm'))
class_str += tab_str + 'Roughness : {:0.2f}\n'.format(self.roughness)
class_str += tab_str + '----\n'
# traverse all substructures
for sub_structure in self.sub_structures:
if isinstance(sub_structure[0], (AmorphousLayer, UnitCell)):
# the substructure is an unitCell
class_str += tab_str + '{:d} times {:s}: {:0.2f}\n'.format(
sub_structure[1],
sub_structure[0].name,
sub_structure[1]*sub_structure[0].thickness.to('nm'))
else:
# the substructure is a structure instance by itself
# call the display() method recursively
class_str += tab_str + 'sub-structure {:d} times:\n'.format(
sub_structure[1])
class_str += sub_structure[0].__str__(tabs+1)
class_str += tab_str + '----\n'
# check for a substrate
if isinstance(self.substrate, Structure):
class_str += tab_str + 'Substrate:\n'
class_str += tab_str + '----\n'
class_str += tab_str + '{:d} times {:s}: {:0.2f}\n'.format(
self.substrate.sub_structures[0][1],
self.substrate.sub_structures[0][0].name,
self.substrate.sub_structures[0][1]
* self.substrate.sub_structures[0][0].thickness.to('nm'))
else:
class_str += tab_str + 'no substrate\n'
return class_str
[docs]
def visualize(self, unit='nm', fig_size=[20, 1], cmap='Set1', linewidth=0.1, show=True):
"""visualize
Simple visualization of the structure.
Args:
unit (str): SI unit of the distance of the Structure. Defaults to
'nm'.
fig_size (list[float]): figure size of the visualization plot.
Defaults to [20, 1].
cmap (str): Matplotlib colormap for colors of layers.
linewidth (float): line width of the patches.
show (boolean): show visualization plot at the end.
"""
import matplotlib.pyplot as plt
from matplotlib import patches
from matplotlib import cm
_, d_end, _ = self.get_distances_of_layers(True) # distance vector of all layers
layer_interfaces = np.append(0, d_end.to(unit).magnitude) # Append zero at the start
thickness = np.max(layer_interfaces)
layer_ids = self.get_unique_layers()[0]
N = len(layer_ids) # number of unique layers
colortable = {}
for i in range(N):
colortable[layer_ids[i]] = cm.get_cmap(cmap)(i)
plt.figure(figsize=fig_size)
ax = plt.axes()
for i, name in enumerate(self.get_layer_vectors()[1]):
col = colortable.get(name, 'k')
rect = patches.Rectangle((layer_interfaces[i], 0), np.diff(layer_interfaces)[i], 1,
linewidth=linewidth, facecolor=col, edgecolor='k')
ax.add_patch(rect)
plt.xlim(0, thickness)
plt.ylim(0, 1)
plt.xlabel('Distance [{:s}]'.format(unit))
plt.yticks([], [])
# add labels for legend
for layer_id, col in colortable.items():
plt.plot(0, 0, color=col, label=layer_id)
leg = plt.legend(bbox_to_anchor=(0., 1.08, 1, .102), frameon=False, ncol=8)
for line in leg.get_lines():
line.set_linewidth(8.0)
if show:
plt.show()
[docs]
def get_hash(self, **kwargs):
"""get_hash
Create an unique hash from all layer IDs in the correct order in the
structure as well as the corresponding material properties which are
given by the `kwargs`.
Args:
**kwargs (list[str]): types of requested properties..
Returns:
hash (str): unique hash.
"""
param = []
layers = self.get_unique_layers()
for layer in layers[1]:
param.append(layer.get_property_dict(**kwargs))
_, IDs, _ = self.get_layer_vectors()
param.append(IDs)
return make_hash_md5(param)
[docs]
def add_sub_structure(self, sub_structure, N=1):
"""add_sub_structure
Add a sub_structure of :math:`N` layers or sub-structures to the
structure.
Args:
sub_structure (AmorphousLayer, UnitCell, Structure):
amorphous layer, unit cell, or structure to add as sub-structure.
N (int): number or repetitions.
"""
# check of the sub_structure is an instance of the unitCell or
# structure class
if not isinstance(sub_structure, (AmorphousLayer, UnitCell, Structure)):
raise ValueError('Class '
+ type(sub_structure).__name__
+ ' is no possible sub structure. '
+ 'Only AmorphousLayer, UnitCell, and '
+ 'Structure classes are allowed!')
# if a structure is added as a sub_structure, the sub_structure
# can not have a substrate
if isinstance(sub_structure, Structure):
if sub_structure.substrate:
raise ValueError('No substrate in sub_structure allowed!')
# check the number of subsystems of the sub_structure
if ((self.num_sub_systems > 1)
and not (sub_structure.num_sub_systems == self.num_sub_systems)):
raise ValueError('The number of subsystems in each sub_structure'
'must be the same!')
else:
self.num_sub_systems = sub_structure.num_sub_systems
# add a sub_structure of N repetitions to the structure with
self.sub_structures.append([sub_structure, N])
[docs]
def add_substrate(self, sub_structure):
"""add_substrate
Add a structure as static substrate to the structure.
Args:
sub_structure (Structure): substrate structure.
"""
if not isinstance(sub_structure, Structure):
raise ValueError('Class '
+ type(sub_structure).__name__
+ ' is no possible substrate. '
+ 'Only structure class is allowed!')
self.substrate = sub_structure
[docs]
def get_number_of_sub_structures(self):
"""get_number_of_sub_structures
This methods does not return the number of all layers in the
structure, see :meth:`.get_number_of_layers`.
Returns:
N (int): number of all sub structures.
"""
N = 0
for i in range(len(self.sub_structures)):
if isinstance(self.sub_structures[i][0], (AmorphousLayer, UnitCell)):
N = N + 1
else:
N = N + self.sub_structures[i][0].get_number_of_sub_structures()
return N
[docs]
def get_number_of_layers(self):
"""get_number_of_layers
Determines the number of all layers in the structure.
Returns:
L (int): number of all layers in the structure.
"""
L = 0
# traverse the substructures
for i in range(len(self.sub_structures)):
if isinstance(self.sub_structures[i][0], AmorphousLayer) or \
isinstance(self.sub_structures[i][0], UnitCell):
L = L + self.sub_structures[i][1]
else:
# its a structure, so call the method recursively
L = L + self.sub_structures[i][0].get_number_of_layers() \
* self.sub_structures[i][1]
return L
[docs]
def get_number_of_unique_layers(self):
"""get_number_of_unique_layers
Determines the number of unique layers in the structure.
Returns:
N (int): number of unique layers in the structure.
"""
N = len(self.get_unique_layers()[0])
return N
[docs]
def get_thickness(self, units=True):
"""get_thickness
Determines the thickness of the structure.
Args:
units (boolean, optional): whether units should be returned or not.
Defaults to True.
Returns:
thickness (float, Quantity): the thickness from surface to bottom
of the structure.
"""
_, d_end, _ = self.get_distances_of_layers(units)
return d_end[-1]
[docs]
def get_unique_layers(self):
"""get_unique_layers
The uniqueness is determined by the handle of each layer instance.
Returns:
(tuple):
- *layer_ids (list[str])* - ids of all unique layers instances in
the structure.
- *layer_handles (list[AmorphousLayer, UnitCell, Structure])* -
handles of all unique layers instances in the structure.
"""
layer_ids = []
layer_handles = []
# traverse the sub_structures
for i in range(len(self.sub_structures)):
if isinstance(self.sub_structures[i][0], (AmorphousLayer)) or \
isinstance(self.sub_structures[i][0], (UnitCell)):
# its a AmorphousLayer or UnitCell
layer_id = self.sub_structures[i][0].id
if not layer_ids:
# the list is empty at the beginning so add
# the first layer
layer_ids = layer_ids + [layer_id]
layer_handles = layer_handles + [self.sub_structures[i][0]]
else:
# the list is not empty so check if the id is
# already in the layers id list
if layer_id not in layer_ids:
# if id not in list, so add it
layer_ids = layer_ids + [layer_id]
layer_handles = layer_handles + [self.sub_structures[i][0]]
else:
# its a sub_structure
if not layer_ids:
# the list is empty at the beginning so call
# the method recursively and add the result to the
# layers list
layer_ids = self.sub_structures[i][0].get_unique_layers()[0]
layer_handles = self.sub_structures[i][0].get_unique_layers()[1]
else:
# the list is not empty so check if the ids
# from the recursive call are already in the layers id
# list.
temp1 = self.sub_structures[i][0].get_unique_layers()[0]
temp2 = self.sub_structures[i][0].get_unique_layers()[1]
for j in range(len(temp1)):
# check all ids from recursive call
if temp1[j] not in layer_ids:
# ids not in list, so add them
layer_ids = layer_ids + [temp1[j]]
layer_handles = layer_handles + [temp2[j]]
return layer_ids, layer_handles
[docs]
def get_layer_vectors(self, *args):
"""get_layer_vectors
Returns three lists with the numeric index of all layers
in a structure given by the get_unique_layers() method and
additionally vectors with the ids and Handles of the
corresponding layer instances.
The list and order of the unique layers can be either handed
as an input parameter or is requested at the beginning.
Args:
layers (Optional[list]): list of unique layers including
ids and handles
Returns:
(tuple):
- *indices (list[int])* - numeric index of all layers in a
structure.
- *layer_ids (list[str])* - ids of all unique layers instances in
the structure.
- *layer_handles (list[AmorphousLayer, UnitCell, Structure])* -
handles of all unique layers instances in the structure.
"""
indices = []
layer_ids = []
layer_handles = []
# if no layers (unique layers) are given, we have to get them
if len(args) < 1:
layers = self.get_unique_layers()
else:
layers = args[0]
# traverse the substructres
for i in range(len(self.sub_structures)):
if isinstance(self.sub_structures[i][0], (AmorphousLayer, UnitCell)):
# its a AmorphousLayer or UnitCell
# find the index of the current layer id in the unique
# layer list
Index = layers[0].index(self.sub_structures[i][0].id)
# add the index N times to the indices vector
indices = np.append(indices, Index*np.ones(self.sub_structures[i][1]))
# create a list of N layer ids and add them to
# the ids list
temp1 = list(itertools.repeat(self.sub_structures[i][0].id,
self.sub_structures[i][1]))
layer_ids = layer_ids + list(temp1)
# create a list of N layer handles and add them to
# the Handles list
temp2 = list(itertools.repeat(self.sub_structures[i][0],
self.sub_structures[i][1]))
layer_handles = layer_handles + list(temp2)
else:
# its a structure
# make a recursive call and hand in the same unique
# layer vector as we used before
[temp1, temp2, temp3] = self.sub_structures[i][0].get_layer_vectors(layers)
temp11 = []
temp22 = []
temp33 = []
# concat the temporary arrays N times
for _ in range(self.sub_structures[i][1]):
temp11 = temp11 + list(temp1)
temp22 = temp22 + list(temp2)
temp33 = temp33 + list(temp3)
# add the temporary arrays to the outputs
indices = np.append(indices, temp11)
layer_ids = layer_ids + list(temp22)
layer_handles = layer_handles + list(temp33)
return indices, layer_ids, layer_handles
[docs]
def get_all_positions_per_unique_layer(self):
"""get_all_positions_per_unique_layer
Determines the position indices for each unique layer in the structure.
Returns:
pos (dict{ndarray[int]}): position indices for each unique layer in
the structure.
"""
layers = self.get_unique_layers()
indices = self.get_layer_vectors()[0]
pos = {} # Dictionary used instead of array
for i, layer in enumerate(layers[0]):
pos[layer] = np.flatnonzero(indices == i)
# Each element accessible through layer id
return pos
[docs]
def get_distances_of_layers(self, units=True):
"""get_distances_of_layers
Returns a vector of the distance from the surface for each layer
starting at 0 (dStart) and starting at the end of the first
layer (dEnd) and from the center of each layer (dMid).
Args:
units (boolean, optional): whether units should be returned or not.
Defaults to True.
Returns:
(tuple):
- *d_start (ndarray[float, Quantity])* - distances from the surface
of each layer starting at 0.
- *d_end (ndarray[float, Quantity])* - distances from the bottom
of each layer.
- *d_mid (ndarray[float, Quantity])*: distance from the middle of
each layer.
"""
thickness = self.get_layer_property_vector('_thickness')
d_end = np.cumsum(thickness)
d_start = np.hstack([[0], d_end[0:-1]])
d_mid = (d_start + thickness/2)
if units:
return Q_(d_start, u.m), Q_(d_end, u.m), Q_(d_mid, u.m)
else:
return d_start, d_end, d_mid
[docs]
def get_distances_of_interfaces(self, units=True):
"""get_distances_of_interfaces
Calculates the distances of the interafaces of the structure.
Args:
units (boolean, optional): whether units should be returned or not.
Defaults to True.
Returns:
res (ndarray[float, Quantity]): distances from the surface of each
interface of the structure.
"""
d_start, d_end, _ = self.get_distances_of_layers(False)
indices = np.r_[1, np.diff(self.get_layer_vectors()[0])]
res = np.append(d_start[np.nonzero(indices)], d_end[-1])
if units:
return Q_(res, u.m)
else:
return res
[docs]
def interp_distance_at_interfaces(self, N, units=True):
""" interp_distance_at_interfaces
Interpolates the distances at the layer interfaces by an odd number
:math:`N`.
Args:
N (int): number of point of interpolation at interface
units (boolean, optional): whether units should be returned or not.
Defaults to True.
Returns:
(tuple):
- *dist_interp (ndarray[float, Quantity])* - distance array of the
middle of each layer interpolated by an odd number :math:`N` at
the interfaces
- *original_indicies (ndarray[int])* - indicies of the original
distances in the interpolated array
"""
[d_start, d_end, d_mid] = self.get_distances_of_layers(False)
# these are the distances of the interfaces
dist_intf = self.get_distances_of_interfaces(False)
# start with the distances of the centers of the layers
dist_interp = d_mid
N = int(N) # make N an integer
if N % 2 == 0:
# odd numbers are required
N += 1
# traverse all distances
for z in dist_intf:
inda = finderb(z, d_start) # this is the index of a layer after the interface
indb = inda-1 # this is the index of a layer before the interface
# interpolate linearly N new distances at the interface
if indb == -1: # this is the surface interface
dist_interp = np.append(dist_interp, np.linspace(0, d_mid[inda], int(2+(N-1)/2)))
elif inda >= (len(d_mid)-1): # this is the bottom interface
dist_interp = np.append(dist_interp,
np.linspace(d_mid[inda], d_end[-1], int(2+(N-1)/2)))
else: # this is a surface inside the structure
dist_interp = np.append(dist_interp, np.linspace(d_mid[indb], d_mid[inda], 2+N))
dist_interp = np.unique(np.sort(dist_interp)) # sort and unify the distances
# these are the indicies of the original distances in the interpolated new array
original_indicies = finderb(d_mid, dist_interp)
if units:
return Q_(dist_interp, u.m), original_indicies
else:
return dist_interp, original_indicies
[docs]
def get_layer_property_vector(self, property_name):
"""get_layer_property_vector
Returns a vector for a property of all layers in the
structure. The property is determined by the propertyName and
returns a scalar value or a function handle.
Args:
property_name (str): name of property to return as array
Returns:
prop (ndarray[float, @lambda]): array of a property for all layers
in the structure.
"""
# get the Handle to all layers in the Structure
handles = self.get_layer_vectors()[2]
if callable(getattr(handles[0], property_name)):
# it's a function
prop = np.zeros([self.get_number_of_layers()])
for i in range(self.get_number_of_layers()):
prop[i] = getattr(handles[i], property_name)
elif ((type(getattr(handles[0], property_name)) is list) or
(type(getattr(handles[0], property_name)) is str) or
(type(getattr(handles[0], property_name)) is dict)):
# it's a list of functions or str
prop = []
for i in range(self.get_number_of_layers()):
# Prop = Prop + getattr(Handles[i],types)
prop.append(getattr(handles[i], property_name))
elif type(getattr(handles[0], property_name)) is Q_:
# its a pint quantity
unit = getattr(handles[0], property_name).units
prop = np.empty([self.get_number_of_layers()])
for i in range(self.get_number_of_layers()):
prop[i] = getattr(handles[i], property_name).magnitude
prop *= unit
else:
# its a number or array
layers = self.get_unique_layers()
temp = np.zeros([len(layers[0]), 1])
set_dtype = float
for i, layer in enumerate(layers[1]):
if isinstance(getattr(layer, property_name), complex):
set_dtype = complex
try:
temp[i] = len(getattr(layer, property_name))
except TypeError:
temp[i] = 1
max_dim = int(np.max(temp))
if max_dim > 1:
prop = np.empty([self.get_number_of_layers(), max_dim], dtype=set_dtype)
else:
prop = np.empty([self.get_number_of_layers()], dtype=set_dtype)
del temp
# traverse all layers
for i in range(self.get_number_of_layers()):
temp = getattr(handles[i], property_name)
if isinstance(temp, complex):
prop.dtype = complex
if max_dim > 1:
prop[i, :] = temp
else:
prop[i] = temp
return prop
[docs]
def get_layer_handle(self, i):
"""get_layer_handle
Returns the handle to a layer at a given position index.
Args:
i (int): index of the layer to return.
Returns:
handle (AmorphousLayer, UnitCell): handle to the layer at position
`i` in the structure.
"""
handles = self.get_layer_vectors()[2]
return handles[i]
[docs]
def reverse(self):
"""reverse
Returns a reversed structure also reversing all nested sub_structure.
Returns:
reversed (Structure): reversed structure.
"""
from copy import deepcopy
reversed = deepcopy(self)
# need to handle superstrate and substrate
return self.reverse_sub_structures(reversed)
[docs]
def reverse_sub_structures(self, structure):
"""reverse_sub_structures
Reverse a `Structure` and recursively call itself if a
sub_structure is a `Structure` itself.
Args:
structure (Structure): structure to be reversed.
Returns:
structure (Structure): reversed structure.
"""
# reverse the list of sub_structures
structure.sub_structures.reverse()
for (sub_structure, N) in structure.sub_structures:
if isinstance(sub_structure, Structure):
# recursive call
self.reverse_sub_structures(sub_structure)
else:
pass
return structure