Skip to content

Commit 69d6aa2

Browse files
committed
Add autoscale enum and add alternative options
1 parent 1ef919a commit 69d6aa2

6 files changed

Lines changed: 267 additions & 98 deletions

File tree

MDMC/control/control.py

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from MDMC.MD.engine_facades.facade import MDEngineError
1818
from MDMC.MD.parameters import Parameters
1919
from MDMC.MD.simulation import Simulation
20-
from MDMC.refinement.FoM.FoM_abs import ObservablePair
20+
from MDMC.refinement.FoM.FoM_abs import AutoScale, ObservablePair
2121
from MDMC.refinement.FoM.FoM_factory import FoMFactory
2222
from MDMC.refinement.minimizers.minimizer_factory import MinimizerFactory
2323
from MDMC.resolution.resolution_factory import ResolutionFactory
@@ -64,10 +64,8 @@ class Control:
6464
- ``rescale_factor`` (`float`, optional, defaults to `1.`) applied to
6565
the experimental data when calculating the FoM to ensure it is on
6666
the same scale as the calculated observable
67-
- ``auto_scale`` (`bool`, optional, defaults to `False`) set the
68-
``rescale_factor`` automatically to minimise the FoM, if both
69-
``rescale_factor`` and ``auto_scale`` are provided then a warning
70-
is printed and ``auto_scale`` takes precedence
67+
- ``auto_scale`` (str or :class:`AutoScale`, optional, defaults to `CONSTANT`)
68+
See :class:`AutoScale` documentation for more information.
7169
- ``use_FFT`` (`bool`, optional, defaults to `True`) whether to use
7270
Fast Fourier Transforms in the calculation of dependent variables.
7371
FFT speeds up calculation but places restrictions on spacing in the
@@ -161,7 +159,7 @@ class Control:
161159
'reader':'GENERIC_READER',
162160
'weight':0.5,
163161
'resolution':{'gaussian':2.35}
164-
'auto_scale':True}]
162+
'auto_scale': 'minimise_fom'}]
165163
166164
Attributes
167165
----------
@@ -253,39 +251,45 @@ def __init__(self, simulation: Simulation, exp_datasets: List[dict],
253251

254252
self._validate_energy(MD_observable)
255253

256-
auto_scale = dset.get('auto_scale', False)
257-
rescale_factor = dset.get('rescale_factor')
258-
if auto_scale and rescale_factor and self.verbose != -1:
259-
print('Both `rescale_factor` and `auto_scale` set for file {};'
260-
' scaling will be automated to minimise FoM'
261-
''.format(dset['file_name']))
262-
rescale_factor = 1.
263-
elif not rescale_factor:
264-
rescale_factor = 1.
265-
266-
observable_pair = ObservablePair(exp_observable,
267-
MD_observable,
268-
dset['weight'],
269-
rescale_factor=rescale_factor,
270-
auto_scale=auto_scale)
254+
auto_scale = dset.get("auto_scale", "CONSTANT")
255+
rescale_factor = dset.get("rescale_factor", 1)
256+
257+
if auto_scale == "NONE" and rescale_factor != 1:
258+
logging.warning("Requested no scaling, but provided `rescale_factor`. "
259+
"Disabling scaling.")
260+
rescale_factor = 1
261+
262+
auto_scale = (AutoScale[auto_scale.upper()]
263+
if isinstance(auto_scale, str) else
264+
AutoScale(auto_scale))
265+
266+
if (
267+
rescale_factor != 1 and
268+
auto_scale is not AutoScale.CONSTANT and
269+
self.verbose != -1
270+
):
271+
logging.warning("`rescale_factor` incompatible with %s set for file"
272+
" %s; scaling will be %s.",
273+
auto_scale.name, dset['file_name'], auto_scale.name)
274+
275+
observable_pair = ObservablePair(
276+
exp_observable,
277+
MD_observable,
278+
dset['weight'],
279+
rescale_factor=float(rescale_factor),
280+
auto_scale=auto_scale,
281+
)
271282
self.observable_pairs.append(observable_pair)
272283
self.recreated_independent_vars = {}
273284
self.production_time_step = self.simulation.time_step
274285

275286
# Take the largest minimum number of MD_steps needed by any dataset
276-
min_MD_steps_dset = self._calculate_minimum_MD_steps(
277-
observable_pair)
287+
min_MD_steps_dset = self._calculate_minimum_MD_steps(observable_pair)
278288
minimum_MD_steps = max(minimum_MD_steps, min_MD_steps_dset)
279289

280-
if FoM_options is None or FoM_options.get('error') is None:
281-
FoM_error = 'exp'
282-
else:
283-
FoM_error = FoM_options.get('error')
284-
285-
if FoM_options is None or FoM_options.get('norm') is None:
286-
FoM_norm = 'data_points'
287-
else:
288-
FoM_norm = FoM_options.get('norm')
290+
FoM_options = FoM_options if FoM_options is not None else {}
291+
FoM_error = FoM_options.get("error", "exp")
292+
FoM_norm = FoM_options.get("norm", "data_points")
289293

290294
self.FoM_calculator = FoMFactory.create_FoM(FoM_error, self.observable_pairs,
291295
norm=FoM_norm,

MDMC/refinement/FoM/ChiSquared_experror.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,43 @@ class ChiSquaredExpError(FigureOfMerit):
1414
mathematical details.
1515
"""
1616

17+
def _compute_unreduced(self, obs_pair: ObservablePair):
18+
"""
19+
Compute the unreduced FoM value for the given observable pair.
20+
21+
Parameters
22+
----------
23+
obs_pair : ObservablePair
24+
An ``ObservablePair`` for which the FoM is calculated.
25+
26+
Returns
27+
-------
28+
float
29+
Unreduced FoM value.
30+
"""
31+
return np.sum((obs_pair.calculate_difference() /
32+
obs_pair.calculate_exp_errors()) ** 2)
33+
34+
def _minimise_factor(self, obs_pair: ObservablePair) -> float:
35+
"""
36+
Minimise the FoM factor for the given FoM type.
37+
38+
Parameters
39+
----------
40+
obs_pair : ObservablePair
41+
An ``ObservablePair`` for which the FoM is calculated
42+
43+
Returns
44+
-------
45+
float
46+
Computed auto_scale factor to minimise the FoM.
47+
"""
48+
exp_errors = np.array(*obs_pair.exp_obs.errors.values())
49+
exp_values = np.array(*obs_pair.exp_obs.dependent_variables.values())
50+
MD_values = np.array(*obs_pair.MD_obs.dependent_variables.values())
51+
return (np.sum((MD_values / exp_errors) ** 2) /
52+
np.sum(MD_values * exp_values / exp_errors ** 2))
53+
1754
def calculate_single_FoM(self, obs_pair: ObservablePair):
1855
"""
1956
Calculates the chi-squared figure of merit for a single
@@ -30,16 +67,7 @@ def calculate_single_FoM(self, obs_pair: ObservablePair):
3067
float
3168
The FoM for the obs_pair
3269
"""
33-
34-
if obs_pair.auto_scale:
35-
exp_errors = np.array(*obs_pair.exp_obs.errors.values())
36-
exp_values = np.array(
37-
*obs_pair.exp_obs.dependent_variables.values())
38-
MD_values = np.array(*obs_pair.MD_obs.dependent_variables.values())
39-
obs_pair.rescale_factor = (np.sum((MD_values / exp_errors) ** 2) / np.sum(
40-
MD_values * exp_values / exp_errors ** 2))
41-
42-
norm_factor = self.data_norm_factor(obs_pair=obs_pair)
43-
value_unreduced = np.sum((obs_pair.calculate_difference()
44-
/ obs_pair.calculate_exp_errors()) ** 2)
70+
obs_pair.rescale_factor = self.compute_rescale_factor(obs_pair)
71+
norm_factor = self.data_norm_factor(obs_pair)
72+
value_unreduced = self._compute_unreduced(obs_pair)
4573
return obs_pair.weight * value_unreduced / norm_factor

MDMC/refinement/FoM/FoM_abs.py

Lines changed: 114 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,59 @@
11
"""A module for Figure of Merits"""
22

33
from abc import ABC, abstractmethod
4+
from enum import Enum, auto
45

56
import numpy as np
67

78
from MDMC.common.decorators import repr_decorator
89
from MDMC.trajectory_analysis.observables.obs import Observable
910

1011

12+
class AutoScale(Enum):
13+
"""
14+
Autoscaling methods.
15+
16+
Notes
17+
-----
18+
19+
- ``CONSTANT`` applies a constant scaling factor to the MD data
20+
to align with the epxerimental data.
21+
22+
- ``MINIMISE_FOM`` uses the algorithm described in
23+
:ref:`explanation/figure-of-merit:rescaling` to best minimise
24+
the figure of merit.
25+
26+
- ``MATCH_MAXIMUM`` rescales the MD data such that the maximum is
27+
the same as that of the experimental data.
28+
29+
- ``MATCH_ABS_MAXIMUM`` rescales the MD data such that the absolute maximum is
30+
the same as that of the experimental data.
31+
32+
- ``MATCH_SUM`` is a naïve approximation to rescale the MD data such that
33+
the integral area under the curve is the same under the assumumption
34+
that the samples are evenly spaced.
35+
36+
- ``MATCH_ABS_SUM`` is like ``MATCH_SUM`` except that the data's
37+
absolute values are used instead.
38+
"""
39+
#: Constant valued scaling.
40+
CONSTANT = auto()
41+
#: Minimise the main FoM factor e.g. χ²
42+
MINIMISE_FOM = auto()
43+
#: Match the maxima of the values.
44+
MATCH_MAXIMUM = auto()
45+
#: Match the absolute maxima of the values.
46+
MATCH_ABS_MAXIMUM = auto()
47+
#: Match the integral area (approximated by a sum) of the values.
48+
MATCH_SUM = auto()
49+
#: Match the absolute integral area (approximated by an abs->sum) of the values.
50+
MATCH_ABS_SUM = auto()
51+
52+
#: No scaling to be applied (CONSTANT w/ 1.).
53+
NONE = CONSTANT
54+
MINIMIZE_FOM = MINIMISE_FOM
55+
56+
1157
@repr_decorator('weight', 'exp_obs', 'MD_obs', 'rescale_factor', 'auto_scale')
1258
class ObservablePair:
1359

@@ -35,7 +81,7 @@ class ObservablePair:
3581
"""
3682

3783
def __init__(self, exp_obs: Observable, MD_obs: Observable, weight: float,
38-
rescale_factor: float = 1., auto_scale: bool = False):
84+
rescale_factor: float = 1., auto_scale: AutoScale = AutoScale.NONE):
3985

4086
self.exp_obs = exp_obs
4187
self.MD_obs = MD_obs
@@ -326,7 +372,6 @@ def calculate_exp_errors(self) -> np.ndarray:
326372

327373
return np.array(*self.exp_obs.errors.values()) * self.rescale_factor
328374

329-
330375
@repr_decorator('value', 'obs_pairs')
331376
class FigureOfMerit(ABC):
332377

@@ -421,8 +466,7 @@ def data_norm_factor(self, obs_pair: ObservablePair) -> int:
421466
"""
422467

423468
if self.norm:
424-
norm_factor = np.size(
425-
*obs_pair.MD_obs.dependent_variables.values())
469+
norm_factor = np.size(*obs_pair.MD_obs.dependent_variables.values())
426470
norm_factor -= self.n_parameters
427471
else:
428472
norm_factor = 1
@@ -446,3 +490,69 @@ def calculate_single_FoM(self, obs_pair: ObservablePair) -> float:
446490
"""
447491

448492
raise NotImplementedError
493+
494+
@abstractmethod
495+
def _compute_unreduced(self, obs_pair: ObservablePair) -> float:
496+
"""
497+
Compute the unreduced FoM value for the given observable pair.
498+
499+
Parameters
500+
----------
501+
obs_pair : ObservablePair
502+
An ``ObservablePair`` for which the FoM is calculated.
503+
504+
Returns
505+
-------
506+
float
507+
Unreduced FoM value.
508+
"""
509+
510+
@abstractmethod
511+
def _minimise_factor(self, obs_pair: ObservablePair) -> float:
512+
"""
513+
Minimise the FoM factor for the given FoM type.
514+
515+
Parameters
516+
----------
517+
obs_pair : ObservablePair
518+
An ``ObservablePair`` for which the FoM is calculated
519+
520+
Returns
521+
-------
522+
float
523+
Computed auto_scale factor to minimise the FoM.
524+
"""
525+
526+
def compute_rescale_factor(self, obs_pair: ObservablePair) -> float:
527+
"""
528+
Compute rescale factor for calculated observable to match experimental data.
529+
530+
Parameters
531+
----------
532+
obs_pair : ObservablePair
533+
An ``ObservablePair`` for which the FoM is calculated
534+
535+
Returns
536+
-------
537+
float
538+
Computed rescale factor.
539+
"""
540+
dep_vars = np.array(*obs_pair.exp_obs.dependent_variables.values())
541+
542+
match obs_pair.auto_scale:
543+
case AutoScale.CONSTANT:
544+
fac = obs_pair.rescale_factor
545+
case AutoScale.MINIMISE_FOM:
546+
fac = self._minimise_factor(obs_pair)
547+
case AutoScale.MATCH_MAXIMUM:
548+
fac = max(obs.max() for obs in dep_vars)
549+
case AutoScale.MATCH_ABS_MAXIMUM:
550+
fac = max(np.abs(obs).max() for obs in dep_vars)
551+
case AutoScale.MATCH_SUM:
552+
fac = sum(obs.sum() for obs in dep_vars)
553+
case AutoScale.MATCH_ABS_SUM:
554+
fac = sum(np.abs(obs.sum()) for obs in dep_vars)
555+
case _:
556+
fac = 1.
557+
558+
return fac

MDMC/refinement/FoM/RSquared_noneerror.py

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,43 @@ class RSquared_noneerror(FigureOfMerit):
3333
simple linear scaling.
3434
"""
3535

36+
def _compute_unreduced(self, obs_pair: ObservablePair):
37+
"""
38+
Compute the unreduced FoM value for the given observable pair.
39+
40+
Parameters
41+
----------
42+
obs_pair : ObservablePair
43+
An ``ObservablePair`` for which the FoM is calculated.
44+
45+
Returns
46+
-------
47+
float
48+
Unreduced FoM value.
49+
"""
50+
return np.sum(obs_pair.calculate_difference() ** 2)
51+
52+
def _minimise_factor(self, obs_pair: ObservablePair) -> float:
53+
"""
54+
Minimise the FoM factor for the given FoM type.
55+
56+
Parameters
57+
----------
58+
obs_pair : ObservablePair
59+
An ``ObservablePair`` for which the FoM is calculated
60+
61+
Returns
62+
-------
63+
float
64+
Computed auto_scale factor to minimise the FoM.
65+
"""
66+
exp_values = np.array(
67+
*obs_pair.exp_obs.dependent_variables.values())
68+
MD_values = np.array(*obs_pair.MD_obs.dependent_variables.values())
69+
A = np.sum(MD_values * exp_values)
70+
B = np.sum(exp_values ** 2)
71+
return A / B
72+
3673
def calculate_single_FoM(self, obs_pair: ObservablePair):
3774
# ignore line too long linting as it is necessary for LaTeX formatting
3875
# pylint: disable=line-too-long
@@ -68,14 +105,7 @@ def calculate_single_FoM(self, obs_pair: ObservablePair):
68105
The FoM for the obs_pair
69106
"""
70107

71-
if obs_pair.auto_scale:
72-
exp_values = np.array(
73-
*obs_pair.exp_obs.dependent_variables.values())
74-
MD_values = np.array(*obs_pair.MD_obs.dependent_variables.values())
75-
A = np.sum(MD_values * exp_values)
76-
B = np.sum(exp_values ** 2)
77-
obs_pair.rescale_factor = A / B
78-
79-
norm_factor = self.data_norm_factor(obs_pair=obs_pair)
80-
value_unreduced = np.sum(obs_pair.calculate_difference() ** 2)
108+
obs_pair.rescale_factor = self.compute_rescale_factor(obs_pair)
109+
norm_factor = self.data_norm_factor(obs_pair)
110+
value_unreduced = self._compute_unreduced(obs_pair)
81111
return obs_pair.weight * value_unreduced / norm_factor

MDMC/refinement/FoM/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
"""A module for Figure of Merit calculation"""
22
from . import ChiSquared_experror, FoM_abs, FoM_factory, RSquared_noneerror
3+
from .FoM_abs import AutoScale
34

45
__all__ = [
56
"ChiSquared_experror",
67
"FoM_abs",
78
"FoM_factory",
89
"RSquared_noneerror",
10+
"AutoScale",
911
]

0 commit comments

Comments
 (0)