Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions RATapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""RATapi is a Python package for modelling, fitting and optimising reflectivity problems."""

import RATapi.examples as examples
from RATapi import events, models
from RATapi.classlist import ClassList
Expand Down
84 changes: 45 additions & 39 deletions RATapi/classlist.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""The classlist module. Contains the ClassList class, which defines a list containing instances of a particular
class.
"""
"""The ClassList class, which defines a list containing instances of a particular class."""

import collections
import contextlib
Expand All @@ -24,12 +22,12 @@ class ClassList(collections.UserList, Generic[T]):
attribute given in the ClassList's "name_field" attribute (the default is "name"), the ClassList will ensure that
all objects within the ClassList have unique values for that attribute. It is then possible to use this attribute
of an object in the .remove(), .count(), and .index() routines in place of the full object. Due to the requirement
of unique values of the name_field attribute, the multiplication operators __mul__, __rmul__, and __imul__ have
of unique values of the ``name_field`` attribute, the multiplication operators __mul__, __rmul__, and __imul__ have
been disabled, since they cannot allow for unique attribute values by definition.

We extend the UserList class to enable objects to be added and modified using just the keyword arguments, enable
the object name_field attribute to be used in place of the full object, and ensure all elements are of the
specified type, with unique name_field attributes defined.
the object ``name_field`` attribute to be used in place of the full object, and ensure all elements are of the
specified type, with unique ``name_field`` attributes defined.

Parameters
----------
Expand Down Expand Up @@ -171,8 +169,9 @@ def __imul__(self, n: int) -> None:
raise TypeError(f"unsupported operand type(s) for *=: '{self.__class__.__name__}' and '{n.__class__.__name__}'")

def append(self, obj: T = None, **kwargs) -> None:
"""Append a new object to the ClassList using either the object itself, or keyword arguments to set attribute
values.
"""Append a new object to the ClassList.

This method can use the object itself, or can provide attribute values as keyword arguments for a new object.

Parameters
----------
Expand All @@ -184,7 +183,7 @@ def append(self, obj: T = None, **kwargs) -> None:
Raises
------
ValueError
Raised if the input arguments contain a name_field value already defined in the ClassList.
Raised if the input arguments contain a ``name_field`` value already defined in the ClassList.

Warnings
--------
Expand Down Expand Up @@ -216,8 +215,9 @@ def append(self, obj: T = None, **kwargs) -> None:
self.data.append(self._class_handle(**kwargs))

def insert(self, index: int, obj: T = None, **kwargs) -> None:
"""Insert a new object into the ClassList at a given index using either the object itself, or keyword arguments
to set attribute values.
"""Insert a new object at a given index.

This method can use the object itself, or can provide attribute values as keyword arguments for a new object.

Parameters
----------
Expand All @@ -231,7 +231,7 @@ def insert(self, index: int, obj: T = None, **kwargs) -> None:
Raises
------
ValueError
Raised if the input arguments contain a name_field value already defined in the ClassList.
Raised if the input arguments contain a ``name_field`` value already defined in the ClassList.

Warnings
--------
Expand Down Expand Up @@ -263,20 +263,25 @@ def insert(self, index: int, obj: T = None, **kwargs) -> None:
self.data.insert(index, self._class_handle(**kwargs))

def remove(self, item: Union[T, str]) -> None:
"""Remove an object from the ClassList using either the object itself or its name_field value."""
"""Remove an object from the ClassList using either the object itself or its ``name_field`` value."""
item = self._get_item_from_name_field(item)
self.data.remove(item)

def count(self, item: Union[T, str]) -> int:
"""Return the number of times an object appears in the ClassList using either the object itself or its
name_field value.
"""Return the number of times an object appears in the ClassList.

This method can use either the object itself or its ``name_field`` value.

"""
item = self._get_item_from_name_field(item)
return self.data.count(item)

def index(self, item: Union[T, str], offset: bool = False, *args) -> int:
"""Return the index of a particular object in the ClassList using either the object itself or its
name_field value. If offset is specified, add one to the index. This is used to account for one-based indexing.
"""Return the index of a particular object in the ClassList.

This method can use either the object itself or its ``name_field`` value.
If offset is specified, add one to the index. This is used to account for one-based indexing.

"""
item = self._get_item_from_name_field(item)
return self.data.index(item, *args) + int(offset)
Expand Down Expand Up @@ -357,12 +362,12 @@ def __exit__(self, exctype, excinst, exctb):
self._class_handle.model_validate(self.data[index])

def get_names(self) -> list[str]:
"""Return a list of the values of the name_field attribute of each class object in the list.
"""Return a list of the values of the ``name_field`` attribute of each class object in the list.

Returns
-------
names : list [str]
The value of the name_field attribute of each object in the ClassList.
The value of the ``name_field`` attribute of each object in the ClassList.

