Repository: rhayes777/PyAutoFit
Pull request #914 (2024-01-29 14:32:11)

feature/derived quantity
Use derived quantities when creating instances from a full set of samples



Patch:
diff --git a/autofit/aggregator/search_output.py b/autofit/aggregator/search_output.py
index a769f7293..803007f48 100644
--- a/autofit/aggregator/search_output.py
+++ b/autofit/aggregator/search_output.py
@@ -235,10 +235,24 @@ class SearchOutput(AbstractSearchOutput):
                 with open(self.files_path / "samples.csv") as f:
                     sample_list = samples_from_iterator(csv.reader(f))
 
+                try:
+                    with open(self.files_path / "derived_quantities.csv") as f:
+                        reader = csv.reader(f)
+                        headers = [
+                            tuple(part.strip() for part in header.split("."))
+                            for header in next(reader)
+                        ]
+                        derived_quantities_list = [
+                            dict(zip(headers, map(float, row))) for row in reader
+                        ]
+                except FileNotFoundError:
+                    derived_quantities_list = None
+
                 self._samples = SamplesPDF.from_list_info_and_model(
                     sample_list=sample_list,
                     samples_info=info_json,
                     model=self.model,
+                    derived_quantities_list=derived_quantities_list,
                 )
             except FileNotFoundError:
                 raise AttributeError("No samples found")
diff --git a/autofit/database/aggregator/aggregator.py b/autofit/database/aggregator/aggregator.py
index a61cf58df..4bfbebc9c 100644
--- a/autofit/database/aggregator/aggregator.py
+++ b/autofit/database/aggregator/aggregator.py
@@ -391,6 +391,7 @@ class Aggregator(AbstractAggregator):
         directory: str,
         auto_commit=True,
         reference: Optional[dict] = None,
+        completed_only: bool = False,
     ):
         """
         Recursively search a directory for autofit results
@@ -415,8 +416,15 @@ class Aggregator(AbstractAggregator):
         reference
             A dictionary mapping the names of objects in the model
             to their class path.
+        completed_only
+            If true only searches that have completed are added
         """
-        scraper = Scraper(directory, self.session, reference=reference)
+        scraper = Scraper(
+            directory,
+            self.session,
+            reference=reference,
+            completed_only=completed_only,
+        )
         scraper.scrape()
 
         if auto_commit:
diff --git a/autofit/database/aggregator/scrape.py b/autofit/database/aggregator/scrape.py
index 0538bf0b5..d46d9c197 100644
--- a/autofit/database/aggregator/scrape.py
+++ b/autofit/database/aggregator/scrape.py
@@ -15,6 +15,7 @@ class Scraper:
         directory: Union[Path, str],
         session: sa.orm.Session,
         reference: Optional[dict] = None,
+        completed_only: bool = True,
     ):
         """
         Facilitates scraping of data output into a directory
@@ -28,6 +29,8 @@ class Scraper:
             A database session
         reference
             A dictionary mapping search names to model paths
+        completed_only
+            If True, only completed fits will be scraped
         """
         self.directory = directory
         self.session = session
@@ -38,6 +41,7 @@ class Scraper:
         self.aggregator = ClassicAggregator.from_directory(
             self.directory,
             reference=self.reference,
+            completed_only=completed_only,
         )
 
     def scrape(self):
diff --git a/autofit/example/model.py b/autofit/example/model.py
index 7d3d22ea5..2e10a31f6 100644
--- a/autofit/example/model.py
+++ b/autofit/example/model.py
@@ -39,12 +39,8 @@ class Gaussian:
         self.sigma = sigma
 
     @derived_quantity
-    def lower_bound(self):
-        return self.centre - 5 * self.sigma
-
-    @derived_quantity
-    def upper_bound(self):
-        return self.centre + 5 * self.sigma
+    def fwhm(self):
+        return 2 * np.sqrt(2 * np.log(2)) * self.sigma
 
     def _tree_flatten(self):
         return (self.centre, self.normalization, self.sigma), None
