Repository: rhayes777/PyAutoFit
Pull request #1144 (2025-05-30 10:30:25)

feature/constant float
Constant should behave more like a float

```python
isinstance(constant, float)
> True

isinstance(constant, Constant)
> True



Patch:
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 27a26830b..75e0a78a2 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -59,9 +59,10 @@ jobs:
         pip3 install setuptools
         pip3 install wheel
         pip3 install pytest coverage pytest-cov
-        pip install ./PyAutoConf
-        pip install ./PyAutoFit
-        pip install ./PyAutoFit[optional]
+        pip3 install -r PyAutoConf/requirements.txt
+        pip3 install -r PyAutoFit/requirements.txt
+        pip3 install -r PyAutoFit/optional_requirements.txt
+        pip3 install -r PyAutoFit/build_requirements.txt
     - name: Run tests
       run: |
         export ROOT_DIR=`pwd`
diff --git a/autofit/mapper/model_object.py b/autofit/mapper/model_object.py
index fd2fbd123..c5a4c48ec 100644
--- a/autofit/mapper/model_object.py
+++ b/autofit/mapper/model_object.py
@@ -296,6 +296,8 @@ class ModelObject:
             type_ = "tuple_prior"
         elif isinstance(self, Array):
             type_ = "array"
+        elif isinstance(self, Constant):
+            type_ = "constant"
         else:
             raise AssertionError(
                 f"{self.__class__.__name__} cannot be serialised to dict"
diff --git a/autofit/mapper/prior/constant.py b/autofit/mapper/prior/constant.py
index 3e1db0c96..69b023003 100644
--- a/autofit/mapper/prior/constant.py
+++ b/autofit/mapper/prior/constant.py
@@ -3,7 +3,11 @@ from numbers import Number
 from autofit.mapper.model_object import ModelObject
 
 
-class Constant(ModelObject, Number):
+class Constant(float, ModelObject):
+    def __new__(cls, value: float, id_=None):
+        obj = super().__new__(cls, value)
+        return obj
+
     def __init__(self, value: float, id_=None):
         """
         Represents a constant value in a model.
@@ -18,63 +22,27 @@ class Constant(ModelObject, Number):
         value
             The constant value.
         """
-        super().__init__(
-            id_=id_,
-        )
+        if isinstance(value, Constant):
+            value = value.value
+
+        ModelObject.__init__(self, id_=id_)
         self.value = value
 
     def __str__(self):
         return str(self.value)
 
     def __eq__(self, other):
-        if isinstance(other, (int, float)):
-            return self.value == other
-        return super().__eq__(other)
+        if isinstance(other, ModelObject):
+            return ModelObject.__eq__(self, other)
+        return self.value == other
+
+    def __ne__(self, other):
+        if isinstance(other, ModelObject):
+            return ModelObject.__ne__(self, other)
+        return self.value != other
 
     def __hash__(self):
         return hash(self.id)
 
     def dict(self):
         return {"type": "Constant", "value": self.value}
-
-    def __add__(self, other):
-        return self.value + other
-
-    def __sub__(self, other):
-        return self.value - other
-
-    def __mul__(self, other):
-        return self.value * other
-
-    def __truediv__(self, other):
-        return self.value / other
-
-    def __pow__(self, other):
-        return self.value**other
-
-    def __radd__(self, other):
-        return other + self.value
-
-    def __rsub__(self, other):
-        return other - self.value
-
-    def __rmul__(self, other):
-        return other * self.value
-
-    def __rtruediv__(self, other):
-        return other / self.value
-
-    def __rpow__(self, other):
-        return other**self.value
-
-    def __lt__(self, other):
-        return self.value < other
-
-    def __le__(self, other):
-        return self.value <= other
-
-    def __gt__(self, other):
-        return self.value > other
-
-    def __ge__(self, other):
-        return self.value >= other
diff --git a/autofit/mapper/prior/tuple_prior.py b/autofit/mapper/prior/tuple_prior.py
index 148b3963e..a3937b4a6 100644
--- a/autofit/mapper/prior/tuple_prior.py
+++ b/autofit/mapper/prior/tuple_prior.py
@@ -51,21 +51,17 @@ class TuplePrior(ModelObject):
         instances: [(String, instance)]
             A list of instances
         """
-        return list(
-            sorted(
+        return [
+            (key, value.value if isinstance(value, Constant) else value)
+            for key, value in sorted(
                 [
                     (key, value)
                     for key, value in self.__dict__.items()
                     if key != "id" and isinstance(value, (int, float))
-                ]
-                + [
-                    (key, value.value)
-                    for key, value in self.__dict__.items()
-                    if isinstance(value, Constant)
                 ],
                 key=lambda tup: tup[0],
             )
-        )
+        ]
 
     def value_for_arguments(self, arguments):
         """
diff --git a/docs/conf.py b/docs/conf.py
index cdd05935d..fcb18313a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -12,18 +12,30 @@ import datetime
 # documentation root, use os.path.abspath to make it absolute, like shown here.
 #
 
+from pyprojroot import here
+
+workspace_path = str(here())
+
 import os
 import sys
 
 sys.path.insert(0, os.path.abspath("."))
 
+clone_path = str(here())
+clone_path = os.path.split(clone_path)[0]
+
+sys.path.insert(
+    0,
+    os.path.abspath(clone_path),
+)
+
 import autofit
 
 # -- Project information -----------------------------------------------------
 
 year = datetime.date.today().year
 project = "PyAutoFit"
-copyright = "2025, James Nightingale, Richard Hayes"
+copyright = "2022, James Nightingale, Richard Hayes"
 author = "James Nightingale, Richard Hayes"
 
 # The full version, including alpha/beta/rc tags
diff --git a/docs/overview/the_basics.rst b/docs/overview/the_basics.rst
index 3bdd0fd66..da3d213d5 100644
--- a/docs/overview/the_basics.rst
+++ b/docs/overview/the_basics.rst
@@ -573,69 +573,31 @@ Multiple Datasets
 Many model-fitting problems require multiple datasets to be fitted simultaneously in order to provide the best
 constraints on the model.
 
-In **PyAutoFit**, all you have to do to fit multiple datasets is combine them with the model via ``AnalysisFactor``
-objects.
+In **PyAutoFit**, all you have to do to fit multiple datasets is sum your ``Analysis`` classes together:
 
 .. code-block:: python
 
-    analysis_0 = Analysis(data=data, noise_map=noise_map)
-    analysis_1 = Analysis(data=data, noise_map=noise_map)
+    analysis_0 = Analysis(data=data_0, noise_map=noise_map_0)
+    analysis_1 = Analysis(data=data_1, noise_map=noise_map_1)
 
-    analysis_list = [analysis_0, analysis_1]
+    # This means the model is fitted to both datasets simultaneously.
 
-    analysis_factor_list = []
+    analysis = analysis_0 + analysis_1
 
-    for analysis in analysis_list:
+    # summing a list of analysis objects is also a valid API:
 
-        # The model can be customized here so that different model parameters are tied to each analysis.
-        model_analysis = model.copy()
+    analysis = sum([analysis_0, analysis_1])
 
-        analysis_factor = af.AnalysisFactor(prior_model=model_analysis, analysis=analysis)
-
-        analysis_factor_list.append(analysis_factor)
-
-All ``AnalysisFactor`` objects are combined into a ``FactorGraphModel``, which represents a global model fit to
-multiple datasets using a graphical model structure.
-
-The key outcomes of this setup are:
-
- - The individual log likelihoods from each ``Analysis`` object are summed to form the total log likelihood
-   evaluated during the model-fitting process.
-
- - Results from all datasets are output to a unified directory, with subdirectories for visualizations
-   from each analysis object, as defined by their ``visualize`` methods.
-
-This is a basic use of **PyAutoFit**'s graphical modeling capabilities, which support advanced hierarchical
-and probabilistic modeling for large, multi-dataset analyses.
-
-To inspect the model, we print ``factor_graph.global_prior_model.info``.
-
-.. code-block:: python
-
-    print(factor_graph.global_prior_model.info)
-
-To fit multiple datasets, we pass the ``FactorGraphModel`` to a non-linear search.
-
-Unlike single-dataset fitting, we now pass the ``factor_graph.global_prior_model`` as the model and
-the ``factor_graph`` itself as the analysis object.
-
-This structure enables simultaneous fitting of multiple datasets in a consistent and scalable way.
-
-.. code-block:: python
-
-    search = af.DynestyStatic(
-        nlive=100,
-    )
-
-    result_list = search.fit(model=factor_graph.global_prior_model, analysis=factor_graph)
+By summing analysis objects the log likelihood values computed by the ``log_likelihood_function`` of each individual
+analysis class are summed to give an overall log likelihood value that the non-linear search samples when model-fitting.
 
 .. note::
 
     In the simple example above, instances of the same ``Analysis`` class (``analysis_0`` and ``analysis_1``) were
-    combined. However, different ``Analysis`` classes can also be combined. This is useful when fitting different
-    datasets that each require a unique ``log_likelihood_function`` to be fitted simultaneously. For more detailed
-    information and a dedicated API for customizing how the model changes across different datasets, refer to
-    the [multiple datasets cookbook](https://pyautofit.readthedocs.io/en/latest/cookbooks/multiple_datasets.html).
+summed. However, different ``Analysis`` classes can also be summed together. This is useful when fitting different
+datasets that each require a unique ``log_likelihood_function`` to be fitted simultaneously. For more detailed
+information and a dedicated API for customizing how the model changes across different datasets, refer to
+the [multiple datasets cookbook](https://pyautofit.readthedocs.io/en/latest/cookbooks/multiple_datasets.html).
 
 Wrap Up
 -------
diff --git a/pyproject.toml b/pyproject.toml
index 3d42be5cd..5900bd2eb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -74,15 +74,6 @@ optional=[
     "ultranest==4.3.2",
     "zeus-mcmc==2.5.4",
 ]
-docs=[
-    "sphinx",
-    "furo",
-    "myst-parser",
-    "sphinx_copybutton",
-    "sphinx_design",
-    "sphinx_inline_tabs",
-    "sphinx_autodoc_typehints"
-]
 
 test = ["pytest"]
 dev = ["pytest", "black"]
diff --git a/readthedocs.yml b/readthedocs.yml
index 2c9c24e6d..ca3049c2a 100644
--- a/readthedocs.yml
+++ b/readthedocs.yml
@@ -1,16 +1,13 @@
 version: 2
 
 build:
-  os: ubuntu-22.04
+  os: ubuntu-20.04
   tools:
     python: "3.11"
 
 python:
   install:
-    - method: pip
-      path: .
-      extra_requirements:
-        - docs
+  - requirements: docs/requirements.txt
 
 sphinx:
   configuration: docs/conf.py
\ No newline at end of file


Test patch:
diff --git a/test_autofit/mapper/test_constant.py b/test_autofit/mapper/test_constant.py
index 671227cb9..037f1e0c3 100644
--- a/test_autofit/mapper/test_constant.py
+++ b/test_autofit/mapper/test_constant.py
@@ -167,3 +167,21 @@ def test_model_with_constants():
     assert instance.centre == 1.0
     assert isinstance(instance.centre, float)
     assert not isinstance(instance.centre, Constant)
+
+
+def test_as_float():
+    constant = af.Constant(1.0)
+    assert float(constant) == 1.0
+
+    constant_2 = af.Constant(2.0)
+    assert float(constant_2) == 2.0
+
+    assert constant.id != constant_2.id
+
+    assert isinstance(float(constant), float)
+
+
+def test_is_instance():
+    constant = af.Constant(1.0)
+
+    assert isinstance(constant, float)