"""
return [getattr(model, self.name_field) for model in self.data if hasattr(model, self.name_field)]
Expand All @@ -389,8 +394,7 @@ def get_all_matches(self, value: Any) -> list[tuple]:
]

def _validate_name_field(self, input_args: dict[str, Any]) -> None:
"""Raise a ValueError if the name_field attribute is passed as an object parameter, and its value is already
used within the ClassList.
"""Raise a ValueError if the user tries to add an object with a ``name_field`` already in the ClassList.

Parameters
----------
Expand All @@ -400,7 +404,7 @@ def _validate_name_field(self, input_args: dict[str, Any]) -> None:
Raises
------
ValueError
Raised if the input arguments contain a name_field value already defined in the ClassList.
Raised if the input arguments contain a ``name_field`` value already defined in the ClassList.

"""
names = [name.lower() for name in self.get_names()]
Expand All @@ -413,8 +417,7 @@ def _validate_name_field(self, input_args: dict[str, Any]) -> None:
)

def _check_unique_name_fields(self, input_list: Sequence[T]) -> None:
"""Raise a ValueError if any value of the name_field attribute is used more than once in a list of class
objects.
"""Raise a ValueError if any value of the ``name_field`` attribute is repeated in a list of class objects.

Parameters
----------
Expand Down Expand Up @@ -470,17 +473,17 @@ def _check_unique_name_fields(self, input_list: Sequence[T]) -> None:
)

def _check_classes(self, input_list: Sequence[T]) -> None:
"""Raise a ValueError if any object in a list of objects is not of the type specified by self._class_handle.
"""Raise a ValueError if any object in a list of objects is not of the type specified by ``self._class_handle``.

Parameters
----------
input_list : iterable
A list of instances of the class given in self._class_handle.
A list of instances of the class given in ``self._class_handle``.

Raises
------
ValueError
Raised if the input list contains objects of any type other than that given in self._class_handle.
If the input list contains objects of any type other than that given in ``self._class_handle``.

"""
error_list = []
Expand All @@ -495,18 +498,18 @@ def _check_classes(self, input_list: Sequence[T]) -> None:
)

def _get_item_from_name_field(self, value: Union[T, str]) -> Union[T, str]:
"""Return the object with the given value of the name_field attribute in the ClassList.
"""Return the object with the given value of the ``name_field`` attribute in the ClassList.

Parameters
----------
value : T or str
Either an object in the ClassList, or the value of the name_field attribute of an object in the ClassList.
Either an object in the ClassList, or the value of the ``name_field`` for an object in the ClassList.

Returns
-------
instance : T or str
Either the object with the value of the name_field attribute given by value, or the input value if an
object with that value of the name_field attribute cannot be found.
Either the object with the value of the ``name_field`` attribute given by value, or the input value if an
object with that value of the ``name_field`` attribute cannot be found.

"""
try:
Expand All @@ -518,24 +521,27 @@ def _get_item_from_name_field(self, value: Union[T, str]) -> Union[T, str]:

@staticmethod
def _determine_class_handle(input_list: Sequence[T]):
"""When inputting a sequence of object to a ClassList, the _class_handle should be set as the type of the
element which satisfies "issubclass" for all the other elements.
"""Determine the class handle from a sequence of objects.

The ``_class_handle`` of the sequence is the type of the first element in the sequence
which is a subclass of all elements in the sequence. If no such element exists, the handle
is set to be the type of the first element in the list.

Parameters
----------
input_list : Sequence [object]
input_list : Sequence[T]
A list of instances to populate the ClassList.

Returns
-------
class_handle : type
The type object of the element fulfilling the condition of satisfying "issubclass" for all of the other
elements.
The type object of the first element which is a subclass of all of the other
elements, or the first element if no such element exists.

"""
for this_element in input_list:
if all([issubclass(type(instance), type(this_element)) for instance in input_list]):
class_handle = type(this_element)
for element in input_list:
if all(issubclass(type(instance), type(element)) for instance in input_list):
class_handle = type(element)
break
else:
class_handle = type(input_list[0])
Expand Down
7 changes: 5 additions & 2 deletions RATapi/controls.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""The Controls class for providing RAT algorithm settings."""

import contextlib
import os
import tempfile
Expand Down Expand Up @@ -196,19 +198,20 @@ def __str__(self) -> str:
return table.get_string()

def initialise_IPC(self):
"""Setup the inter-process communication file."""
"""Set up the inter-process communication file."""
IPC_obj, self._IPCFilePath = tempfile.mkstemp()
os.write(IPC_obj, b"0")
os.close(IPC_obj)
return None

def sendStopEvent(self):
"""Sends the stop event via the inter-process communication file.
"""Send the stop event via the inter-process communication file.

Warnings
--------
UserWarning
Raised if we try to delete an IPC file that was not initialised.

