Skip to content

Commit 9652c31

Browse files
committed
Add context manager to optimization limits
1 parent 505c448 commit 9652c31

5 files changed

Lines changed: 359 additions & 39 deletions

File tree

‎H2MM_C/H2MM_C.pyx‎

Lines changed: 120 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ from libc.stdint cimport int64_t, int32_t, uint8_t, INT32_MAX
1616
from libc.string cimport memcpy
1717
from cpython cimport array
1818
cimport numpy as cnp
19+
import contextlib
1920

2021
import os
2122
import numpy as np
@@ -446,6 +447,8 @@ cdef class opt_lim_const:
446447
The optimization_limits module variable should be of this class
447448
448449
"""
450+
_attrs = ('max_iter', 'max_time', 'converged_min', 'num_cores', 'squarem',
451+
'formatter', 'outstream')
449452
cdef:
450453
int64_t _max_iter
451454
double _max_time
@@ -454,6 +457,7 @@ cdef class opt_lim_const:
454457
bint _squarem
455458
object _formatter
456459
object _outstream
460+
457461
def __cinit__(self, max_iter=2046, max_time=np.inf, converged_min=1e-14,
458462
num_cores=os.cpu_count()//2, squarem=False, formatter=StdPrinter,
459463
outstream=lambda: sys.stdout):
@@ -473,6 +477,7 @@ cdef class opt_lim_const:
473477
def max_iter(self):
474478
"""Maximum number of iterations before ending optimization"""
475479
return self._max_iter
480+
476481
@max_iter.setter
477482
def max_iter(self, max_iter):
478483
assert max_iter > 0 and np.issubdtype(type(max_iter), np.integer), ValueError("max_iter must be integer greater than 0")
@@ -489,6 +494,7 @@ cdef class opt_lim_const:
489494
490495
"""
491496
return self._max_time
497+
492498
@max_time.setter
493499
def max_time(self, max_time):
494500
assert max_time > 0 and np.issubdtype(type(max_time), np.floating), ValueError("max_time must be float and greater than 0")
@@ -507,7 +513,8 @@ cdef class opt_lim_const:
507513

508514
@property
509515
def num_cores(self):
510-
"""Number of cores to use in optimization
516+
"""
517+
Number of cores to use in optimization
511518
512519
.. note::
513520
@@ -561,7 +568,7 @@ cdef class opt_lim_const:
561568

562569
def __repr__(self):
563570
return (f'Optimization limits:: num_cores: {self._num_cores}, max_iter: {self._max_iter}, '
564-
f'converged_min: {self._converged_min}, max_time: {self._max_time},\n'
571+
f'converged_min: {self._converged_min}, max_time: {self._max_time},squarem={self._squarem}\n'
565572
f'formatter: {self._formatter.__qualname__}, outstream={self._outstream}')
566573