diff --git a/autofit/non_linear/samples/interface.py b/autofit/non_linear/samples/interface.py
index d872ed2bc..4b1b653c1 100644
--- a/autofit/non_linear/samples/interface.py
+++ b/autofit/non_linear/samples/interface.py
@@ -9,16 +9,40 @@ from autofit.mapper.prior_model.abstract import Path
 
 
 def to_instance(func):
+    """
+    Decorator for methods that return a vector of parameters, which can be converted to a model instance.
+
+    If derived quantities are defined, they are added to the instance.
+
+    Parameters
+    ----------
+    func
+        A method that returns a vector of parameters
+
+    Returns
+    -------
+    A wrapper that converts the vector to a model instance
+    """
+
     @wraps(func)
     def wrapper(
-        obj, *args, as_instance: bool = True, **kwargs
+        self, *args, as_instance: bool = True, **kwargs
     ) -> Union[List, ModelInstance]:
-        vector = func(obj, *args, **kwargs)
+        vector = func(self, *args, **kwargs)
 
         if as_instance:
-            return obj.model.instance_from_vector(
+            instance = self.model.instance_from_vector(
                 vector=vector, ignore_prior_limits=True
             )
+            if self.parameters_derived_map is not None:
+                derived_quantities = self.parameters_derived_map[tuple(vector)]
+                for path, value in derived_quantities.items():
+                    obj = instance
+                    for attr in path[:-1]:
+                        obj = getattr(obj, attr)
+                    setattr(obj, path[-1], value)
+
+            return instance
 
         return vector
 
@@ -36,6 +60,10 @@ class SamplesInterface(ABC):
         self._names = None
         self._instance = None
 
+    @property
+    def parameters_derived_map(self):
+        return None
+
     @property
     def instance(self):
         if self._instance is None:
diff --git a/autofit/non_linear/samples/mcmc.py b/autofit/non_linear/samples/mcmc.py
index 65329f19c..34eccac85 100644
--- a/autofit/non_linear/samples/mcmc.py
+++ b/autofit/non_linear/samples/mcmc.py
@@ -23,6 +23,7 @@ class SamplesMCMC(SamplesPDF):
         search_internal: Optional = None,
         auto_correlation_settings: Optional[AutoCorrelationsSettings] = None,
         auto_correlations: Optional[AutoCorrelations] = None,
+        derived_quantities_list: Optional[List] = None,
     ):
         """
         The `Samples` classes in **PyAutoFit** provide an interface between the search_internal of
@@ -58,6 +59,7 @@ class SamplesMCMC(SamplesPDF):
             sample_list=sample_list,
             samples_info=samples_info,
             search_internal=search_internal,
+            derived_quantities_list=derived_quantities_list,
         )
 
     def __add__(self, other: "SamplesMCMC") -> "SamplesMCMC":
@@ -94,6 +96,34 @@ class SamplesMCMC(SamplesPDF):
             search_internal=None,
         )
 
+    @classmethod
+    def from_list_info_and_model(
+        cls,
+        sample_list,
+        samples_info,
+        model: AbstractPriorModel,
+        search_internal=None,
+        derived_quantities_list=None,
+    ):
+        try:
+            auto_correlation_settings = AutoCorrelationsSettings(
+                check_for_convergence=True,
+                check_size=samples_info["check_size"],
+                required_length=samples_info["required_length"],
+                change_threshold=samples_info["change_threshold"],
+            )
+        except (KeyError, NameError):
+            auto_correlation_settings = None
+
+        return cls(
+            model=model,
+            sample_list=sample_list,
+            samples_info=samples_info,
+            search_internal=search_internal,
+            auto_correlation_settings=auto_correlation_settings,
+            derived_quantities_list=derived_quantities_list,
+        )
+
     @property
     def pdf_converged(self):
         """