"""
if os.path.isfile(self._IPCFilePath):
with open(self._IPCFilePath, "wb") as f:
Expand Down
17 changes: 10 additions & 7 deletions RATapi/events.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
"""Hooks for connecting to run callback events."""

import os
from typing import Callable, Union

from RATapi.rat_core import EventBridge, EventTypes, PlotEventData, ProgressEventData


def notify(event_type: EventTypes, data: Union[str, PlotEventData, ProgressEventData]) -> None:
"""Calls registered callbacks with the data when event type has
been triggered.
"""Call registered callbacks with data when event type has been triggered.

Parameters
----------
Expand All @@ -22,7 +23,7 @@ def notify(event_type: EventTypes, data: Union[str, PlotEventData, ProgressEvent


def get_event_callback(event_type: EventTypes) -> list[Callable[[Union[str, PlotEventData, ProgressEventData]], None]]:
"""Returns all callbacks registered for the given event type.
"""Return all callbacks registered for the given event type.

Parameters
----------
Expand All @@ -39,7 +40,7 @@ def get_event_callback(event_type: EventTypes) -> list[Callable[[Union[str, Plot


def register(event_type: EventTypes, callback: Callable[[Union[str, PlotEventData, ProgressEventData]], None]) -> None:
"""Registers a new callback for the event type.
"""Register a new callback for the event type.

Parameters
----------
Expand All @@ -58,12 +59,14 @@ def register(event_type: EventTypes, callback: Callable[[Union[str, PlotEventDat


def clear(key=None, callback=None) -> None:
"""Clears all event callbacks or specific callback.
"""Clear all event callbacks or specific callback.

Parameters
----------
callback : Callable[[Union[str, PlotEventData, ProgressEventData]], None]
The callback for when the event is triggered.
key : EventTypes, optional
The event type of the callback to clear if given.
callback : Callable[[Union[str, PlotEventData, ProgressEventData]], None], optional
A callback for an event which will be cleared if given.

"""
if key is None and callback is None:
Expand Down
2 changes: 2 additions & 0 deletions RATapi/examples/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Usage examples for the Python RAT API."""

from RATapi.examples.absorption.absorption import absorption
from RATapi.examples.convert_rascal_project.convert_rascal import convert_rascal
from RATapi.examples.domains.domains_custom_layers import domains_custom_layers
Expand Down
1 change: 1 addition & 0 deletions RATapi/examples/absorption/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""An example of using absorption in a RAT project."""
14 changes: 13 additions & 1 deletion RATapi/examples/absorption/absorption.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""An example for using absorption in RAT."""

import pathlib

import numpy as np
Expand All @@ -6,7 +8,17 @@


def absorption():
"""Custom layers model including absorption"""
"""Run a custom layers model including absorption.

RAT allows the use of an imaginary, as well as real part of the SLD.
The effect of this is usually seen below the critical edge, and must sometimes be accounted for.

This is an example of a Custom Layers project using absorption. used here is Custom Layers.
It analyses a bilayer sample on a permalloy / gold substrate,
measured using polarised neutrons, against D2O and H2O, leading to 4 contrasts in total.
Absorption (i.e. imaginary SLD) is defined for Gold and the Permalloy,
to account for non-flat data below the critical edge.
"""
problem = RAT.Project(
name="Absorption example",
calculation="normal",
Expand Down
3 changes: 3 additions & 0 deletions RATapi/examples/absorption/volume_thiol_bilayer.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
"""Custom layer model file for the absorption example."""


def volume_thiol_bilayer(params, bulk_in, bulk_out, contrast):
"""VolumeThiolBilayer RAT Custom Layer Model File.

Expand Down
7 changes: 6 additions & 1 deletion RATapi/examples/bayes_benchmark/bayes_benchmark.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""
"""An example script to compare different methods of Bayesian fitting.

This example compares three Bayesian posteriors for a low-dimensional
example: a posterior generated by DREAM, one generated by NS, and
one calculated directly.
Expand Down Expand Up @@ -34,6 +35,7 @@
# this is the RasCAL-1 default project
# it is a bare D2O substrate
def get_project() -> RAT.Project:
"""Create the project used as our example."""
return RAT.Project(
name="Bare D2O Substrate",
calculation="normal",
Expand Down Expand Up @@ -191,6 +193,7 @@ def calculate_posterior(roughness_index: int, background_index: int) -> float:
-------
float
The value of exp(-chi^2 / 2) for the given roughness and background values.

"""
problem.parameters[0].value = roughness[roughness_index]
problem.background_parameters[0].value = background[background_index]
Expand Down Expand Up @@ -265,6 +268,7 @@ def calculate_posterior(roughness_index: int, background_index: int, scalefactor
-------
float
The value of exp(-chi^2 / 2) for the given roughness and background values.

"""
problem.parameters[0].value = roughness[roughness_index]
problem.background_parameters[0].value = background[background_index]
Expand Down Expand Up @@ -300,6 +304,7 @@ def plot_posterior_comparison(
The BayesResults object from a DREAM calculation.
calc_results : CalculationResults
The results from a direct calculation.

"""
num_params = calc_results.distribution.ndim
fig, axes = plt.subplots(3, num_params, figsize=(3 * num_params, 9))
Expand Down
3 changes: 3 additions & 0 deletions RATapi/examples/convert_rascal_project/Model_IIb.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
"""A custom model file for a monolayer volume model."""

from math import cos, radians


def Model_IIb(params, bulk_in, bulk_out, contrast):
"""Calculate layer parameters for a monolayer volume model at two deuterations."""
# converted from matlab file Model_IIb.m

Roughness, APM, thickHead, theta = params
Expand Down
1 change: 1 addition & 0 deletions RATapi/examples/convert_rascal_project/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""An example of converting between RAT and RasCAL-1."""
Loading