Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
12ee356
initial commit
rozyczko Aug 6, 2026
f783dc7
weird linting
rozyczko Aug 6, 2026
2ee4d8d
updated comments, type hinting and such
rozyczko Aug 7, 2026
cf920f4
removed all legacy junk
rozyczko Aug 9, 2026
3e59810
minor CR comments addressed
rozyczko Aug 10, 2026
59d0eb8
minor text fixes and simplifications
rozyczko Sep 1, 2026
7519f4f
fixed two minor issues in the LMFit wrapper. #301 and #302
rozyczko Sep 2, 2026
d364c7e
more unit tests so Codecov doesn't complain
rozyczko Sep 2, 2026
fff7998
CR comments addressed
rozyczko Sep 2, 2026
693e11c
Merge pull request #303 from easyscience/assign-none-if-no-covariance
rozyczko Sep 3, 2026
fd8879b
Replaced CollectionBase with EasyList #304
rozyczko Sep 4, 2026
39f308c
DescriptorBase now is a ModelBase class
rozyczko Sep 4, 2026
fda0d28
updated handling of unique_names
rozyczko Sep 4, 2026
c02814f
updated docstrings and some docs
rozyczko Sep 4, 2026
ac5966a
code review comments. part 1
rozyczko Sep 8, 2026
85f0bb3
removed optional Parameters list passed to the minimizers. Part of the
rozyczko Sep 8, 2026
6aadc07
docstring lint
rozyczko Sep 8, 2026
d3a1d0d
More fixes for the PR review. Not ready yet
rozyczko Sep 10, 2026
82a1b01
PR review issues addressed #3
rozyczko Sep 11, 2026
4c3a8b4
PR issues addressed
rozyczko Sep 11, 2026
2e9d9c2
PR review fixes
rozyczko Sep 11, 2026
08b08e9
Merge branch 'easylist-on-multifitter' into modelbase-on-descriptor
rozyczko Sep 11, 2026
03f86be
fix the behaviour when parameters are passed
rozyczko Sep 14, 2026
f646049
list -> sequence
rozyczko Sep 15, 2026
5f8bd2f
Make Sampler completely Fitter-agnostic
rozyczko Sep 17, 2026
71e3327
CR review fixes
rozyczko Sep 22, 2026
922b2a3
Addressed #307: Fix EasyList to accept a list of parameters
rozyczko Sep 22, 2026
eb73c97
Merge pull request #306 from easyscience/modelbase-on-descriptor
rozyczko Sep 22, 2026
6a7fa25
Merge pull request #305 from easyscience/easylist-on-multifitter
rozyczko Sep 22, 2026
1214080
Merge branch 'sampler-engine-structure-280' into 280-more-refactoring
rozyczko Sep 22, 2026
22513ad
Merge pull request #310 from easyscience/280-more-refactoring
rozyczko Sep 22, 2026
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
7 changes: 4 additions & 3 deletions docs/docs/api-reference/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ This section contains the reference detailing the functions and modules
available in EasyScience.