diff --git a/autofit/non_linear/samples/nest.py b/autofit/non_linear/samples/nest.py
index 988f0f3dd..c00e0a265 100644
--- a/autofit/non_linear/samples/nest.py
+++ b/autofit/non_linear/samples/nest.py
@@ -17,6 +17,7 @@ class SamplesNest(SamplesPDF):
         sample_list: List[Sample],
         samples_info: Optional[Dict] = None,
         search_internal: Optional = None,
+        derived_quantities_list: Optional[List] = None,
     ):
         """
         The `Samples` classes in **PyAutoFit** provide an interface between the search_internal of
@@ -49,6 +50,7 @@ class SamplesNest(SamplesPDF):
             sample_list=sample_list,
             samples_info=samples_info,
             search_internal=search_internal,
+            derived_quantities_list=derived_quantities_list,
         )
 
     def __add__(self, other: "SamplesNest") -> "SamplesNest":
diff --git a/autofit/non_linear/samples/pdf.py b/autofit/non_linear/samples/pdf.py
index 08bf8d44b..375bf78f9 100644
--- a/autofit/non_linear/samples/pdf.py
+++ b/autofit/non_linear/samples/pdf.py
@@ -21,6 +21,7 @@ class SamplesPDF(Samples):
         sample_list: List[Sample],
         samples_info: Optional[Dict] = None,
         search_internal: Optional = None,
+        derived_quantities_list: Optional[List] = None,
     ):
         """
         The `Samples` classes in **PyAutoFit** provide an interface between the search_internal of
@@ -53,6 +54,7 @@ class SamplesPDF(Samples):
             sample_list=sample_list,
             samples_info=samples_info,
             search_internal=search_internal,
+            derived_quantities_list=derived_quantities_list,
         )
 
     def summary(self):
diff --git a/autofit/non_linear/samples/sample.py b/autofit/non_linear/samples/sample.py
index 45bf67092..4a38affc7 100644
--- a/autofit/non_linear/samples/sample.py
+++ b/autofit/non_linear/samples/sample.py
@@ -38,6 +38,7 @@ class Sample:
             tuple(key.split(".")) if isinstance(key, str) and "." in key else key: value
             for key, value in (kwargs or dict()).items()
         }