567574
def _get_max_iter(self, max_iter):
@@ -722,25 +729,132 @@ cdef class opt_lim_const:
722729
Parameters
723730
----------
724731
outstream : Output Stream
725-
Stream .
732+
Stream to assign as outptu stream.
726733
727734
Returns
728735
-------
729736
OutputStream
730-
DESCRIPTION.
737+
default output stream object.
731738
732739
"""
733740
if outstream is None:
734741
return self._outstream()
735742
return outstream
743+
744+
def __call__(self, **kwargs):
745+
"""
746+
Open a context manager, all keyword arguments must be valid attributes
747+
of optimization_limits. Values set in kwargs, or to optimization limits
748+
within the ``with`` block will be reset upon exciting the ``with`` block.
749+
750+
This can be used as follows::
751+
752+
with hm.optimization_limits(converged_min=1e-5):
753+
out = hm.EM_H2MM_C(init, index, times)
754+
755+
756+
Parameters
757+
----------
758+
max_iter : int, optional
759+
Maximum number of iterations before ending optimization
760+
max_time : float, optional
761+
Maximum time of optimization.
762+
converged_min : float, optional
763+
Minimum difference between loglik of succesive iterations for a
764+
model to be considered converged
765+
num_cores : int, optional
766+
Number of cores to use in optimization.
767+
squarem : bool, optional
768+
Choice of whether or not to use SQUAREM (|Varadhan2008|) method of
769+
accelerating optimization using quadratic projection of 3 successive
770+
models.
771+
formatter : Printer
772+
A :class:`Printer` subclass, used to format result of print_func
773+
for display of optimiation progress
774+
outstream : Callable, optional
775+
Callable, when called, generates an output stream for where to
776+
print displayed progress
777+
778+
Returns
779+
-------
780+
OptlimContext
781+
Special context manager class for managing :class:opt_lim_const`
782+
This class only defines the ``__init__``, ``__enter__`` and ``__exit__``
783+
magic methods for use in with statemetns
784+
"""
785+
for key in kwargs.keys():
786+
if key not in self._attrs:
787+
raise TypeError(f"{key} is not a valid keyword argument for context manager")
788+
cntx = OptlimContext(self)
789+
for key, value in kwargs.items():
790+
if value is None:
791+
continue
792+
try:
793+
self[key] = value
794+
except Exception as e:
795+
print("exception raised")
796+
for k, v in cntx._values.items():
797+
self[key] = value
798+
raise e
799+
return cntx
800+
801+
802+
cdef class OptlimContext:
803+
"""
804+
Context manager class, upon enter, records the values of ``optlim`` and
805+
restores upont exit
806+
807+
Parameters
808+
----------
809+
optlim : opt_lim_const
810+
The optimization_limits to restore after exiting the context manager.
811+
"""
812+
cdef:
813+
opt_lim_const _optlim
814+
dict _values
815+
816+
def __cinit__(self, opt_lim_const optlim):
817+
self._optlim = optlim
818+
self._values = {key:optlim[key] for key in opt_lim_const._attrs}
819+
820+
def __enter__(self):
821+
return self._optlim
822+
823+
def __exit__(self, exc_type, exc_value, traceback):
824+
for key, value in self._values.items():
825+
self._optlim[key] = value
826+
736827

737828
def get_stdout():
738829
"""Simple callable returns sys.stdout"""
739830
return sys.stdout
740831

741-
#: Like rcParams, a place to set defaults for optimization limits
832+
#: This module variable stores default values for optimization limits.
833+
#: if the values are not supplied as keyword arguments to the optimization functions,
834+
#: ``H2MM_C`` will fall back to the values stored in this variable.
835+
#: The options can be accessed either as attributions or keys.
836+
#:
837+
#: Options are:
838+
#: - max_iter: maximum number of iterations to perform during optimization
839+
#: - max_time: maximum
840+
#: - converged_min: difference between loglik of iteration at which to terminate optimization
841+
#: (if difference is less than this value, the model is considered optimized)
842+
#: - num_cores: number of cores to use in optimization
843+
#: - squarem: Whether or not to use squarem acceleration
844+
#: - formatter: printer object, defines how to format optimization progress
845+
#: - outstream: output stream buffer
846+
#:
847+
#:
848+
#: Support context manager to temporarily set a value:
849+
#:
850+
#: .. code-block::
851+
#:
852+
#: with hm.optimization_limits(max_iter=1000):
853+
#: model = hm.EM_H2MM_C(model, index, time)
854+
#:
742855
optimization_limits = opt_lim_const(max_iter=2046, max_time=np.inf,
743856
converged_min=1e-14, num_cores=os.cpu_count()//2,
857+
squarem=False,
744858
formatter=StdPrinter, outstream=get_stdout)
745859

746860
cdef class ConvCodes:
@@ -1715,7 +1829,7 @@ cdef class h2mm_model:
17151829
print_stream=print_stream, print_formatter=print_formatter,
17161830
print_fmt_args=print_fmt_args, print_fmt_kwargs=print_fmt_kwargs,
17171831
max_time=max_time, converged_min=converged_min, num_cores=num_cores,
1718-
reset_niter=reset_niter, ll=ll, gamma=gamma, opt_array=opt_array)
1832+
reset_niter=reset_niter, ll=ll, gamma=gamma, opt_array=opt_array, squarem=squarem)
17191833
if inplace:
17201834
# separate the models from ll_array / gamma
17211835
if gamma or ll:

‎docs/source/ChangeNotes.rst‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,18 @@ Change Notes
33

44
.. currentmodule:: H2MM_C
55

6+
Changed in Version 2.3.1
7+
------------------------
8+
9+
The main addition in v2.3.1 is the context manager for optimization limits
10+
11+
- Added:
12+
- Optimization limits context manager. :attr:`H2MM_C.optimization_limits`` can now be called in a with block to
13+
modify the limits only withing the with block. This also added the :class:`H2MM_C.OptlimContext`class to support it.
14+
15+
- Fixed:
16+
- ``squarem`` keyword argument not passed in :meth:`H2MM_C.h2mm_model.optimize` this has now been fixed
17+
618

