"""
Implementation of the simplified model language for the ORSO header.
It includes parsing of models from header or different input information and
resolving the model to a simple list of slabs.
"""
import warnings
from dataclasses import dataclass
from typing import Dict, List, Optional, Union
from ..utils.chemical_formula import Formula
from . import model_complex
from .base import Header, Literal
from .model_building_blocks import (DENSITY_RESOLVERS, SPECIAL_MATERIALS, Composit, Layer, Material, ModelParameters,
SubStackType)
[docs]
def find_idx(string, start, value):
res = string[start:].find(value)
if res >= 0:
next_idx = start + res
else:
next_idx = len(string)
return next_idx
[docs]
def find_closing(string, start):
open_brackets = 1
idx = start
while idx < len(string):
if string[idx] == "(":
open_brackets += 1
if string[idx] == ")":
open_brackets -= 1
if open_brackets == 0:
return idx
idx += 1
return -1
[docs]
@dataclass
class SubStack(Header, SubStackType):
repetitions: int = 1
stack: Optional[str] = None
sequence: Optional[List[Layer]] = None
sub_stack_class: Optional[Literal["SubStack"]] = "SubStack"
environment: Optional[Union[str, Material, Composit]] = None
original_name = None
[docs]
def resolve_names(self, resolvable_items):
if isinstance(self.environment, str):
env_orig = self.environment
if self.environment in resolvable_items:
self.environment = resolvable_items[self.environment]
elif self.environment in SPECIAL_MATERIALS:
self.environment = SPECIAL_MATERIALS[self.environment]
else:
self.environment = Material(formula=self.environment)
self.environment.original_name = env_orig
if self.environment is not None:
resolvable_items = {"environment": self.environment, **resolvable_items}
if self.stack is None and self.sequence is None:
raise ValueError("SubStack has to either define stack or sequence")
if self.sequence is None:
stack = self.stack
output = []
idx = 0
while idx < len(stack):
next_idx = find_idx(stack, idx, "|")
if "(" in stack[idx:next_idx]:
close_idx = find_closing(stack, find_idx(stack, idx, "(") + 1)
next_idx = find_idx(stack, close_idx, "|")
rep, sub_stack = stack[idx:close_idx].split("(", 1)
if rep.strip() == "":
rep = 1
else:
rep = int(rep)
rest = stack[close_idx + 1 : next_idx]
if rest.strip().startswith("in "):
# the Stack has elements within a matrix material
environment = rest.strip()[3:]
else:
# if there is a higher level envirnment, it is kept if not overwritten
environment = self.environment
obj = SubStack(repetitions=rep, stack=sub_stack.strip(), environment=environment)
else:
items = stack[idx:next_idx].strip().rsplit(None, 1)
item = items[0].strip()
if len(items) == 2:
try:
thickness = float(items[1])
except ValueError:
# it can't be interpreted as number, assume name has space
thickness = 0.0
item = stack[idx:next_idx].strip()
else:
thickness = 0.0
if item in resolvable_items:
obj = resolvable_items[item]
if isinstance(obj, SubStackType):
# create a copy of the object to allow different environments for same key
obj = obj.__class__.from_dict(obj.to_dict())
if isinstance(obj, Material) or isinstance(obj, Composit):
obj = Layer(material=obj, thickness=thickness)
elif getattr(obj, "thickness", "ignore") is None:
obj.thickness = thickness
else:
try:
Formula(item, strict=True)
except ValueError:
# try to resolve name directly with databse
res = None
for resolver in DENSITY_RESOLVERS:
res = resolver.resolve_item(item)
if res is not None:
break
if res is None:
# assume name is a Formula to resolve within Layer
obj = Layer(material=item, thickness=thickness)
obj.original_name = item
else:
if "material" in res:
obj = Layer.from_dict(res)
elif "composition" in res:
obj = Layer(material=Composit.from_dict(res), thickness=thickness)
elif "formula" in res or "sld" in res:
obj = Layer(material=Material.from_dict(res), thickness=thickness)
else:
obj = Layer(material=item, thickness=thickness)
obj.original_name = item
if getattr(obj, "thickness", "ignore") is None:
obj.thickness = thickness
else:
obj = Layer(material=item, thickness=thickness)
obj.original_name = item
if hasattr(obj, "resolve_names"):
obj.resolve_names(resolvable_items)
output.append(obj)
idx = next_idx + 1
self.sequence = output
else:
for li in self.sequence:
li.resolve_names(resolvable_items)
[docs]
def resolve_defaults(self, defaults: ModelParameters):
for li in self.sequence:
if hasattr(li, "resolve_defaults"):
li.resolve_defaults(defaults)
if self.environment is not None:
self.environment.resolve_defaults(defaults)
[docs]
def resolve_to_blocks(self) -> List[Union[Layer, SubStackType]]:
# like resovle_to_layers but keeping SubStackType classes in tact
blocks = list(self.sequence)
added = 0
for i in range(len(blocks)):
if isinstance(blocks[i + added], Layer):
if blocks[i + added].material is None:
# TODO: verify this case actually exists
blocks[i + added].generate_material()
blocks[i + added].material.generate_density()
else:
obj = blocks.pop(i + added)
sub_blocks = obj.resolve_to_blocks()
blocks = blocks[: i + added] + sub_blocks + blocks[i + added :]
added += len(sub_blocks) - 1
return blocks * self.repetitions
[docs]
def resolve_to_layers(self) -> List[Layer]:
layers = list(self.sequence)
added = 0
for i in range(len(layers)):
if isinstance(layers[i + added], Layer):
if layers[i + added].material is None:
layers[i + added].generate_material()
layers[i + added].material.generate_density()
else:
obj = layers.pop(i + added)
sub_layers = obj.resolve_to_layers()
layers = layers[: i + added] + sub_layers + layers[i + added :]
added += len(sub_layers) - 1
return layers * self.repetitions
SUBSTACK_TYPE = SubStack
for T in SubStackType.__subclasses__():
SUBSTACK_TYPE = Union[SUBSTACK_TYPE, T]
[docs]
@dataclass
class ItemChanger(Header):
"""
Allows to define a simple change in SubStackType item by
just updating a selected set of parameters.
"""
like: str
but: dict
original_name = None
[docs]
@dataclass
class SampleModel(Header):
stack: str
origin: Optional[str] = None
sub_stacks: Optional[Dict[str, Union[ItemChanger, SUBSTACK_TYPE]]] = None
layers: Optional[Dict[str, Layer]] = None
materials: Optional[Dict[str, Material]] = None
composits: Optional[Dict[str, Composit]] = None
globals: Optional[ModelParameters] = None
reference: Optional[str] = None
def __post_init__(self):
super().__post_init__()
names = []
for di in [self.sub_stacks, self.layers, self.materials, self.composits]:
if di is None:
continue
for ni in di.keys():
if ni in names:
warnings.warn(f'Duplicate name "{ni}" in SampleModel definition')
names.append(ni)
@property
def resolvable_items(self):
output = {}
if self.sub_stacks:
for key, ssi in self.sub_stacks.items():
if isinstance(ssi, ItemChanger):
ssi_ref = self.sub_stacks[ssi.like]
ssi_ref_data = ssi_ref.to_dict()
ssi_ref_data.update(ssi.but)
ssi = ssi_ref.__class__.from_dict(ssi_ref_data)
self.sub_stacks[key] = ssi
ssi.original_name = key
output.update(self.sub_stacks)
if self.layers:
for key, li in self.layers.items():
li.original_name = key
output.update(self.layers)
if self.materials:
for key, mi in self.materials.items():
mi.original_name = key
output.update(self.materials)
if self.composits:
for key, ci in self.composits.items():
ci.original_name = key
output.update(self.composits)
return output
[docs]
def resolve_stack(self):
if self.globals is None:
defaults = ModelParameters()
else:
defaults = self.globals
stack = self.stack
ri = self.resolvable_items
output = []
idx = 0
while idx < len(stack):
next_idx = find_idx(stack, idx, "|")
if "(" in stack[idx:next_idx]:
close_idx = find_closing(stack, find_idx(stack, idx, "(") + 1)
next_idx = find_idx(stack, close_idx, "|")
rep, sub_stack = stack[idx:close_idx].split("(", 1)
if rep.strip() == "":
rep = 1
else:
rep = int(rep)
rest = stack[close_idx + 1 : next_idx]
if rest.strip().startswith("in "):
# the Stack has elements within a matrix material
environment = rest.strip()[3:]
else:
environment = None
obj = SubStack(repetitions=rep, stack=sub_stack.strip(), environment=environment)
else:
items = stack[idx:next_idx].strip().rsplit(None, 1)
item = items[0].strip()
if len(items) == 2:
try:
thickness = float(items[1])
except ValueError:
# if can't be interpreted as umber, assume name has space
thickness = 0.0
item = stack[idx:next_idx].strip()
else:
thickness = 0.0
if item in ri:
obj = ri[item]
if isinstance(obj, Material) or isinstance(obj, Composit):
obj = Layer(material=obj, thickness=thickness)
elif getattr(obj, "thickness", "ignore") is None:
obj.thickness = thickness
else:
try:
Formula(item, strict=True)
except ValueError:
# try to resolve name directly with databse
res = None
if len(DENSITY_RESOLVERS) == 0:
from ..utils.resolver_slddb import ResolverSLDDB
DENSITY_RESOLVERS.append(ResolverSLDDB())
for resolver in DENSITY_RESOLVERS:
res = resolver.resolve_item(item)
if res is not None:
break
if res is None:
# assume name is a Formula to resolve within Layer
obj = Layer(material=item, thickness=thickness)
obj.original_name = item
else:
if "material" in res:
obj = Layer.from_dict(res)
elif "composition" in res:
obj = Layer(material=Composit.from_dict(res), thickness=thickness)
elif "formula" in res or "sld" in res:
obj = Layer(material=Material.from_dict(res), thickness=thickness)
else:
obj = Layer(material=item, thickness=thickness)
obj.original_name = item
if getattr(obj, "thickness", "ignore") is None:
obj.thickness = thickness
else:
obj = Layer(material=item, thickness=thickness)
obj.original_name = item
if hasattr(obj, "resolve_names"):
obj.resolve_names(ri)
if hasattr(obj, "resolve_defaults"):
obj.resolve_defaults(defaults)
output.append(obj)
idx = next_idx + 1
return output
[docs]
def resolve_to_blocks(self) -> List[Union[Layer, SubStackType]]:
# like resovle_to_layers but keeping SubStackType classes intact
blocks = self.resolve_stack()
added = 0
for i in range(len(blocks)):
if isinstance(blocks[i + added], Layer):
if blocks[i + added].material is None:
blocks[i + added].generate_material()
blocks[i + added].material.generate_density()
else:
obj = blocks.pop(i + added)
sub_blocks = obj.resolve_to_blocks()
blocks = blocks[: i + added] + sub_blocks + blocks[i + added :]
added += len(sub_blocks) - 1
return blocks
[docs]
def resolve_to_layers(self) -> List[Layer]:
layers = self.resolve_stack()
added = 0
for i in range(len(layers)):
if isinstance(layers[i + added], Layer):
if layers[i + added].material is None:
layers[i + added].generate_material()
layers[i + added].material.generate_density()
else:
obj = layers.pop(i + added)
sub_layers = obj.resolve_to_layers()
layers = layers[: i + added] + sub_layers + layers[i + added :]
added += len(sub_layers) - 1
return layers