+        self.derived_quantities = {}
 
     def dict(self):
         return {
diff --git a/autofit/non_linear/samples/samples.py b/autofit/non_linear/samples/samples.py
index 8c71d53c1..167ff417c 100644
--- a/autofit/non_linear/samples/samples.py
+++ b/autofit/non_linear/samples/samples.py
@@ -7,8 +7,8 @@ from typing import Dict, List, Optional, Tuple, Union
 
 import numpy as np
 
+from autoconf import cached_property
 from autofit import exc
-from autofit.non_linear.search.mcmc.auto_correlations import AutoCorrelationsSettings
 from autofit.mapper.model import ModelInstance
 from autofit.mapper.prior_model.abstract import AbstractPriorModel
 from autofit.non_linear.samples.sample import Sample
@@ -25,6 +25,7 @@ class Samples(SamplesInterface, ABC):
         sample_list: List[Sample],
         samples_info: Optional[Dict] = None,
         search_internal: Optional = None,
+        derived_quantities_list: Optional[List] = None,
     ):
         """
         The `Samples` classes in **PyAutoFit** provide an interface between the search_internal of
@@ -57,6 +58,31 @@ class Samples(SamplesInterface, ABC):
         self.sample_list = sample_list
         self.samples_info = samples_info
         self.search_internal = search_internal
+        self._derived_quantities_list = derived_quantities_list
+
+    @cached_property
+    def parameters_derived_map(
+        self,
+    ) -> Optional[Dict[Tuple[float], Dict[Tuple[str, ...], float]]]:
+        """
+        Maps unique sets of parameters to dictionaries of derived quantities.
+
+        This is used to associated derived quantities with instances efficiently.
+        """
+        if self._derived_quantities_list is None:
+            return None
+        return {
+            tuple(parameters): derived_quantities
+            for parameters, derived_quantities in zip(
+                self.parameter_lists, self.derived_quantities_list
+            )
+        }
+
+    def __str__(self):
+        return f"{self.__class__.__name__}({len(self.sample_list)})"
+
+    def __repr__(self):
+        return str(self)
 
     @property
     def instances(self):
@@ -67,7 +93,8 @@ class Samples(SamplesInterface, ABC):
             self.model.instance_from_vector(
                 sample.parameter_lists_for_paths(
                     self.paths if sample.is_path_kwargs else self.names
-                )
+                ),
+                ignore_prior_limits=True
             )
             for sample in self.sample_list
         ]
@@ -113,7 +140,11 @@ class Samples(SamplesInterface, ABC):
         """
         The derived quantities of the model for each sample
         """
-        return self.derived_quantities_for_instances(self.instances)
+        if self._derived_quantities_list is None:
+            self._derived_quantities_list = self.derived_quantities_for_instances(
+                self.instances
+            )
+        return self._derived_quantities_list
 
     @property
     def derived_quantities_summary_dict(self) -> dict:
@@ -183,32 +214,15 @@ class Samples(SamplesInterface, ABC):
         samples_info,
         model: AbstractPriorModel,
         search_internal=None,
+        derived_quantities_list=None,
     ):
-        try:
-            auto_correlation_settings = AutoCorrelationsSettings(
-                check_for_convergence=True,
-                check_size=samples_info["check_size"],
-                required_length=samples_info["required_length"],
-                change_threshold=samples_info["change_threshold"],
-            )
-        except (KeyError, NameError):
-            auto_correlation_settings = None
-
-        try:
-            return cls(
-                model=model,
-                sample_list=sample_list,
-                samples_info=samples_info,
-                search_internal=search_internal,
-                auto_correlation_settings=auto_correlation_settings,
-            )
-        except TypeError:
-            return cls(
-                model=model,
-                sample_list=sample_list,
-                samples_info=samples_info,
-                search_internal=search_internal,
-            )
+        return cls(
+            model=model,
+            sample_list=sample_list,
+            samples_info=samples_info,
+            search_internal=search_internal,
+            derived_quantities_list=derived_quantities_list,
+        )
 
     def summary(self):
         return SamplesSummary(


Test patch:
diff --git a/test_autofit/aggregator/search_output/.completed b/test_autofit/aggregator/search_output/.completed
new file mode 100644
index 000000000..e69de29bb
diff --git a/test_autofit/aggregator/search_output/files/samples_info.json b/test_autofit/aggregator/search_output/files/samples_info.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/test_autofit/aggregator/search_output/files/samples_info.json
@@ -0,0 +1,1 @@
+{}
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/.identifier b/test_autofit/aggregator/search_output_derived/.identifier
new file mode 100644
index 000000000..b3c9a1831
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/.identifier
@@ -0,0 +1,24 @@
+Emcee
+nwalkers
+50
+Model
+cls
+autofit.example.model.Gaussian
+centre
+UniformPrior
+lower_limit
+0.0
+upper_limit
+100.0
+normalization
+LogUniformPrior
+lower_limit
+1e-06
+upper_limit
+1000000.0
+sigma
+UniformPrior
+lower_limit
+0.0
+upper_limit
+25.0
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/derived.results b/test_autofit/aggregator/search_output_derived/derived.results
new file mode 100644
index 000000000..24d20182c
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/derived.results
@@ -0,0 +1,13 @@
+Maximum Log Likelihood Model:
+
+fwhm                                                                            23.805
+
+
+Summary (3.0 sigma limits):
+
+fwhm                                                                            23.4626 (22.6460, 24.4424)
+
+
+Summary (1.0 sigma limits):
+
+fwhm                                                                            23.4626 (23.3095, 23.6148)
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/files/covariance.csv b/test_autofit/aggregator/search_output_derived/files/covariance.csv
new file mode 100644
index 000000000..aa7f9abef
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/files/covariance.csv
@@ -0,0 +1,3 @@
+1.754680576242605150e-02,2.350824293592921623e-04,1.108470354034918288e-04
+2.350824293592921623e-04,8.551239156803223940e-02,2.220791295071217544e-02
+1.108470354034918288e-04,2.220791295071217544e-02,1.832802489171495713e-02
diff --git a/test_autofit/aggregator/search_output_derived/files/derived_quantities.csv b/test_autofit/aggregator/search_output_derived/files/derived_quantities.csv
new file mode 100644
index 000000000..85435c133
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/files/derived_quantities.csv
@@ -0,0 +1,2 @@
+              fwhm
+10
diff --git a/test_autofit/aggregator/search_output_derived/files/derived_summary.json b/test_autofit/aggregator/search_output_derived/files/derived_summary.json
new file mode 100644
index 000000000..9142156bc
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/files/derived_summary.json
@@ -0,0 +1,5 @@
+{
+    "max_log_likelihood_sample": {
+        "fwhm": 23.805411127235168
+    }
+}
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/files/model.json b/test_autofit/aggregator/search_output_derived/files/model.json
new file mode 100644
index 000000000..742e4fe30
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/files/model.json
@@ -0,0 +1,24 @@
+{
+    "class_path": "autofit.example.model.Gaussian",
+    "type": "model",
+    "arguments": {
+        "centre": {
+            "lower_limit": 0.0,
+            "upper_limit": 100.0,
+            "type": "Uniform",
+            "id": 6
+        },
+        "normalization": {
+            "lower_limit": 1e-06,
+            "upper_limit": 1000000.0,
+            "type": "LogUniform",
+            "id": 7
+        },
+        "sigma": {
+            "lower_limit": 0.0,
+            "upper_limit": 25.0,
+            "type": "Uniform",
+            "id": 8
+        }
+    }
+}
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/files/model.paramnames b/test_autofit/aggregator/search_output_derived/files/model.paramnames
new file mode 100644
index 000000000..e16f0ebf4
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/files/model.paramnames
@@ -0,0 +1,3 @@
+centre                                                                x_g
+normalization                                                         norm_g
+sigma                                                                 \sigma_g
diff --git a/test_autofit/aggregator/search_output_derived/files/samples.csv b/test_autofit/aggregator/search_output_derived/files/samples.csv
new file mode 100644
index 000000000..abd8eb144
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/files/samples.csv
@@ -0,0 +1,2 @@
+            centre,     normalization,             sigma,    log_likelihood,           log_prior,     log_posterior,weight
+ 49.89547152732197,  25.0243143120209,  9.82192444968168,  182.126490185352, 0.03996113489989339, 182.1664513202519,   1.0
diff --git a/test_autofit/aggregator/search_output_derived/files/samples_info.json b/test_autofit/aggregator/search_output_derived/files/samples_info.json
new file mode 100644
index 000000000..c6a32a564
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/files/samples_info.json
@@ -0,0 +1,8 @@
+{
+    "check_size": 100,
+    "required_length": 50,
+    "change_threshold": 0.01,
+    "total_walkers": 50,
+    "total_steps": 1500,
+    "time": "5.6052281856536865"
+}
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/files/samples_summary.json b/test_autofit/aggregator/search_output_derived/files/samples_summary.json
new file mode 100644
index 000000000..cd4010cb2
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/files/samples_summary.json
@@ -0,0 +1,69 @@
+{
+    "type": "instance",
+    "class_path": "autofit.non_linear.samples.summary.SamplesSummary",
+    "arguments": {
+        "log_evidence": null,
+        "covariance_matrix": {
+            "type": "ndarray",
+            "array": [
+                [
+                    0.01754680576242605,
+                    0.00023508242935929216,
+                    0.00011084703540349183
+                ],
+                [
+                    0.00023508242935929216,
+                    0.08551239156803224,
+                    0.022207912950712175
+                ],
+                [
+                    0.00011084703540349183,
+                    0.022207912950712175,
+                    0.018328024891714957
+                ]
+            ],
+            "dtype": "float64"
+        },
+        "model": {
+            "class_path": "autofit.example.model.Gaussian",
+            "type": "model",
+            "arguments": {
+                "centre": {
+                    "lower_limit": 0.0,
+                    "upper_limit": 100.0,
+                    "type": "Uniform",
+                    "id": 6
+                },
+                "normalization": {
+                    "lower_limit": 1e-06,
+                    "upper_limit": 1000000.0,
+                    "type": "LogUniform",
+                    "id": 7
+                },
+                "sigma": {
+                    "lower_limit": 0.0,
+                    "upper_limit": 25.0,
+                    "type": "Uniform",
+                    "id": 8
+                }
+            }
+        },
+        "max_log_likelihood_sample": {
+            "type": "instance",
+            "class_path": "autofit.non_linear.samples.sample.Sample",
+            "arguments": {
+                "log_likelihood": 182.5127553145817,
+                "log_prior": 0.03962272827953348,
+                "weight": 1.0,
+                "kwargs": {
+                    "type": "dict",
+                    "arguments": {
+                        "centre": 50.08256853054789,
+                        "normalization": 25.238039969007758,
+                        "sigma": 10.109227317589907
+                    }
+                }
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/files/search.json b/test_autofit/aggregator/search_output_derived/files/search.json
new file mode 100644
index 000000000..9b9a2c1fb
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/files/search.json
@@ -0,0 +1,34 @@
+{
+    "type": "instance",
+    "class_path": "autofit.non_linear.search.mcmc.emcee.search.Emcee",
+    "arguments": {
+        "path_prefix": {
+            "type": "instance",
+            "class_path": "pathlib.PosixPath",
+            "arguments": {}
+        },
+        "unique_tag": null,
+        "number_of_cores": 1,
+        "nwalkers": 50,
+        "auto_correlation_settings": {
+            "type": "instance",
+            "class_path": "autofit.non_linear.search.mcmc.auto_correlations.AutoCorrelationsSettings",
+            "arguments": {
+                "change_threshold": 0.01,
+                "required_length": 50,
+                "check_size": 100,
+                "check_for_convergence": true
+            }
+        },
+        "initializer": {
+            "type": "instance",
+            "class_path": "autofit.non_linear.initializer.InitializerBall",
+            "arguments": {
+                "upper_limit": 0.51,
+                "lower_limit": 0.49
+            }
+        },
+        "name": "tutorial_3_non_linear_search",
+        "iterations_per_update": 500
+    }
+}
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/metadata b/test_autofit/aggregator/search_output_derived/metadata
new file mode 100644
index 000000000..de64cba2f
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/metadata
@@ -0,0 +1,3 @@
+name=tutorial_3_non_linear_search
+non_linear_search=emcee
+            
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/model.info b/test_autofit/aggregator/search_output_derived/model.info
new file mode 100644
index 000000000..42fea2ac8
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/model.info
@@ -0,0 +1,7 @@
+Total Free Parameters = 3
+
+model                                                                           Gaussian (N=3)
+
+centre                                                                          UniformPrior [6], lower_limit = 0.0, upper_limit = 100.0
+normalization                                                                   LogUniformPrior [7], lower_limit = 1e-06, upper_limit = 1000000.0
+sigma                                                                           UniformPrior [8], lower_limit = 0.0, upper_limit = 25.0
\ No newline at end of file
diff --git a/test_autofit/aggregator/search_output_derived/model.results b/test_autofit/aggregator/search_output_derived/model.results
new file mode 100644
index 000000000..7762e3d81
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/model.results
@@ -0,0 +1,27 @@
+Maximum Log Likelihood                                                          182.51275531
+Maximum Log Posterior                                                           182.55237804
+
+model                                                                           Gaussian (N=3)
+
+Maximum Log Likelihood Model:
+
+centre                                                                          50.083
+normalization                                                                   25.238
+sigma                                                                           10.109
+
+
+Summary (3.0 sigma limits):
+
+centre                                                                          50.04 (49.65, 50.40)
+normalization                                                                   24.90 (24.07, 25.72)
+sigma                                                                           9.96 (9.62, 10.38)
+
+
+Summary (1.0 sigma limits):
+
+centre                                                                          50.04 (49.98, 50.11)
+normalization                                                                   24.90 (24.75, 25.03)
+sigma                                                                           9.96 (9.90, 10.03)
+
+instances
+
diff --git a/test_autofit/aggregator/search_output_derived/search.summary b/test_autofit/aggregator/search_output_derived/search.summary
new file mode 100644
index 000000000..59fef0387
--- /dev/null
+++ b/test_autofit/aggregator/search_output_derived/search.summary
@@ -0,0 +1,6 @@
+Total Samples = 2650
+Time To Run = 0:00:05.605228
+Time Per Sample (seconds) = 0.0021151804474164853
+Log Likelihood Function Evaluation Time (seconds) = 1.4066696166992188e-05
+Expected Time To Run (seconds) = 0:00:00.037277
+Speed Up Factor (e.g. due to parallelization) = 0.006650398300538183
\ No newline at end of file
diff --git a/test_autofit/aggregator/test_child_analysis.py b/test_autofit/aggregator/test_child_analysis.py
index 28f919164..3fa5ba9c2 100644
--- a/test_autofit/aggregator/test_child_analysis.py
+++ b/test_autofit/aggregator/test_child_analysis.py
@@ -31,7 +31,10 @@ def test_child_analysis_pickles(child_analyses):
 
 
 def test_child_analysis_values(directory):
-    aggregator = Aggregator.from_directory(directory)
+    aggregator = Aggregator.from_directory(
+        directory,
+        completed_only=True,
+    )
 
     assert list(aggregator.child_values("example")) == [["hello world", "hello world"]]
     assert list(aggregator)[0].child_values("example") == ["hello world", "hello world"]
@@ -40,7 +43,10 @@ def test_child_analysis_values(directory):
 @pytest.fixture(name="aggregator")
 def make_aggregator(session, directory):
     aggregator = af.Aggregator(session)
-    aggregator.add_directory(directory)
+    aggregator.add_directory(
+        directory,
+        completed_only=True,
+    )
     return aggregator
 
 
diff --git a/test_autofit/aggregator/test_derived_quantities.py b/test_autofit/aggregator/test_derived_quantities.py
new file mode 100644
index 000000000..ef5964f6a
--- /dev/null
+++ b/test_autofit/aggregator/test_derived_quantities.py
@@ -0,0 +1,13 @@
+import pytest
+
+from autofit import SearchOutput
+
+
+@pytest.fixture(name="search_output")
+def make_search_output(directory):
+    return SearchOutput(directory / "search_output_derived")
+
+
+def test(search_output):
+    samples = search_output.samples
+    assert samples.instance.fwhm == 10.0
diff --git a/test_autofit/aggregator/test_reference.py b/test_autofit/aggregator/test_reference.py
index 0ff155d24..5b0141973 100644
--- a/test_autofit/aggregator/test_reference.py
+++ b/test_autofit/aggregator/test_reference.py
@@ -48,6 +48,7 @@ def database_aggregator(
     aggregator.add_directory(
         directory,
         reference={"": get_class_path(af.Exponential)},
+        completed_only=True,
     )
 
     return aggregator
@@ -85,7 +86,7 @@ def test_database_info(
     assert (
         (output_directory / "database.info").read_text()
         == """                         unique_id,name,unique_tag,total_free_parameters,is_complete
