Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add optional deps #98

Merged
merged 21 commits into from
Feb 26, 2025
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
8 changes: 5 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,15 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: checkout
uses: actions/checkout@v3
uses: actions/checkout@v4
with:
fetch-depth: "0"
- name: install build
run: python -m pip install build pytest --user
- name: build wheel
run: python -m build --sdist --wheel
- name: upload wheel
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
path: dist/

Expand All @@ -33,10 +33,12 @@ jobs:
id: mint
uses: tschm/[email protected]

- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: artifact
path: dist
pattern: cibw-*
merge-multiple: true

- name: Publish package distribution to PYPI
uses: pypa/gh-action-pypi-publish@release/v1
Expand Down
11 changes: 9 additions & 2 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,15 @@ jobs:
run: |
conda activate quickBayes-dev
python -m pip install .
- name: run tests
- name: run tests default
timeout-minutes: 10
shell: bash -l {0}
run: |
python -m pytest
python -m pytest test/default test/shared
- name: run tests gofit
timeout-minutes: 10
shell: bash -l {0}
run: |
python -m pip uninstall quickBayes
python -m pip install .[gofit]
python -m pytest test/gofit test/shared
2 changes: 1 addition & 1 deletion docs/source/examples/QENS.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"metadata": {},
"outputs": [],
"source": [
"DATA_DIR = os.path.join('..', '..', '..', 'test', 'data')\n",
"DATA_DIR = os.path.join('..', '..', '..', 'test', 'shared', 'data')\n",
"\n",
"sample_file = os.path.join(DATA_DIR, 'sample_data_red.npy')\n",
"resolution_file = os.path.join(DATA_DIR, 'resolution_data_red.npy')\n",
Expand Down
2 changes: 1 addition & 1 deletion docs/source/examples/muon.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
"metadata": {},
"outputs": [],
"source": [
"DATA_DIR = os.path.join('..', '..', '..', 'test', 'data', 'muon')\n",
"DATA_DIR = os.path.join('..', '..', '..', 'test', 'shared', 'data', 'muon')\n",
"data_file = os.path.join(DATA_DIR, 'muon_expdecay_2.npy')\n",
"\n",
"sx, sy, se = np.loadtxt(data_file)\n",
Expand Down
36 changes: 36 additions & 0 deletions docs/source/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,42 @@ The library is available on `PyPi <https://pypi.org/project/quickBayes/#descript

python -m pip install quickBayes

The default install does not include `gofit <https://ralna.github.io/GOFit/_build/html/index.html>`_.
To install quickBayes with gofit the command is

.. code-block:: python

python -m pip install quickBayes[gofit]


Running the Tests
-----------------

The tests are split into three sections:

- shared
- default
- gofit

and these distinctions exist to allow the testing for both with and without gofit.
The tests in shared correspond to those that should be the same regardless of the installed packages.
The default tests are what should happen if an optional dependency is missing.
Typically this is producing an error at initialisation.
The gofit tests are to test the functionality of gofit.

To run the default tests

.. code-block:: python

pytest test/default

and to run both the gofit and shared tests

.. code-block:: python

pytest test/shared test/gofit


Reporting Issues
----------------

Expand Down
8 changes: 6 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ build-backend = 'hatchling.build'

[project]
name = 'quickBayes'
version = "1.0.0b23"
version = "1.0.1b0"
requires-python = ">=3.7.1"
dependencies = ['numpy<2.0.0', 'scipy', 'pybind11[global]', 'eigen', 'gofit']
dependencies = ['numpy<2.0.0', 'scipy']
authors = [{name='Anthony Lim', email='[email protected]'}]
description = "A Bayesian fitting package used for model selection and grid searches of fits for neutron and muon data."
keywords=['bayesian', 'fitting', 'QENS', 'muons']
Expand All @@ -17,6 +17,10 @@ license = {text = 'BSD'}
docs = ["sphinx==7.3.7",
'jupyter-book',
"nbsphinx==0.9.4"]
gofit = ['pybind11[global]',
'eigen',
'gofit']


[project.urls]
Homepage = "https://quickbayes.readthedocs.io/en/latest/"
Expand Down
1 change: 0 additions & 1 deletion quickBayes-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,4 @@ dependencies:
- pybind11[global]
- eigen
- pip:
- gofit
- readthedocs-sphinx-ext
106 changes: 106 additions & 0 deletions src/quickBayes/fitting/gofit_base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from numpy import ndarray
from typing import Callable
from quickBayes.fitting.fit_engine import FitEngine
from gofit import multistart


"""
This file contains all of the code needed for a gofit
engine, because of the way gofit works we need to provide
a cost function. However, when the cost function is called
by gofit it is not provided with the original data. Hence,
we need to construct a cost function class first that has
the data as a member variable.
"""


class ChiSquared(object):
def __init__(self, x_data: ndarray, y_data: ndarray,
e_data: ndarray, func: Callable):
"""
A chi^2 cost function class for use with gofit
:param x_data: x data that fitted against
:param y_data: y data that fitted against
:param e_data: e data that fitted against
:param func: the fitting function used
"""
self._x_data = x_data
self._y_data = y_data
self._e_data = e_data
self._func = func

def __call__(self, params: ndarray) -> float:
"""
Calls the evaluation of the cost function
:param params: the fit parameters
:return the cost function evaluation
"""
fit = self._func(self._x_data, *params)
return (fit - self._y_data)**2 / self._e_data**2


class _GoFitEngine(FitEngine):
"""
A gofit multistart fit engine.
This will use gofit's multistart to
fit data.
"""

def __init__(self, x_data: ndarray, y_data: ndarray, e_data: ndarray,
lower: ndarray, upper: ndarray, samples: int = 10,
max_iterations: int = 220000):
"""
Creates the gofit multistart fit engine class
Stores useful information about each fit
:param name: name of the fit engine
:param x_data: original x data (can fit to an interpolation)
:param y_data: original y data (can fit to an interpolation)
:param e_data: original e data (can fit to an interpolation)
:param lower: the lower bounds for the fit parameters
:param upper: the upper bounds for the fit parameters
:param samples: the number of samples to use in multistart
:param max_iterations: the maximum number of iterations for the fit
"""
super().__init__("gofit", x_data, y_data, e_data)
# extra parameters
self.set_bounds_and_N_params(lower, upper)
self._max_iterations = max_iterations
self._samples = samples

def set_bounds_and_N_params(self, lower: ndarray, upper: ndarray) -> None:
"""
Sets the current bounds and number of parameters for the fit function.
If the functional form changes this method will need to be called
with updated values.
:param lower: the lower bound for the function parameters
:param upper: the upper bound for the function parameters
"""
# validate
if len(upper) != len(lower):
raise ValueError(f"The lower {lower} and "
f"upper {upper} bounds must "
"be the same length")
self._lower = lower
self._upper = upper
self._N_params = len(upper)

def _do_fit(self, x_data: ndarray, y_data: ndarray, e_data: ndarray,
func: Callable) -> ndarray:
"""
Calls gofit multistart
:param x_data: the x data to fit
:param y_data: the y data to fit
:param e_data: the error data to fit
:param func: the fitting function
:return the fit parameters
"""
cost_function = ChiSquared(x_data, y_data, e_data, func)

data_length = len(x_data)

params, _ = multistart(data_length, self._N_params,
self._lower, self._upper,
cost_function, samples=self._samples,
maxit=self._max_iterations)

return params
147 changes: 45 additions & 102 deletions src/quickBayes/fitting/gofit_engine.py
Original file line number Diff line number Diff line change
@@ -1,106 +1,49 @@
from gofit import multistart
from numpy import ndarray
from typing import Callable
from quickBayes.fitting.fit_engine import FitEngine


"""
This file contains all of the code needed for a gofit
engine, because of the way gofit works we need to provide
a cost function. However, when the cost function is called
by gofit it is not provided with the original data. Hence,
we need to construct a cost function class first that has
the data as a member variable.
"""


class ChiSquared(object):
def __init__(self, x_data: ndarray, y_data: ndarray,
e_data: ndarray, func: Callable):
"""
A chi^2 cost function class for use with gofit
:param x_data: x data that fitted against
:param y_data: y data that fitted against
:param e_data: e data that fitted against
:param func: the fitting function used
"""
self._x_data = x_data
self._y_data = y_data
self._e_data = e_data
self._func = func

def __call__(self, params: ndarray) -> float:
"""
Calls the evaluation of the cost function
:param params: the fit parameters
:return the cost function evaluation
"""
fit = self._func(self._x_data, *params)
return (fit - self._y_data)**2 / self._e_data**2


class GoFitEngine(FitEngine):
"""
A gofit multistart fit engine.
This will use gofit's multistart to
fit data.
"""

def __init__(self, x_data: ndarray, y_data: ndarray, e_data: ndarray,
lower: ndarray, upper: ndarray, samples: int = 10,
max_iterations: int = 220000):
"""
Creates the scipy curve fit engine class
Stores useful information about each fit
:param name: name of the fit engine
:param x_data: original x data (can fit to an interpolation)
:param y_data: original y data (can fit to an interpolation)
:param e_data: original e data (can fit to an interpolation)
:param lower: the lower bounds for the fit parameters
:param upper: the upper bounds for the fit parameters
:param samples: the number of samples to use in multistart
:param max_iterations: the maximum number of iterations for the fit
"""
super().__init__("gofit", x_data, y_data, e_data)
# extra parameters
self.set_bounds_and_N_params(lower, upper)
self._max_iterations = max_iterations
self._samples = samples

def set_bounds_and_N_params(self, lower: ndarray, upper: ndarray) -> None:
"""
Sets the current bounds and number of parameters for the fit function.
If the functional form changes this method will need to be called
with updated values.
:param lower: the lower bound for the function parameters
:param upper: the upper bound for the function parameters
"""
# validate
if len(upper) != len(lower):
raise ValueError(f"The lower {lower} and "
f"upper {upper} bounds must "
"be the same length")
self._lower = lower
self._upper = upper
self._N_params = len(upper)

def _do_fit(self, x_data: ndarray, y_data: ndarray, e_data: ndarray,
func: Callable) -> ndarray:
"""
Calls gofit multistart
:param x_data: the x data to fit
:param y_data: the y data to fit
:param e_data: the error data to fit
:param func: the fitting function
:return the fit parameters
"""
cost_function = ChiSquared(x_data, y_data, e_data, func)

data_length = len(x_data)

params, _ = multistart(data_length, self._N_params,
self._lower, self._upper,
cost_function, samples=self._samples,
maxit=self._max_iterations)

return params
try:
from quickBayes.fitting.gofit_base import _GoFitEngine

class GoFitEngine(_GoFitEngine):

def __init__(self, x_data: ndarray, y_data: ndarray, e_data: ndarray,
lower: ndarray, upper: ndarray, samples: int = 10,
max_iterations: int = 220000):
"""
Creates the gofit multistart fit engine class
Stores useful information about each fit
:param name: name of the fit engine
:param x_data: original x data (can fit to an interpolation)
:param y_data: original y data (can fit to an interpolation)
:param e_data: original e data (can fit to an interpolation)
:param lower: the lower bounds for the fit parameters
:param upper: the upper bounds for the fit parameters
:param samples: the number of samples to use in multistart
:param max_iterations: the maximum number of iterations for the fit
"""
super().__init__(x_data, y_data, e_data,
lower, upper, samples, max_iterations)

except ImportError:

class GoFitEngine(FitEngine):

def __init__(self, x_data: ndarray, y_data: ndarray, e_data: ndarray,
lower: ndarray, upper: ndarray, samples: int = 10,
max_iterations: int = 220000):
"""
Creates a dummy gofit engine class.
This is to prevent errors if gofit is not installed.
Stores useful information about each fit
:param name: name of the fit engine
:param x_data: original x data (can fit to an interpolation)
:param y_data: original y data (can fit to an interpolation)
:param e_data: original e data (can fit to an interpolation)
:param lower: the lower bounds for the fit parameters
:param upper: the upper bounds for the fit parameters
:param samples: the number of samples to use in multistart
:param max_iterations: the maximum number of iterations for the fit
"""
raise RuntimeError("gofit is not installed. Please "
"install gofit to use this functionality")
Loading
Loading