from typing import Optional
from typing import Union
import numpy as np
from easyscience import global_object
from easyscience.Constraints import FunctionalConstraint
from easyscience.Objects.new_variable import Parameter
from easyreflectometry.parameter_utils import get_as_parameter
from easyreflectometry.special.calculations import area_per_molecule_to_scattering_length_density
from easyreflectometry.special.calculations import neutron_scattering_length
from ..materials.material import Material
from ..materials.material_solvated import DEFAULTS as MATERIAL_SOLVATED_DEFAULTS
from ..materials.material_solvated import MaterialSolvated
from .layer import DEFAULTS as LAYER_DEFAULTS
from .layer import Layer
DEFAULTS = {
'molecular_formula': 'C10H18NO8P',
'area_per_molecule': {
'description': 'Surface coverage',
'value': 48.2,
'unit': 'angstrom^2',
'min': 0,
'max': np.inf,
'fixed': True,
},
'sl': {
'description': 'The real scattering length for a molecule formula in angstrom.',
'url': 'https://www.ncnr.nist.gov/resources/activation/',
'value': 4.186,
'unit': 'angstrom',
'min': -np.Inf,
'max': np.Inf,
'fixed': True,
},
'isl': {
'description': 'The real scattering length for a molecule formula in angstrom.',
'url': 'https://www.ncnr.nist.gov/resources/activation/',
'value': 0.0,
'unit': 'angstrom',
'min': -np.Inf,
'max': np.Inf,
'fixed': True,
},
}
DEFAULTS.update(MATERIAL_SOLVATED_DEFAULTS)
DEFAULTS.update(LAYER_DEFAULTS)
[docs]
class LayerAreaPerMolecule(Layer):
"""The `LayerAreaPerMolecule` class allows a layer to be defined in terms of some
molecular formula an area per molecule, and a solvent.
"""
# Added in __init__
#: Real part of the scattering length.
_scattering_length_real: Parameter
#: Imaginary part of the scattering length.
_scattering_length_imag: Parameter
#: Area per molecule in the layer in Anstrom^2.
_area_per_molecule: Parameter
# Other typer than in __init__.super()
material: MaterialSolvated
[docs]
def __init__(
self,
molecular_formula: Union[str, None] = None,
thickness: Union[Parameter, float, None] = None,
solvent: Union[Material, None] = None,
solvent_fraction: Union[Parameter, float, None] = None,
area_per_molecule: Union[Parameter, float, None] = None,
roughness: Union[Parameter, float, None] = None,
name: str = 'EasyLayerAreaPerMolecule',
unique_name: Optional[str] = None,
interface=None,
):
"""Constructor.
:param molecular_formula: Formula for the molecule in the layer.
:param thickness: Layer thickness in Angstrom.
:param solvent: Solvent containing the molecule.
:param solvent_fraction: Fraction of solvent in layer. Fx solvation or surface coverage.
:param area_per_molecule: Area per molecule in the layer
:param roughness: Upper roughness on the layer in Angstrom.
:param name: Name of the layer, defaults to "EasyLayerAreaPerMolecule"
:param interface: Interface object, defaults to `None`
"""
if solvent is None:
solvent = Material(6.36, 0, 'D2O', interface=interface)
if unique_name is None:
unique_name = global_object.generate_unique_name(self.__class__.__name__)
# Create the solvated molecule and corresponding constraints
if molecular_formula is None:
molecular_formula = DEFAULTS['molecular_formula']
molecule_material = Material(
sld=0.0,
isld=0.0,
name=molecular_formula,
interface=interface,
unique_name=unique_name + 'Material',
)
thickness = get_as_parameter(
name='thickness',
value=thickness,
default_dict=DEFAULTS,
unique_name_prefix=f'{unique_name}_Thickness',
)
_area_per_molecule = get_as_parameter(
name='area_per_molecule',
value=area_per_molecule,
default_dict=DEFAULTS,
unique_name_prefix=f'{unique_name}_AreaPerMolecule',
)
_scattering_length_real = get_as_parameter(
name='scattering_length_real',
value=0.0,
default_dict=DEFAULTS['sl'],
unique_name_prefix=f'{unique_name}_Sl',
)
_scattering_length_imag = get_as_parameter(
name='scattering_length_imag',
value=0.0,
default_dict=DEFAULTS['isl'],
unique_name_prefix=f'{unique_name}_Isl',
)
# Constrain the real part of the sld value for the molecule
constraint_sld_real = FunctionalConstraint(
dependent_obj=molecule_material.sld,
func=area_per_molecule_to_scattering_length_density,
independent_objs=[_scattering_length_real, thickness, _area_per_molecule],
)
thickness.user_constraints['area_per_molecule'] = constraint_sld_real
_area_per_molecule.user_constraints['area_per_molecule'] = constraint_sld_real
_scattering_length_real.user_constraints['area_per_molecule'] = constraint_sld_real
# Constrain the imaginary part of the sld value for the molecule
constraint_sld_imag = FunctionalConstraint(
dependent_obj=molecule_material.isld,
func=area_per_molecule_to_scattering_length_density,
independent_objs=[_scattering_length_imag, thickness, _area_per_molecule],
)
thickness.user_constraints['iarea_per_molecule'] = constraint_sld_imag
_area_per_molecule.user_constraints['iarea_per_molecule'] = constraint_sld_imag
_scattering_length_imag.user_constraints['iarea_per_molecule'] = constraint_sld_imag
solvated_molecule_material = MaterialSolvated(
material=molecule_material,
solvent=solvent,
solvent_fraction=solvent_fraction,
interface=interface,
unique_name=unique_name + 'MaterialSolvated',
)
super().__init__(
material=solvated_molecule_material,
thickness=thickness,
roughness=roughness,
name=name,
interface=interface,
)
self._add_component('_scattering_length_real', _scattering_length_real)
self._add_component('_scattering_length_imag', _scattering_length_imag)
self._add_component('_area_per_molecule', _area_per_molecule)
scattering_length = neutron_scattering_length(molecular_formula)
self._scattering_length_real.value = scattering_length.real
self._scattering_length_imag.value = scattering_length.imag
self._molecular_formula = molecular_formula
self.interface = interface
@property
def area_per_molecule_parameter(self) -> Parameter:
"""Get the parameter for area per molecule."""
return self._area_per_molecule
@property
def area_per_molecule(self) -> float:
"""Get the area per molecule."""
return self._area_per_molecule.value
@area_per_molecule.setter
def area_per_molecule(self, new_area_per_molecule: float) -> None:
"""Set the area per molecule.
:param new_area_per_molecule: New area per molecule.
"""
if new_area_per_molecule < 0:
raise ValueError('new_area_per_molecule must be greater than 0.0.')
self._area_per_molecule.value = new_area_per_molecule
@property
def molecule(self) -> Material:
"""Get the molecule material."""
return self.material.material
@property
def solvent(self) -> Material:
"""Get the solvent material."""
return self.material.solvent
@solvent.setter
def solvent(self, new_solvent: Material) -> None:
"""Set the solvent material.
:param new_solvent: New solvent material.
"""
self.material.solvent = new_solvent
@property
def solvent_fraction_parameter(self) -> float:
"""Get parameter for the fraction of the layer occupied by the solvent."""
return self.material.solvent_fraction_parameter
@property
def solvent_fraction(self) -> float:
"""Get the fraction of the layer occupied by the solvent.
This could be a result of either water solvating the molecule, or incomplete surface coverage of the molecules.
"""
return self.material.solvent_fraction
@solvent_fraction.setter
def solvent_fraction(self, solvent_fraction: float) -> None:
"""Set the fraction of the layer occupied by the solvent.
This could be a result of either water solvating the molecule, or incomplete surface coverage of the molecules.
:param solvent_fraction: Fraction of layer described by the solvent.
"""
self.material.solvent_fraction = solvent_fraction
@property
def molecular_formula(self) -> str:
"""Get the formula of molecule the layer."""
return self._molecular_formula
@molecular_formula.setter
def molecular_formula(self, formula_string: str) -> None:
"""Set the formula of the molecule in the material.
:param formula_string: String that defines the molecular formula.
"""
self._molecular_formula = formula_string
scattering_length = neutron_scattering_length(formula_string)
# The molecule is also being updated through the constraints
self._scattering_length_real.value = scattering_length.real
self._scattering_length_imag.value = scattering_length.imag
self.molecule.name = formula_string
self.material._update_name()
@property
def _dict_repr(self) -> dict[str, str]:
"""Dictionary representation of the `area_per_molecule` object. Produces a simple dictionary"""
dict_repr = super()._dict_repr
dict_repr['molecular_formula'] = self._molecular_formula
dict_repr['area_per_molecule'] = f'{self.area_per_molecule:.2f} ' f'{self._area_per_molecule.unit}'
return dict_repr
[docs]
def as_dict(self, skip: Optional[list[str]] = None) -> dict[str, str]:
"""Produces a cleaned dict using a custom as_dict method to skip necessary things.
The resulting dict matches the parameters in __init__
:param skip: List of keys to skip, defaults to `None`.
"""
this_dict = super().as_dict(skip=skip)
this_dict['solvent_fraction'] = self.material._fraction.as_dict(skip=skip)
this_dict['area_per_molecule'] = self._area_per_molecule.as_dict(skip=skip)
this_dict['solvent'] = self.solvent.as_dict(skip=skip)
del this_dict['material']
del this_dict['_scattering_length_real']
del this_dict['_scattering_length_imag']
return this_dict