-  c4bf344d706947aa66b129ed2e05e1bd,    ,          ,                    4,      False
+  c4bf344d706947aa66b129ed2e05e1bd,    ,          ,                    4,       True
 c4bf344d706947aa66b129ed2e05e1bd_0,    ,          ,                    0,           
 c4bf344d706947aa66b129ed2e05e1bd_1,    ,          ,                    0,           
 """
diff --git a/test_autofit/mapper/model/test_derived_quantities.py b/test_autofit/mapper/model/test_derived_quantities.py
index e3aea925e..61ab7e719 100644
--- a/test_autofit/mapper/model/test_derived_quantities.py
+++ b/test_autofit/mapper/model/test_derived_quantities.py
@@ -9,16 +9,12 @@ from autofit.text.text_util import derived_info_from
 def test_derived_quantities():
     gaussian = af.Gaussian()
 
-    assert gaussian.upper_bound == 0.05
-
-    gaussian.upper_bound = 0.1
-    assert gaussian.upper_bound == 0.1
+    assert gaussian.fwhm == 0.023548200450309493
 
 
 def test_model_derived_quantities(model):
     assert set(model.derived_quantities) == {
-        ("upper_bound",),
-        ("lower_bound",),
+        ("fwhm",),
     }
 
 
@@ -29,10 +25,8 @@ def test_embedded_derived_quantities():
     )
 
     assert set(collection.derived_quantities) == {
-        ("one", "upper_bound"),
-        ("one", "lower_bound"),
-        ("two", "upper_bound"),
-        ("two", "lower_bound"),
+        ("one", "fwhm"),
+        ("two", "fwhm"),
     }
 
 
@@ -45,10 +39,8 @@ def test_multiple_levels():
     )
 
     assert set(collection.derived_quantities) == {
-        ("one", "upper_bound"),
-        ("one", "lower_bound"),
-        ("two", "three", "upper_bound"),
-        ("two", "three", "lower_bound"),
+        ("one", "fwhm"),
+        ("two", "three", "fwhm"),
     }
 
 
@@ -78,7 +70,7 @@ def make_samples(model):
 
 def test_samples(samples):
     derived_quantities = samples.derived_quantities_list[0]
-    assert derived_quantities == [-5.0, 5.0]
+    assert derived_quantities == [2.3548200450309493]
 
 
 def test_persist(samples, model):
@@ -93,7 +85,7 @@ def test_persist_database(samples, model, session):
     paths.model = model
     paths.save_derived_quantities(samples)
 
-    assert paths.fit["derived_quantities"].shape == (1, 2)
+    assert paths.fit["derived_quantities"].shape == (1, 1)
 
 
 def test_summary(samples):
@@ -103,8 +95,7 @@ def test_summary(samples):
 
 Summary (3.0 sigma limits):
 
-lower_bound        -5.0000 (-5.0000, -5.0000)
-upper_bound        5.0000 (5.0000, 5.0000)"""
+fwhm        2.3548 (2.3548, 2.3548)"""
     )
 
 
@@ -113,8 +104,7 @@ def test_derived_info_from(samples):
         derived_info_from(samples)
         == """Maximum Log Likelihood Model:
 
-lower_bound                                                                     -5.000
-upper_bound                                                                     5.000
+fwhm                                                                            2.355
 
  WARNING: The samples have not converged enough to compute a PDF and model errors. 
  The model below over estimates errors. 
@@ -123,15 +113,13 @@ upper_bound
 
 Summary (1.0 sigma limits):
 
-lower_bound                                                                     -5.0000 (-5.0000, -5.0000)
-upper_bound                                                                     5.0000 (5.0000, 5.0000)"""
+fwhm                                                                            2.3548 (2.3548, 2.3548)"""
     )
 
 
 def test_derived_quantities_summary_dict(samples):
     assert samples.derived_quantities_summary_dict == {
         "max_log_likelihood_sample": {
-            "lower_bound": -5.0,
-            "upper_bound": 5.0,
+            "fwhm": 2.3548200450309493,
         },
     }