Skip to content

Commit

Permalink
better control of DOFs and objectives
Browse files Browse the repository at this point in the history
  • Loading branch information
Thomas Morris committed Apr 15, 2024
1 parent e143ff7 commit 8e8917b
Show file tree
Hide file tree
Showing 10 changed files with 616 additions and 286 deletions.
30 changes: 28 additions & 2 deletions docs/source/dofs.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
Degrees of freedom (DOFs)
+++++++++++++++++++++++++

Continuous degrees of freedom
-----------------------------

A degree of freedom is a variable that affects our optimization objective. We can define a simple DOF as

.. code-block:: python
Expand All @@ -27,7 +30,30 @@ In this case, we can define a read-only DOF as
from blop import DOF
dof = DOF(device=a_read_only_ophyd_device, description="a thermometer or something", read_only=True, trust_bounds=(lower, upper))
dof = DOF(device=a_read_only_ophyd_device, description="a thermometer or something", read_only=True, trust_domain=(lower, upper))
and the agent will use the received values to model its objective, but won't try to move it.
We can also pass a set of ``trust_bounds``, so that our agent will ignore experiments where the DOF value jumps outside of the interval.
We can also pass a set of ``trust_domain``, so that our agent will ignore experiments where the DOF value jumps outside of the interval.


Discrete degrees of freedom
---------------------------

In addition to degrees of freedom that vary continuously between a lower and upper bound, we can define discrete degrees of freedom.
One kind is a binary degree of freedom, where the input can take one of two values, e.g.

.. code-block:: python
discrete_dof = DOF(name="x1", description="A discrete DOF", type="binary", search_domain={"in", "out"})
Another is an ordinal degree of freedom, which takes more than two discrete values but has some ordering, e.g.

.. code-block:: python
ordinal_dof = DOF(name="x1", description="An ordinal DOF", type="ordinal", search_domain={"low", "medium", "high"})
The last is a categorical degree of freedom, which can take many different discrete values with no ordering, e.g.

.. code-block:: python
categorical_dof = DOF(name="x1", description="A categorical DOF", type="categorical", search_domain={"banana", "mango", "papaya"})
80 changes: 72 additions & 8 deletions docs/source/objectives.rst
Original file line number Diff line number Diff line change
@@ -1,25 +1,40 @@
Objectives
++++++++++

We can describe an optimization problem as a list of objectives to. A simple objective is
Objectives are what control how optimal the output of our experiment is, and are defined by ``Objective`` objects.

``blop`` combines one or many ``Objective`` objects into an ``ObjectiveList``, which encapsulates how we model and optimize our outputs.

Fitness
-------

A fitness objective is an ``Objective`` that minimizes or maximizes a given value.

* Maximize the flux of a beam of light.
* Minimize the size of a beam.

We can construct an objective to maximize some output with

.. code-block:: python
from blop import Objective
objective = Objective(name="y1", target="max")
objective = Objective(name="some_output", target="max") # or "min"
Given some data, the ``Objective`` object will try to model the quantity "y1" and find the corresponding inputs that maximize it.
The objective will expect that this quantity will be spit out by the experimentation loop, so we will check later that it is set up correctly.
There are many ways to specify an objective's behavior, which is done by changing the objective's target:
Given some data, the ``Objective`` object will try to model the quantity ``y1`` and find the corresponding inputs that maximize it.
We can also apply a transform to the value to make it more Gaussian when we fit to it.
This is especially useful when the quantity tends to be non-Gaussian, like with a beam flux.

.. code-block:: python
from blop import Objective
objective = Objective(name="y1", target="min") # minimize the quantity "y1"
objective = Objective(name="some_output", target="max", transform="log")
objective = Objective(name="some_output", target="max", transform="arctanh")
objective = Objective(name="y1", target=2) # get the quantity "y1" as close to 2 as possible
.. code-block:: python
objective = Objective(name="y1", target=(1, 3)) # find any input that puts "y1" between 1 and 3.
Expand All @@ -31,4 +46,53 @@ In this case, a smart thing to do is to set ``log=True``, which will model and s
from blop import Objective
objective = Objective(name="some_strictly_positive_quantity", target="max", log=True)
objective = Objective(name="some_strictly_positive_output", target="max", log=True)
Constraints
-----------

A constraint objective doesn't try to minimize or maximize a value, and instead just tries to maximize the chance that the objective is within some acceptable range.
This is useful in optimization problems like

* Require a beam to be within some box.
* Require the wavelength of light to be a certain color.
* We want a beam to be focused enough to perform some experiment, but not necessarily optimal.

.. code-block:: python
# ensure the color is approximately green
objective = Objective(name="peak_wavelength", target=(520, 530), units="nm")
# ensure the beam is smaller than 10 microns
objective = Objective(name="beam_width", target=(-np.inf, 10), units="um", transform="log")
# ensure our flux is at least some value
objective = Objective(name="beam_flux", target=(1.0, np.inf), transform="log")
Validity
--------

A common problem in beamline optimization is in the random or systematically invalid measurement of objectives. This arises in different ways, like when

* The beam misses the detector, leading our beam parser to return some absurdly small or large value.
* Some part of the experiment glitches, leading to an uncharacteristic data point.
* Some part of the data postprocessing pipeline fails, giving no value for the output.

We obviously want to exclude these points from our model fitting, but if we stopped there, inputs that always lead to invalid outputs will lead to an infinite loop of trying to sample an interesting but invalid points (as the points get immediately removed every time).
The set of points that border valid and invalid data points are often highly nonlinear and unknown *a priori*.
We solve this by implementing a validity model for each ``Objective``, which constructs and fits a probabilistic model for validity for all inputs.
Using this model, we constrain acquisition functions to take into account the possibility that the output value is invalid, meaning it will eventually learn to ignore infeasible points.

We can control the exclusion of data points in two ways. The first is to specify a ``trust_domain`` for the objective, so that the model only "trusts" points in that domain. For example:

.. code-block:: python
# any beam smaller than two pixels shouldn't be trusted.
# any beam larger than 100 pixels will mess up our model and aren't interesting anyway
objective = Objective(name="beam_size", trust_domain=(2, 100), units="pixels")
This will set any value outside of the ``trust_domain`` to ``NaN``, which the model will learn to avoid.
The second way is to ensure that any invalid values are converted to ``NaN`` in the diagnostics, before the agent ever sees them.
Loading

0 comments on commit 8e8917b

Please sign in to comment.