Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3a3738f
Merge pull request #76 from EasyScience/develop
andped10 Sep 18, 2024
59c4a90
Merge pull request #83 from EasyScience/develop
AndrewSazonov Oct 29, 2024
23f4ced
Merge pull request #86 from EasyScience/develop
AndrewSazonov Nov 12, 2024
a6d9379
Merge pull request #87 from EasyScience/develop
andped10 Nov 13, 2024
5fdd558
Merge pull request #90 from EasyScience/develop
AndrewSazonov Nov 21, 2024
c7e2ec4
Merge pull request #106 from easyscience/develop
rozyczko Mar 13, 2025
acb7f17
Merge pull request #107 from easyscience/develop
rozyczko Mar 13, 2025
a02aa89
Merge pull request #126 from easyscience/develop
rozyczko Sep 19, 2025
5f25fc7
Merge pull request #170 from easyscience/develop
damskii9992 Dec 2, 2025
5bd111e
try force pushing docs to gh-pages
rozyczko Dec 2, 2025
0a78fae
copy the content of the produced docs, not the main repo
rozyczko Dec 2, 2025
e7299b2
update the documentation page address
rozyczko Dec 2, 2025
fd9a5a4
added codecov badge
rozyczko Dec 3, 2025
9ff2bcd
apparent need to write permissions on actions-gh-pages
rozyczko Dec 3, 2025
0fd4c37
Merge pull request #171 from easyscience/hotfix_2.1.0a
rozyczko Dec 3, 2025
08ef4e4
Add labelSeparator option to issue label checks
damskii9992 Dec 17, 2025
af7f27b
Merge pull request #178 from easyscience/Hotfix_2.1.0b
damskii9992 Dec 17, 2025
68ed092
Add stateless calculator factory pattern for physics calculators
rozyczko Dec 19, 2025
d79061f
Merge branch 'develop' into calculator_factory
rozyczko Dec 19, 2025
b7da00f
updates
rozyczko Dec 20, 2025
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
10 changes: 9 additions & 1 deletion src/easyscience/fitting/calculators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
# SPDX-License-Identifier: BSD-3-Clause
# © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/easyscience

from .calculator_base import CalculatorBase
from .calculator_factory import CalculatorFactoryBase
from .calculator_factory import SimpleCalculatorFactory
from .interface_factory import InterfaceFactoryTemplate

__all__ = [InterfaceFactoryTemplate]
__all__ = [
'CalculatorBase',
'CalculatorFactoryBase',
'SimpleCalculatorFactory',
'InterfaceFactoryTemplate', # Deprecated, kept for backwards compatibility
]
263 changes: 263 additions & 0 deletions src/easyscience/fitting/calculators/calculator_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# SPDX-FileCopyrightText: 2025 EasyScience contributors <core@easyscience.software>
# SPDX-License-Identifier: BSD-3-Clause
# © 2021-2025 Contributors to the EasyScience project <https://github.com/easyScience/EasyScience

"""
Abstract base class for physics calculators in EasyScience.

This module provides the foundation for implementing physics calculators that compute
theoretical results based on a model and instrumental parameters. Concrete implementations
are provided in product-specific libraries (e.g., EasyReflectometryLib).

Example usage in a product library::

from easyscience.fitting.calculators import CalculatorBase

class ReflectivityCalculator(CalculatorBase):
def __init__(self, model, instrumental_parameters, **kwargs):
super().__init__(model, instrumental_parameters, **kwargs)
# Initialize calculator-specific internals

def calculate(self, x):
# Compute reflectivity using self._model and self._instrumental_parameters
return reflectivity_curve

def get_sld_profile(self):
# Calculator-specific method for SLD profile
return sld_profile
"""

from __future__ import annotations

from abc import ABCMeta
from abc import abstractmethod
from typing import TYPE_CHECKING
from typing import Any
from typing import Optional

if TYPE_CHECKING:
import numpy as np

from easyscience.base_classes import ModelBase


class CalculatorBase(ModelBase, metaclass=ABCMeta):
"""
Abstract base class for physics calculators.

A calculator is responsible for computing theoretical results based on a physical
model and instrumental parameters. This decouples the physics engine from the
model definition, allowing different calculation backends to be used interchangeably.

The calculator:
- Takes a model (sample) and instrumental parameters in its constructor
- Provides a `calculate(x)` method for computing theoretical values
- Allows updating the model and instrumental parameters at runtime

Parameters
----------
model : ModelBase
The physical model (e.g., sample structure) to calculate from.
instrumental_parameters : ModelBase, optional
Instrumental parameters (e.g., resolution, wavelength) that affect the calculation.
**kwargs : Any
Additional calculator-specific configuration options.

Attributes
----------
name : str
The name of this calculator implementation. Should be overridden by subclasses.

Examples
--------
Subclasses must implement the `calculate` method::

class MyCalculator(CalculatorBase):
name = "my_calculator"

def calculate(self, x):
# Use self._model and self._instrumental_parameters
return computed_values
"""