- [base_classes](base_classes.md) – Core abstract and helper base
classes used to build EasyScience objects (e.g. `ObjBase`,
`ModelBase`).
classes used to build EasyScience objects (e.g. `NewBase`,
`ModelBase`, `EasyList`).
- [fitting](fitting.md) – Fitting utilities and interfaces, including
`Fitter` and available minimizers.
- [global_object](global_object.md) – Global singleton providing shared
Expand All @@ -23,4 +23,5 @@ available in EasyScience.
- [utils](utils.md) – Miscellaneous utility functions and helpers (class
tools, decorators, type helpers).
- [variable](variable.md) – Descriptor types and variable abstractions
(e.g. `DescriptorNumber`, `Parameter`, `DescriptorArray`).
(e.g. `DescriptorNumber`, `Parameter`, `DescriptorArray`). All of them
are `NewBase` objects, serialized with `to_dict`/`from_dict`.
76 changes: 33 additions & 43 deletions docs/docs/tutorials/fitting-bayesian.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"\n",
"where $\\theta$ are the model parameters, $d$ is the observed data, $p(d \\mid \\theta)$ is the likelihood, and $p(\\theta)$ is the prior. In `easyscience`, the `min`/`max` bounds of a `Parameter` are interpreted as a **uniform prior**, and a Gaussian likelihood is constructed from the data and supplied weights.\n",
"\n",
"`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `Sampler` class. Under the hood this uses BUMPS' DREAM sampler, so the underlying minimizer must be switched to BUMPS.\n",
"`easyscience` exposes a Bayesian Markov-chain Monte Carlo (MCMC) sampler through the `Sampler` class.\n",
"\n",
"```{note}\n",
"This tutorial focuses on Bayesian analysis with a simple QENS model for illustration. For dedicated QENS fitting with more sophisticated models, consider using [`EasyDynamics`](https://github.com/easyscience/easydynamics).\n",
Expand Down Expand Up @@ -138,12 +138,12 @@
},
{
"cell_type": "markdown",
"id": "9",
"id": "7",
"metadata": {},
"source": [
"## Defining parameters with priors\n",
"\n",
"Create four `Parameter` objects, for the area $A$, $\\gamma$, $\\omega_0$ and $\\sigma$. The `min` and `max` arguments define a **uniform prior** on each parameter — the sampler will only consider values inside this range and will treat every value inside the range as equally plausible *a priori*.\n",
"Create four `Parameter` objects, for the area $A$, $\\gamma$, $\\omega_0$ and $\\sigma$. The `min` and `max` arguments define a **uniform prior** on each parameter — the sampler will only consider values inside this range and will treat every value inside the range as equally plausible *a priori*. The parameters are then collected in an `ObjBase` container: this is the model object that both `Fitter` and `Sampler` take, so it is defined here, before the optional fit.\n",
"\n",
"| Parameter | Initial Value | Min | Max |\n",
"| --- | --- | --- | --- |\n",
Expand All @@ -156,21 +156,24 @@
{
"cell_type": "code",
"execution_count": null,
"id": "10",
"id": "8",
"metadata": {},
"outputs": [],
"source": [
"from easyscience import ObjBase\n",
"from easyscience import Parameter\n",
"\n",
"area = Parameter(name='area', value=10, fixed=False, min=1, max=100)\n",
"gamma = Parameter(name='gamma', value=8e-3, fixed=False, min=1e-4, max=1e-2)\n",
"omega_0 = Parameter(name='omega_0', value=1e-3, fixed=False, min=0, max=2e-3)\n",
"sigma = Parameter(name='sigma', value=1e-3, fixed=False, min=1e-5, max=1e-1)"
"sigma = Parameter(name='sigma', value=1e-3, fixed=False, min=1e-5, max=1e-1)\n",
"\n",
"parameter_container = ObjBase(name='params', A=area, gamma=gamma, omega_0=omega_0, sigma=sigma)"
]
},
{
"cell_type": "markdown",
"id": "77ce3f63",
"id": "9",
"metadata": {},
"source": [
"## The model\n",
Expand All @@ -193,7 +196,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "36a0c4f4",
"id": "10",
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -217,9 +220,7 @@
"source": [
"## Maximum-likelihood fit (optional, but recommended)\n",
"\n",
"Perform a quick maximum-likelihood fit. This is **not** a prerequisite for sampling: `Sampler`\n",
"needs a *configured* `Fitter`, not a *fitted* one, and you can sample straight from the initial\n",
"parameter values.\n",
"Perform a quick maximum-likelihood fit.\n",
"\n",
"It is worth doing anyway, for two reasons:\n",
"\n",
Expand All @@ -239,9 +240,6 @@
"outputs": [],
"source": [
"from easyscience import Fitter\n",
"from easyscience import ObjBase\n",
"\n",
"parameter_container = ObjBase(name='params', A=area, gamma=gamma, omega_0=omega_0, sigma=sigma)\n",
"\n",
"mle_fitter = Fitter(parameter_container, intensity_model)\n",
"mle_result = mle_fitter.fit(x=omega, y=intensity_obs, weights=1 / intensity_error)\n",
Expand All @@ -261,7 +259,7 @@
"\n",
"We now draw samples from the posterior distribution $p(\\theta \\mid d)$ using the BUMPS DREAM (DiffeRential Evolution Adaptive Metropolis) algorithm. DREAM is an ensemble MCMC method that runs multiple chains in parallel and automatically tunes the proposal distribution.\n",
"\n",
"DREAM only works with the BUMPS minimizer. We reuse the ``mle_fitter`` created above — any configured `Fitter` would do, and it does not have to have been fitted — switch it to BUMPS, and create a `Sampler` instance bound to the fitter and data. Calling `sampler.sample()` returns a `SamplingResults` object with the following attributes:\n",
"Create a `Sampler` from the same `parameter_container` and `intensity_model` we gave the `Fitter`, bound to the data. Calling `sampler.sample()` returns a `SamplingResults` object with the following attributes:\n",
"\n",
"- `draws`: a `(n_samples, n_parameters)` array of posterior samples: each **row** is one complete draw from the joint posterior (one value for every parameter simultaneously), and each **column** holds all sampled values for a single parameter. Note this is a *trimmed* view of the chain rather than the raw buffer, so `n_samples` is smaller than `samples / thin` — see the note under [Extend the chain](#extend-the-chain-and-check-convergence);\n",
"- `param_names`: the unique names of the parameters, in the same column order as `draws`;\n",
Expand All @@ -274,7 +272,9 @@
"- `burn` (500): the number of initial *burn-in* generations to discard — the sampler needs time to find the typical set of the posterior, and early samples are not representative. Note this counts generations, not raw samples, so `burn=500` discards `500 × n_chains` raw samples;\n",
"- `thin` (2): the *thinning* interval — only every second generation is kept, which reduces autocorrelation between consecutive draws;\n",
"\n",
"First, we switch to the BUMPS minimizer:"
"```{note}\n",
"If you already have a `Fitter`, `Sampler.from_fitter(mle_fitter, omega, intensity_obs, weights=1 / intensity_error)` builds the same sampler from the model bound to it. The fitter is neither needed afterwards nor modified.\n",
"```"
]
},
{
Expand All @@ -283,22 +283,12 @@
"id": "14",
"metadata": {},
"outputs": [],
"source": [
"from easyscience import AvailableMinimizers\n",
"\n",
"mle_fitter.switch_minimizer(AvailableMinimizers.Bumps)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a219fcd",
"metadata": {},
"outputs": [],
"source": [
"from easyscience.fitting import Sampler\n",
"\n",
"sampler = Sampler(mle_fitter, omega, intensity_obs, weights=1 / intensity_error)\n",
"sampler = Sampler(\n",
" parameter_container, intensity_model, omega, intensity_obs, weights=1 / intensity_error\n",
")\n",
"results = sampler.sample(samples=10000, burn=500, thin=2)\n",
"\n",
"print(f'Drew {results.draws.shape[0]} samples for {results.draws.shape[1]} parameters.')\n",
Expand All @@ -307,7 +297,7 @@
},
{
"cell_type": "markdown",
"id": "8766b170",
"id": "15",
"metadata": {},
"source": [
"## Convergence diagnostics\n",
Expand All @@ -323,7 +313,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "3c49ab6f",
"id": "16",
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -359,7 +349,7 @@
},
{
"cell_type": "markdown",
"id": "15",
"id": "17",
"metadata": {},
"source": [
"## Posterior summaries\n",
Expand All @@ -377,15 +367,15 @@
{
"cell_type": "code",
"execution_count": null,
"id": "ce3e38a8",
"id": "18",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "16",
"id": "19",
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -407,7 +397,7 @@
},
{
"cell_type": "markdown",
"id": "17",
"id": "20",
"metadata": {},
"source": [
"## Visualise the joint posterior\n",
Expand All @@ -418,7 +408,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "18",
"id": "21",
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -452,7 +442,7 @@
},
{
"cell_type": "markdown",
"id": "19",
"id": "22",
"metadata": {},
"source": [
"## Posterior-predictive band\n",
Expand All @@ -463,7 +453,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "20",
"id": "23",
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -499,7 +489,7 @@
},
{
"cell_type": "markdown",
"id": "03339658",
"id": "24",
"metadata": {},
"source": [
"## Extend the chain and check convergence\n",
Expand Down Expand Up @@ -543,7 +533,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "293b140b",
"id": "25",
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -573,7 +563,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "9ec4302c",
"id": "26",
"metadata": {},
"outputs": [],
"source": [
Expand Down Expand Up @@ -614,7 +604,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "b0f30be6",
"id": "27",
"metadata": {},
"outputs": [],
"source": [
Expand All @@ -641,7 +631,7 @@
},
{
"cell_type": "markdown",
"id": "50b7213a",
"id": "28",
"metadata": {},
"source": [
"### What is Gelman-Rubin R-hat?\n",
Expand Down Expand Up @@ -675,7 +665,7 @@
{
"cell_type": "code",
"execution_count": null,
"id": "3449e0a7",
"id": "29",
"metadata": {},
"outputs": [],
"source": [
Expand Down
6 changes: 3 additions & 3 deletions src/easyscience/base_classes/based_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,11 +239,11 @@ def __dir__(self) -> Iterable[str]:

def __copy__(self) -> BasedBase:
"""Return a copy of the object."""
temp = self.as_dict(skip=['unique_name'])
temp = self.to_dict(skip=['unique_name'])
new_obj = self.__class__.from_dict(temp)
return new_obj

def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]:
def to_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]:
"""
Convert an object into a full dictionary using
``SerializerDict``. This is a shortcut for
Expand All @@ -266,4 +266,4 @@ def as_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]:
skip = []
if 'unique_name' not in skip:
skip.append('unique_name')
return super().as_dict(skip=skip)
return super().to_dict(skip=skip)
28 changes: 18 additions & 10 deletions src/easyscience/base_classes/easy_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class EasyList(ModelBase, MutableSequence[ProtectedType_]):
# we would have to overwrite "extend", "remove", "__iadd__", "count", "append", "__iter__" and "clear"
def __init__(
self,
*args: ProtectedType_ | list[ProtectedType_],
*args: ProtectedType_ | Iterable[ProtectedType_],
protected_types: list[Type[NewBase]] | Type[NewBase] | None = None,
unique_name: Optional[str] = None,
display_name: Optional[str] = None,
Expand All @@ -41,11 +41,15 @@ def __init__(

Parameters
----------
*args : ProtectedType_ | list[ProtectedType_]
*args : ProtectedType_ | Iterable[ProtectedType_]
Initial items to add to the list.
protected_types : list[Type[NewBase]] | Type[NewBase] | None, default=None
Types that are allowed in the list. Can be a single NewBase
subclass or a list of them. If None,. By default, None.
subclass or a list of them. If None, any ``NewBase`` object
is accepted, including descriptors and parameters. Both bare
descriptors and ``ModelBase`` items contribute to
``get_all_variables`` and hence to fitting. By default,
None.
unique_name : Optional[str], default=None
Optional unique name for the list. By default, None.
display_name : Optional[str], default=None
Expand Down Expand Up @@ -76,7 +80,7 @@ def __init__(

# Add initial items
for item in args:
if isinstance(item, list):
if isinstance(item, (list, tuple)):
for sub_item in item:
self.append(sub_item)
else:
Expand Down Expand Up @@ -266,12 +270,14 @@ def _get_key(self, obj: ProtectedType_) -> str:

def get_all_variables(self) -> List[DescriptorBase]:
"""
Get all ``Descriptor`` and ``Parameter`` objects from all
elements that are derived from ``ModelBase``.
Get all ``Descriptor`` and ``Parameter`` objects held by this
list.

For each element that is a ``ModelBase`` instance, the element's
own ``get_all_variables()`` method is called and the results are
collected into a single flat list.
Elements that are ``DescriptorBase`` instances (e.g. a bare
``Parameter``) are collected directly, while elements derived
from ``ModelBase`` contribute the result of their own
``get_all_variables()`` call. Everything is collected into a
single flat list.

Returns
-------
Expand All @@ -281,7 +287,9 @@ def get_all_variables(self) -> List[DescriptorBase]:
"""
all_vars: List[DescriptorBase] = []
for item in self._data:
if isinstance(item, ModelBase):
if isinstance(item, DescriptorBase):
all_vars.append(item)
elif isinstance(item, ModelBase):
all_vars.extend(item.get_all_variables())
return all_vars

Expand Down
8 changes: 8 additions & 0 deletions src/easyscience/base_classes/new_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,14 @@ def to_dict(self, skip: Optional[List[str]] = None) -> Dict[str, Any]:
Dict[str, Any]
Encoded object containing all information to reform an
EasyScience object.

Notes
-----
A ``unique_name`` that was generated automatically is not
written; only an explicitly supplied one is, so that a
deserialized object gets a fresh name instead of clashing with
the original. Likewise ``display_name`` is omitted when it is
``None``. Pass ``skip`` to drop further fields.
"""
serializer = SerializerBase()
if skip is None:
Expand Down
Loading
Loading