719
Changed in Version 2.3.0
820
------------------------

‎docs/source/SecondaryControl.rst‎

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ First, our obligatory imports and loading of 3 detector data:
3535
3636
color3, times3 = load_txtdata('sample_data_3det.txt')
3737
38+
3839
Optimization Control
3940
--------------------
4041

@@ -263,4 +264,142 @@ This is useful if you want to save your data for later:
263264
| 0.840976670297041, 0.07645342127367338, 0.08256990842928548
264265
265266

267+
New in v2.3
268+
-----------
269+
270+
SQUAREM acceleration
271+
********************
272+
273+
The primary addition going from v2.2 to v2.3 is SQUAREM acceleration
274+
for the expectation-maximization (EM) aspect of |H2MM| algorithm.
275+
276+
Many thanks to Thomas Peulen for recomending this method.
277+
278+
279+
The method is based on the pseudo-code of |Du2018|,
280+
which was derived from ideas in |Varadhan2008|.
281+
Note that all implementation was done at the C level.
282+
283+
SQUAREM expands/modifies the normal expectation-maximization loop.
284+
285+
We will represent a model with :math:`\theta`.
286+
In |H2MM|, there are several sub-steps.
287+
Usally refered two as the forward-back algorithm,
288+
The loglikelihood of a model :math:`\theta` is calculated,
289+
and also computes a model that is guaunteed to have a better loglikelihood
290+
(unless the optimal model has already been found, also subject to floating point errors).
291+
This algorithm has two phases, the $fwd$ phase which computes that loglikelihood,
292+
and the :math:`bck` phase, (which relies on an array computed in :math:`fwd` ),
293+
which computes the improved model.
294+
295+
The standard EM loop for |H2MM| uses the loop below:
296+
297+
-------
298+
299+
while :math:`ll(\theta_{n-1} < ll(\theta_{n})` \:\:
300+
301+
1. Compute loglikelihood of current model :math:`ll(\theta_{n}) = fwd(\theta_{n})`
302+
2. Compute maximization step next model :math:`\theta_{n+1} = bck(\theta_{n})`
303+
304+
305+
--------
306+
307+
SQUAREM expands this loop, so that a quadractic projection of :math:`\theta` is computed based on the 3 most recent models.
308+
Then if the projected model has a better loglikelhood that the last model, use the projected model in the next round.
309+
310+
The SQUAREM optimization loop works as follows:
311+
312+
-----------
313+
314+
Initialize :math;`ll(\theta_{0}) = fwd(\theta_{0})` (compute loglikelihood of initial model)
315+
316+
while optimal model not found::
317+
1. Compute :math:`\theta_{n+1} = bck(\theta{n^{\prime}})`
318+
2. Compute :math:`ll(\theta_{n+1}) = fwd(\theta_{n+1})`
319+
3. Compute :math:`\theta_{n+2} = bck(\theta_{n+1})`
320+
4. Compute :math:`ll(\theta_{n+2}) = fwd(\theta_{n+2})`
321+
5. Compute projection using the following definitions:
322+
323+
- :math:`r = \theta_{n+1} - \theta_{n}`
324+
- :math:`b = \theta_{n+2} - \theta_{n+1}`
325+
- :math:`\alpha = - \frac{||r||}{||v||}`
326+
- :math:`\theta_{SQ} = \theta_{n} - 2\alpha r + \alpha^{2} v`
327+
- **note** it is possible for :math:`\theta_{SQ}` to have negative (impossible) values,
328+
if this occurs, skip to step 7, and assume :math:`\theta_{SQ}` has the worst possible loglikelihood
329+
330+
6. Compute :math:`ll(\theta_{SQ}) = fwd(\theta_{SQ})`
331+
7. Set :math:`\theta_{{n+2}^{\prime}}` as which model has the better loglikelihood between :math:`\theta_{n+1}` and :math:`\theta_{SQ}`
332+
333+
-------------
334+
335+
Note that after each evaluation of $fwd$, the loglik is tested for convergence.
336+
Also, between these evaluation, the models are normalized and bounded with the bounding function.
337+
338+
.. code-block::
339+
340+
imodel_4s3d = hm.factory_h2mm_model(4,3)
341+
model_4s3d = hm.EM_H2MM_C(imodel_4s3d, color3, times3, squarem=True)
342+
model_4s3d
343+
344+
| Optimization reached maximum number of iterations
345+
|
346+
| nstate: 4, ndet: 3, nphot: 436084, niter: 1000, loglik: -408233.7176734728 converged state: 0x146
347+
| prior:
348+
| 0.6252741215262306, 0.030612454324407904, 0.19173015960537806, 0.1523832645439835
349+
| trans:
350+
| 0.9057287693195645, 0.04050721299160209, 0.05376401768883339, 1.960153729459685e-38
351+
| 2.0596457882248302e-05, 0.9999671506799463, 1.0257544140118528e-05, 1.995318031340882e-06
352+
| 4.9711944981109055e-06, 6.354917051447945e-06, 0.9999787515213548, 9.922367095667794e-06
353+
| 3.0728033099149917e-06, 2.564195058738289e-06, 3.7901004759624195e-05, 0.9999564619968717
354+
| obs:
355+
| 0.6178100534248961, 0.3821899465751039, 1.0611886068674382e-22
356+
| 0.14422555137985735, 0.2894894306973517, 0.566285017922791
357+
| 0.4416303955720442, 0.08699556344822588, 0.4713740409797299
358+
| 0.8409816137668159, 0.07645524944471362, 0.08256313678847053
359+
360+
361+
A note on using ``opt_array`` with SQUAREM:
362+
363+
When using SQUAREM with ``opt_array=False`` (the default), the maximum number of iteratons and the ``hm.h2mm_model.niter`` values match,
364+
and importantly the :math:`\theta_{SQ}` models are counted as having the same number of iteration as the :math:`\theta_{n+2}` model.
365+
366+
However, when using :code:`opt_array=True` the :math:`\theta_{n+2}` and :math:`\theta_{SQ}` models are both stored.
367+
The maximum number of iterations then becomes the number of models in the output array.
368+
Therefore fewer iterations will be evalueated with :code:`opt_array=True, squarem=True` than with :math:`opt_array=False, squarem=True`.
369+
370+
371+
optimization_limits context manager
372+
***********************************
373+
374+
Similar to how in matplotlib ``rcParams`` can open a context manager,
375+
where changes inside the `with` block are restored upon exit,
376+
:attr:`H2MM_C.optimization_limits` can now also be used inside a with block.
377+
378+
Context-limited values can be set either in the kwargs of the with statement,
379+
setting :attr:`H2MM_C.optimization_limits`, or using the return value of the context manager.
380+
381+
.. code-block::
382+
383+
print(f'before opening context: {hm.optimization_limits}')
384+
with hm.optimization_limits(squarem=True) as lm:
385+
print(f'inside context: {hm.optimization_limits}')
386+
hm.optimization_limits.max_iter = 123
387+
lm.converged_min = 1e-4
388+
print(f'set values inside context: {hm.optimization_limits}')
389+
models_4s3d = hm.EM_H2MM_C(imodel_4s3d, color3, times3, opt_array=True)
390+
print(f'after context: {hm.optimization_limits}')
391+
392+
| before opening context: Optimization limits:: num_cores: 2, max_iter: 1000, converged_min: 1e-07, max_time: inf,squarem=False
393+
| formatter: StdPrinter, outstream=<cyfunction get_stdout at 0x7704fe257d10>
394+
| inside context: Optimization limits:: num_cores: 2, max_iter: 1000, converged_min: 1e-07, max_time: inf,squarem=True
395+
| formatter: StdPrinter, outstream=<cyfunction get_stdout at 0x7704fe257d10>
396+
| set values inside context: Optimization limits:: num_cores: 2, max_iter: 123, converged_min: 0.0001, max_time: inf,squarem=True
397+
| formatter: StdPrinter, outstream=<cyfunction get_stdout at 0x7704fe257d10>
398+
| Optimization reached maximum number of iterations
399+
| after context: Optimization limits:: num_cores: 2, max_iter: 1000, converged_min: 1e-07, max_time: inf,squarem=False
400+
| formatter: StdPrinter, outstream=<cyfunction get_stdout at 0x7704fe257d10>
401+
402+
403+
.. |Du2018| replace:: `Du & Varadhan 2018 <https://doi.org/10.48550/arXiv.1810.11163>`__
404+
.. |Varadhan2008| replace:: `Varadhan & Roland 2008. <https://doi.org/10.1111/j.1467-9469.2007.00585.x>`__
266405
.. |H2MM| replace:: H\ :sup:`2`\ MM

0 commit comments

Comments
 (0)