name: str = 'base'

def __init__(
self,
model: ModelBase,
instrumental_parameters: Optional[ModelBase] = None,
unique_name: Optional[str] = None,
display_name: Optional[str] = None,
**kwargs: Any,
) -> None:
"""
Initialize the calculator with a model and instrumental parameters.

Parameters
----------
model : ModelBase
The physical model to calculate from. This is typically a sample
or structure definition containing fittable parameters.
instrumental_parameters : ModelBase, optional
Instrumental parameters that affect the calculation, such as
resolution, wavelength, or detector settings.
unique_name : str, optional
Unique identifier for this calculator instance.
display_name : str, optional
Human-readable name for display purposes.
**kwargs : Any
Additional calculator-specific options.
"""
if not isinstance(model, ModelBase):
raise ValueError('Model must be an instance of ModelBase')

# Initialize ModelBase with naming
super().__init__(unique_name=unique_name, display_name=display_name)

self._model = model
self._instrumental_parameters = instrumental_parameters
self._additional_kwargs = kwargs

@property
def model(self) -> ModelBase:
"""
Get the current physical model.

Returns
-------
ModelBase
The physical model used for calculations.
"""
return self._model

@model.setter
def model(self, new_model: ModelBase) -> None:
"""
Set a new physical model.

Parameters
----------
new_model : ModelBase
The new physical model to use for calculations.

Raises
------
ValueError
If the new model is None.
"""
if new_model is None:
raise ValueError('Model cannot be None')
self._model = new_model

@property
def instrumental_parameters(self) -> Optional[ModelBase]:
"""
Get the current instrumental parameters.

Returns
-------
ModelBase or None
The instrumental parameters, or None if not set.
"""
return self._instrumental_parameters

@instrumental_parameters.setter
def instrumental_parameters(self, new_parameters: Optional[ModelBase]) -> None:
"""
Set new instrumental parameters.

Parameters
----------
new_parameters : ModelBase or None
The new instrumental parameters to use for calculations.
Truly optional, since instrumental parameters may not always be needed.
"""
self._instrumental_parameters = new_parameters

def update_model(self, new_model: ModelBase) -> None:
"""
Update the physical model used for calculations.

This is an alternative to the `model` property setter that can be
overridden by subclasses to perform additional setup when the model changes.

Parameters
----------
new_model : ModelBase
The new physical model to use.

Raises
------
ValueError
If the new model is None.
"""
self.model = new_model

def update_instrumental_parameters(self, new_parameters: Optional[ModelBase]) -> None:
"""
Update the instrumental parameters used for calculations.

This is an alternative to the `instrumental_parameters` property setter
that can be overridden by subclasses to perform additional setup when
instrumental parameters change.

Parameters
----------
new_parameters : ModelBase or None
The new instrumental parameters to use.
"""
self.instrumental_parameters = new_parameters

@property
def additional_kwargs(self) -> dict:
"""
Get additional keyword arguments passed during initialization.

Returns a copy to prevent external modification of internal state.

Returns
-------
dict
Copy of the dictionary of additional kwargs passed to __init__.
"""
return dict(self._additional_kwargs)

@abstractmethod
def calculate(self, x: np.ndarray) -> np.ndarray:
"""
Calculate theoretical values at the given points.

This is the main calculation method that must be implemented by all
concrete calculator classes. It uses the current model and instrumental
parameters to compute theoretical predictions.

Parameters
----------
x : np.ndarray
The independent variable values (e.g., Q values, angles, energies)
at which to calculate the theoretical response.

Returns
-------
np.ndarray
The calculated theoretical values corresponding to the input x values.

Notes
-----
This method is called during fitting and should be thread-safe if
parallel fitting is to be supported.
"""
...

def __repr__(self) -> str:
"""Return a string representation of the calculator."""
model_name = getattr(self._model, 'name', type(self._model).__name__)
instr_info = ''
if self._instrumental_parameters is not None:
instr_name = getattr(
self._instrumental_parameters,
'name',
type(self._instrumental_parameters).__name__, # default to class name if no 'name' attribute
)
instr_info = f', instrumental_parameters={instr_name}'
return f'{self.__class__.__name__}(model={model_name}{instr_info})'
Loading
Loading