-
Notifications
You must be signed in to change notification settings - Fork 15
/
ulens_model_fit.py
4209 lines (3619 loc) · 166 KB
/
ulens_model_fit.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
Class and script for fitting microlensing model using MulensModel.
All the settings are read from a YAML file.
"""
import sys
from os import path, sep
import tempfile
import shutil
import warnings
import math
import numpy as np
import shlex
from scipy.interpolate import interp1d
from matplotlib import pyplot as plt
from matplotlib import gridspec, rcParams, rcParamsDefault
# from matplotlib.backends.backend_pdf import PdfPages
import_failed = set()
try:
import yaml
except Exception:
import_failed.add("yaml")
try:
import emcee
except Exception:
import_failed.add("emcee")
try:
import corner
except Exception:
import_failed.add("corner")
try:
from pymultinest.run import run as mn_run
from pymultinest.analyse import Analyzer
except Exception:
import_failed.add("pymultinest")
try:
import ultranest
except Exception:
import_failed.add("ultranest")
try:
import plotly.graph_objects as go
except Exception:
import_failed.add("plotly")
try:
import MulensModel as mm
except Exception:
raise ImportError('\nYou have to install MulensModel first!\n')
__version__ = '0.42.1'
class UlensModelFit(object):
"""
Class for fitting microlensing model using *MulensModel* package.
Parameters :
photometry_files: *list* or *str*
List of datasets. It can be either a *str* (then just gives
a name of one file that is read) or a *list*. For *list* each
element is either a *str* (then just gives the name of the file)
or a *dict*, which allows more options to be passed, e.g.,
.. code-block:: python
[{'file_name': 'data_1.dat', 'phot_fmt': 'mag'}]
or
.. code-block:: python
[{'file_name': 'data_1.dat'}, 'data_2.dat']
Currently, keyword ``'add_2450000'`` is turned on by default.
Except standard parameters of MulensData, one can additionally
pass
``'scale_errorbars': {'factor': kappa, 'minimum': epsilon}``
to scale uncertainties.
fit_method: *str*
Method of fitting. Currently accepted values are ``EMCEE``,
``MultiNest``, and ``UltraNest``. If not provided, the script
will guess it based on other parameters: ``EMCEE`` is selected
if ``starting_parameters`` are provided. If ``prior_limits`` are
provided, either ``MultiNest`` or ``UltraNest`` will be selected
depending on the ``fitting_parameters``.
Webpage of each method:
- EMCEE: https://emcee.readthedocs.io/en/stable/
- MultiNest: https://johannesbuchner.github.io/PyMultiNest/
- UltraNest: https://johannesbuchner.github.io/UltraNest/
starting_parameters: *dict*
Starting values of the parameters.
It also indicates the EMCEE fitting mode.
There are two possibilities for the information provided.
First, you can provide a name of file with sets of
parameters and names of parameters. For example:
.. code-block:: python
{
'file': 'STARTING_POINTS.txt',
'parameters': 't_0 u_0 t_E'
}
In that case the text file has three columns and
at least 2 * 3 = 6 rows with values.
Second, you can provide distribution that defines distributions
from which values of parameters will be drawn.
Keys of this *dict* are microlensing parameters recognized by
*MulensModel* and values are *str*. First word indicates
the distribution (allowed: ``gauss``, ``uniform``, and
``log-uniform``) and is followed by its parameters.
For ``uniform`` and ``log-uniform`` these parameters are lower
and upper limit.
For ``gauss`` these parameters are mean and sigma of
the distribution. For example:
.. code-block:: python
{
't_0': 'gauss 2455703. 0.01',
'u_0': 'uniform 0.001 1.',
't_E': 'gauss 20. 5.'
}
prior_limits: *dict*
Upper and lower limits of parameters.
It only applies to pyMultiNest and UltraNest fitting.
Keys are MulensModel parameters and values are lists of two floats
each (alternatively a string giving 2 floats can be provided - see
example below). Currently, no informative priors are allowed for
pyMultiNest and UltraNest fitting. Example input:
.. code-block:: python
{
't_0': [2455379.4, 2455379.76]
'u_0': [0.46, 0.65]
't_E': "16. 19.6"
}
model: *dict*
Additional settings for *MulensModel.Model*. Accepted keys:
``'coords'`` - event coordinates,
``'methods'`` - methods used for magnification calculation,
``'methods source 1'`` - methods used for magnification calculation
for the first source in binary source models,
``'methods source 2'`` - methods used for magnification calculation
for the second source in binary source models,
``'default method'`` - default magnification calculation method,
``'methods parameters'`` - dict of dicts that add more parameters
that are passed to methods calculating magnification; typical call:
``'VBBL': {'accuracy': 0.01}``
``'limb darkening u'`` - specifies a *dict* that gives limb
darkening coefficients in "u" convention, e.g.,
{'I': 0.4, 'V': 0.5}; note that for plotting the best model we use
the LD coefficient same as for the first dataset,
``'parameters'`` and ``'values'`` - used to plot specific model.
fixed_parameters: *dict*
Provide parameters that will be kept fixed during the fitting
process. This option is often used to set parameter reference
epoch, e.g., ``{'t_0_par': 2456789.}``.
min_values: *dict*
Minimum values of parameters that define the prior, e.g.,
``{'t_E': 0.}``. Note that the these are only limits of a prior.
Functional form of priors can be defined in ``fit_constraints``.
It works only for EMCEE fitting.
max_values: *dict*
Maximum values of parameters that define the prior, e.g.,
``{'u_0': 1.}``.
It works only for EMCEE fitting.
fitting_parameters: *dict*
Parameters of the fit function. They depend on the method used -
we discuss EMCEE, pyMultiNest and UltraNest below.
First - EMCEE. The required parameter is ``n_steps``.
You can also specify ``n_burn`` and ``n_walkers``. The ``n_burn``
controls the length of burn-in. If not provided, it is assumed to
be ``0.25*n_steps``. The ``n_walkers`` gives number of parallel
walkers to be run. If not provided, it is assumed four times
the number of parameters to be fitted.
Other options are described below.
The ``progress`` option (*bool* type value; default is *False*)
controls if a progress bar is shown.
It is possible to export posterior to a .npy file. Just provide
the file name as ``posterior file`` parameter. You can read this
file using ``numpy.load()``. You will get an array with a shape of
(``n_walkers``, ``n_steps-n_burn``, ``n_parameters``). You can
additionally add option ``posterior file fluxes`` for which
allowed values are ``all`` and *None* (``null`` in yaml file).
The value ``all`` means that additionally all source and blending
fluxes will be saved (``n_parameters`` increases by two times the
number of datasets).
Second - pyMultiNest. There are no required parameters, but a few
can be provided. Currently accepted ones are:
``basename`` (*str*) - common part of the output files produced by
MultiNest. If you don't provide it, then the output would be
saved to temporary files and deleted at the end.
``multimodal`` (*bool*) - do you want multiple modes in
the posterior to be detected and reported separately?
``n_live_points`` (*int*) - number of live points, default value
is 400. Also valid for UltraNest.
``sampling efficiency`` (*float*) - requested sampling efficiency.
MultiNest documentation suggests 0.8 (default value) for parameter
estimation and 0.3 for evidence evaluation.
``evidence tolerance`` (*float*) - requested tolerance of ln(Z)
calculation; default is 0.5 and should work well in most cases.
Third - UltraNest. There are no required parameters, but a few
can be provided. Currently accepted ones are:
``log directory`` (*str*) - where to store output files. If given,
there is a check if directory exists. If not given, no outputs
are saved.
``derived parameter names`` (*str*) - names of additional derived
parameters created by transform. In microlensing, they are usually
the source(s) and blending fluxes. If not given, they are ignored
in the transform function.
``show_status`` (*bool*) - whether to show integration progress
as a status line or not. Default is *True*.
``min_num_live_points`` (*int*) - minimum number of live points
throughout the run. Default value is 400.
``dlogz`` (*float*) - Target evidence uncertainty, in order to
obtain a logz error below a threshold. Default value is 0.5.
It can be increased to allow `min_num_live_points` values below:
sqrt(iterations) / dlogz = sqrt(1000) / 0.5 ~ 64.
``frac_remain`` (*float*) - Integrate until this fraction of the
integral is left in the remainder. Numbers smaller than 0.01
ensure that peaks are discovered, higher numbers can be set if
the posterior is simple. Default value is 0.01.
``max_num_improvement_loops`` (*int*) - Limit the number of times
the algorithm is repeated to improve. Default value is -1.
fit_constraints: *dict*
Constraints on model other than minimal and maximal values.
Currently accepted keys:
``'no_negative_blending_flux'`` - reject models with negative
blending flux if *True*
``'negative_blending_flux_sigma_mag'`` - impose a prior that
disfavors models with negative blending flux using gaussian prior
for negative values; the value provided should be on the order of
*20.*
``'color'`` - specify gaussian prior for colors of the sources.
Parameters:
*mean* and *sigma* are floats in magnitudes, *dataset_label*
are str defined in MulensData.plot_properties['label']
``'color source 1'`` - specify gaussian prior for color of
the primary source in binary source model.
Parameters:
*mean* and *sigma* are floats in magnitudes, *dataset_label*
are str defined in MulensData.plot_properties['label']
``'color source 2'`` - specify gaussian prior for color of
the secondary source in binary source model.
Parameters:
*mean* and *sigma* are floats in magnitudes, *dataset_label*
are str defined in MulensData.plot_properties['label']
``'prior'`` - specifies the priors for quantities. It's also
a *dict*. Possible key-value pairs:
``'t_E': 'Mroz et al. 2017'`` - efficiency-corrected t_E
distribution from that paper with two modifications: 1) it is
constant for t_E < 1d, 2) it follows Mao & Paczynski (1996)
analytical approximation (i.e., slope of -3) for t_E longer
than probed by Mroz et al. (2017; i.e., 316 d). Note that
Mroz et al. (2020) studied Galactic bulge.
``'t_E': 'Mroz et al. 2020'`` - similar to above but for
Mroz et al. (2020), where Galactic disc outside bulge region
was studied. Approximate slopes of 3 and -3 from
Mao & Paczynski (1996) are used for t_E shorter and longer,
respectively, than probed by Mroz et al. (2020).
``'pi_E_N': gauss mean sigma`` (same for ``'pi_E_E'``) -
specify gaussian prior for parallax components. Parameters
*mean* and *sigma* are floats.
References:
Mao & Paczynski 1996 -
https://ui.adsabs.harvard.edu/abs/1996ApJ...473...57M/abstract
Mroz et al. 2017 -
https://ui.adsabs.harvard.edu/abs/2017Natur.548..183M/abstract
Mroz et al. 2020 -
https://ui.adsabs.harvard.edu/abs/2020ApJS..249...16M/abstract
``'posterior parsing'`` - additional settings that allow
modyfing posterior after it's calculated. Possile values:
``'abs': [...]`` - calculate absolute values for parameters
from given list. It's useful for e.g. ``'u_0'`` for
free-floating planet events.
plots: *dict*
Parameters of the plots to be made after the fit. Currently
allowed keys are ``triangle``, ``trace`` (only EMCEE fitting),
``trajectory``, and ``best model``.
The values are also dicts and currently accepted keys are:
1) for ``best model``:
``'file'``,``'interactive' ``'time range'``, ``'magnitude range'``,
``'legend'``,`and ``'rcParams'``,
2) for ``triangle`` and ``trace``:
``'file'`` and ``'shift t_0'`` (*bool*, *True* is default)
3) for ``trajectory``:
``'file'`` and ``'time range'`` (if not provided, then values
from ``best model`` will be used)
e.g.:
.. code-block:: python
{
'triangle':
'file': 'my_fit_triangle.png'
'trace':
'file': 'my_fit_trace_plot.png'
'shift t_0': False
'trajectory':
'file': 'my_trajectory.png'
'time range': 2456050. 2456300.
'best model':
'file': 'my_fit_best.png'
'interactive' : 'my_fit_best.html'
'time range': 2456000. 2456300.
'magnitude range': 15.123 13.012
'legend':
'ncol': 2
'loc': 'lower center'
'rcParams':
'font.size': 15
}
Note that 'rcParams' allows setting many matplotlib parameters.
other_output: *dict*
Parameters for other output. Allowed value are:
``'models': {'file name': NAME_OF_FILE}`` where NAME_OF_FILE is
a *str* that gives a path to text file to which we will print all
models and their chi^2. If ``NAME_OF_FILE`` is ``"-"``, then
the models will be printed to standard output.
``'yaml output': {'file name': NAME_OF_FILE}`` where NAME_OF_FILE
is a *str* that gives a path to YAML-format file to which
the results will be printed
``'residuals': {'files': FILE_1 FILE_2 ...}`` where FILE_X are
the names of the files to which residuals will be printed.
These files will have three columns: time, residuals, and
uncertainties.
"""
def __init__(
self, photometry_files,
starting_parameters=None, prior_limits=None, model=None,
fixed_parameters=None,
min_values=None, max_values=None, fitting_parameters=None,
fit_constraints=None, plots=None, other_output=None,
fit_method=None
):
self._check_MM_version()
self._photometry_files = photometry_files
self._starting_parameters_input = starting_parameters
self._prior_limits = prior_limits
self._model_parameters = model
self._fixed_parameters = fixed_parameters
self._min_values = min_values
self._max_values = max_values
self._fitting_parameters = fitting_parameters
self._fit_constraints = fit_constraints
self._plots = plots
self._other_output = other_output
self._fit_method = fit_method
self._which_task()
self._set_default_parameters()
if self._task == 'fit':
if self._fit_method is None:
self._guess_fitting_method()
else:
self._fit_method = self._fit_method.lower()
self._check_fitting_method()
self._check_starting_parameters_type()
self._set_fit_parameters_unsorted()
self._check_imports()
def _check_MM_version(self):
"""
Check if MulensModel is new enough
"""
# code_version = "{:} and {:}".format(mm.__version__, __version__)
# print('\nMulensModel and script versions:', code_version, end='\n\n')
if int(mm.__version__.split('.')[0]) < 2:
raise RuntimeError(
"ulens_model_fit.py requires MulensModel in version "
"at least 2.0, but you are using " + mm.__version__)
def _which_task(self):
"""
Check if input parameters indicate run_fit() or plot_best_model() will
be run.
"""
if self._starting_parameters_input is not None:
fit = True
elif self._prior_limits is not None:
fit = True
else:
fit = False
plot = False
if self._model_parameters is not None:
keys = set(self._model_parameters.keys())
check = keys.intersection({'parameters', 'values'})
if len(check) == 1:
raise ValueError(
'You have to specify either both or none of ' +
'model["parameters"] and model["values"].')
if len(check) == 2:
plot = True
if plot and fit:
raise ValueError(
'Too many parameters specified!\nThe starting_parameters ' +
'indicate you want to fit, but model["parameters"] and ' +
'model["values"] indicate you want to plot. Please decide')
if not plot and not fit:
if self._fixed_parameters is None:
raise ValueError(
'Missing input information. Please specify parameters ' +
'to be plotted (model["parameters"] and ' +
'model["values"]) or starting_parameters to be fit.')
else:
plot = True
if fit:
self._task = 'fit'
elif plot:
self._task = 'plot'
self._check_unnecessary_settings_plot()
else:
raise ValueError('internal error')
def _check_unnecessary_settings_plot(self):
"""
Make sure that there aren't too many parameters specified for:
self._task == 'plot'
"""
keys = ['_starting_parameters_input', '_min_values', '_max_values',
'_fitting_parameters', '_prior_limits']
for key in keys:
if getattr(self, key) is not None:
raise ValueError(
'In plotting mode you should not provide in __init__: ' +
key[1:])
if self._plots is not None:
if "triangle" in self._plots:
raise ValueError(
'You cannot provide plots["triangle"] if you '
"don't fit")
if "trace" in self._plots:
raise ValueError(
'You cannot provide plots["trace"] if you' " don't fit")
def _set_default_parameters(self):
"""
set some default parameters
"""
if self._task == 'fit':
self._flat_priors = True # Are priors only 0 or 1?
self._return_fluxes = True
self._best_model_ln_prob = -np.inf
self._flux_names = None
self._shift_t_0 = True
elif self._task == 'plot':
pass
else:
raise ValueError('internal error - task ' + str(self._task))
self._print_model = False
self._yaml_results = False
self._residuals_output = False
parameters_str = (
't_0 u_0 t_0_1 u_0_1 t_0_2 u_0_2 t_E t_eff rho rho_1 rho_2 ' +
't_star t_star_1 t_star_2 pi_E_N pi_E_E s q alpha ds_dt ' +
'dalpha_dt x_caustic_in x_caustic_out t_caustic_in ' +
't_caustic_out xi_period xi_semimajor_axis xi_Omega_node ' +
'xi_inclination xi_argument_of_latitude_reference ' +
'xi_eccentricity xi_omega_periapsis q_source')
self._all_MM_parameters = parameters_str.split()
self._fixed_only_MM_parameters = ['t_0_par', 't_0_xi']
self._latex_conversion = dict(
t_0='t_0', u_0='u_0',
t_0_1='t_{0,1}', u_0_1='u_{0,1}',
t_0_2='t_{0,2}', u_0_2='u_{0,2}', t_E='t_{\\rm E}',
t_eff='t_{\\rm eff}', rho='\\rho', rho_1='\\rho_1',
rho_2='\\rho_2', t_star='t_{\\star}', t_star_1='t_{\\star,1}',
t_star_2='t_{\\star,2}', pi_E_N='\\pi_{{\\rm E},N}',
pi_E_E='\\pi_{{\\rm E},E}', s='s', q='q', alpha='\\alpha',
ds_dt='ds/dt', dalpha_dt='d\\alpha/dt',
x_caustic_in='x_{\\rm caustic,in}',
x_caustic_out='x_{\\rm caustic,out}',
t_caustic_in='t_{\\rm caustic,in}',
t_caustic_out='t_{\\rm caustic,out}',
xi_period='\\xi_P',
xi_semimajor_axis='\\xi_a',
xi_Omega_node='\\xi_{\\Omega}',
xi_inclination='\\xi_i',
xi_argument_of_latitude_reference='\\xi_u',
xi_eccentricity='\\xi_e',
xi_omega_periapsis='\\xi_{\\omega}',
q_source='q_{\\rm source}',
)
self._user_parameters = []
self._other_parameters = []
self._latex_conversion_user = dict()
self._latex_conversion_other = dict()
self._set_default_user_and_other_parameters()
def _set_default_user_and_other_parameters(self):
"""
Method to be sub-classed if user defines their own parameters (microlensing or other).
If you use different microlensing parameters, then define self._user_parameters and
self._latex_conversion_user.
If you add non-microlensing parameters (e.g., source distance) then define self._other_parameters and
self._latex_conversion_other.
"""
pass
def _guess_fitting_method(self):
"""
guess what is the fitting method based on parameters provided
"""
method = None
if self._starting_parameters_input is not None:
method = "EMCEE"
if self._prior_limits is not None:
if method is not None:
raise ValueError(
"Both starting_parameters and prior_limits were defined "
"which makes impossible to choose the fitting method. "
"These settings indicate EMCEE and pyMultiNest "
"respectively, and cannot be both set.")
method = self._guess_MultiNest_or_UltraNest()
if method is None:
raise ValueError(
"No fitting method chosen. You can chose either 'EMCEE' or "
"'pyMultiNest' and you do it by providing "
"starting_parameters or prior_limits, respectively.")
self._fit_method = method
def _guess_MultiNest_or_UltraNest(self):
"""
Guess fit_method between MultiNest or UltraNest, based on the
provided fitting_parameters.
"""
args_MultiNest = ['basename', 'multimodal', 'evidence tolerance',
'n_live_points']
if all([key in args_MultiNest for key in self._fitting_parameters]):
return "MultiNest"
args_UltraNest = ['log directory', 'derived parameter names',
'show_status', 'dlogz', 'frac_remain',
'max_num_improvement_loops', 'n_live_points']
if all([key in args_UltraNest for key in self._fitting_parameters]):
return "UltraNest"
raise ValueError(
"Cannot guess fitting method. Provide more parameters in "
"fitting_parameters.")
def _check_fitting_method(self):
"""
Check if fitting method is consistent with the settings.
"""
if self._fit_method == "emcee":
self._fit_method = "EMCEE"
if self._starting_parameters_input is None:
raise ValueError(
"EMCEE fitting method requires starting_parameters.")
elif self._fit_method in ["multinest", "ultranest"]:
self._fit_method = self._fit_method.capitalize()
self._fit_method = self._fit_method.replace("nest", "Nest")
if self._prior_limits is None:
msg = "{:} fitting method requires prior_limits."
raise ValueError(msg.format(self._fit_method))
else:
raise ValueError("Invalid fitting method was inserted.")
def _check_starting_parameters_type(self):
"""
Check if starting parameters are read from file or
will be drawn from distributions specified.
"""
if self._fit_method in ["MultiNest", "UltraNest"]:
return
if 'file' in self._starting_parameters_input:
in_type = 'file'
keys_expected = {'file', 'parameters'}
keys = set(self._starting_parameters_input.keys())
if keys != keys_expected:
error = ('Wrong starting parameters keys. Expected: ' +
str(keys_expected) + '; Provided: ' + str(keys))
raise KeyError(error)
else:
in_type = 'draw'
self._starting_parameters_type = in_type
def _set_fit_parameters_unsorted(self):
"""
Find what are the fitted parameters. It will be sorted later.
"""
if self._fit_method == "EMCEE":
if self._starting_parameters_type == 'draw':
unsorted_keys = self._starting_parameters_input.keys()
elif self._starting_parameters_type == 'file':
unsorted_keys = self._get_unsorted_starting_parameters()
else:
raise ValueError(
'unexpected: ' + str(self._starting_parameters_type))
elif self._fit_method in ["MultiNest", "UltraNest"]:
unsorted_keys = self._prior_limits.keys()
else:
raise ValueError('unexpected method error')
self._fit_parameters_unsorted = list(unsorted_keys)
self._n_fit_parameters = len(self._fit_parameters_unsorted)
def _get_unsorted_starting_parameters(self):
"""
Make sure that a list of parameters is provided
"""
parameters = self._starting_parameters_input['parameters']
if isinstance(parameters, (str)):
parameters = parameters.split()
return parameters
def _check_imports(self):
"""
check if all the required packages are imported
"""
required_packages = set()
if self._task == 'fit':
if self._fit_method == 'EMCEE':
required_packages.add('emcee')
elif self._fit_method == "MultiNest":
required_packages.add('pymultinest')
elif self._fit_method == "UltraNest":
required_packages.add('ultranest')
if self._plots is not None and 'triangle' in self._plots:
required_packages.add('corner')
if self._plots['best model'] and 'interactive' in self._plots['best model']:
required_packages.add('plotly')
failed = import_failed.intersection(required_packages)
if len(failed) > 0:
message = (
'Some of the required packages could not be imported:\n' +
" ".join(failed))
if "corner" in failed:
message += (
"\nFor corner package it's enough that you run:\nwget " +
"https://raw.githubusercontent.com/dfm/corner.py/" +
"v2.0.0/corner/corner.py")
if "plotly" in failed:
message += ("\nThe plotly package is required for creating interactive best model plots.")
raise ImportError(message)
def run_fit(self):
"""
Run the fit, print the output, and make the plots.
This function does not accept any parameters. All the settings
are passed via __init__().
"""
if self._task != "fit":
raise ValueError('wrong settings to run .run_fit()')
self._check_plots_parameters()
self._check_model_parameters()
self._check_other_fit_parameters()
self._parse_other_output_parameters()
self._get_datasets()
self._check_ulens_model_parameters()
self._get_parameters_ordered()
self._get_parameters_latex()
self._set_prior_limits()
self._parse_fit_constraints()
if self._fit_method == "EMCEE":
self._parse_starting_parameters()
self._check_fixed_parameters()
self._make_model_and_event()
self._parse_fitting_parameters()
if self._fit_method == "EMCEE":
self._get_starting_parameters()
self._setup_fit()
self._run_fit()
self._finish_fit()
self._parse_results()
self._write_residuals()
self._make_plots()
def plot_best_model(self):
"""
Plot the best model.
The parameters names and their values are taken from __init__()
keyword ``model``, which is a *dict* and has this information in
``model['parameters']`` and ``model['values']``.
"""
if self._task != "plot":
raise ValueError('wrong settings to run .plot_best_model()')
self._check_plots_parameters()
self._check_model_parameters()
self._get_datasets()
self._check_ulens_model_parameters()
self._check_fixed_parameters()
self._make_model_and_event()
self._write_residuals()
self._make_plots()
def _check_plots_parameters(self):
"""
Check if parameters of plots make sense
"""
allowed_keys = set(['best model', 'trajectory', 'triangle', 'trace'])
if self._plots is None:
self._plots = dict()
return
unknown = set(self._plots.keys()) - allowed_keys
if len(unknown) > 0:
raise ValueError(
'Unknown plot types: {:}\n'.format(unknown) +
'Accepted plot types are: ' + ", ".join(allowed_keys))
for (key, value) in self._plots.items():
if value is None:
self._plots[key] = dict()
if 'best model' in self._plots:
self._check_plots_parameters_best_model()
if 'trajectory' in self._plots:
self._check_plots_parameters_trajectory()
if 'triangle' in self._plots:
self._check_plots_parameters_triangle()
if 'trace' in self._plots:
self._check_plots_parameters_trace()
names = {key: value.get('file', None)
for (key, value) in self._plots.items()}
done = {}
for (plot_type, name) in names.items():
if name is None:
continue
if name in done:
raise ValueError(
"Names of output plot files cannot repeat. They repeat "
"for: {:} and {:}".format(done[name], plot_type))
done[name] = plot_type
def _check_plots_parameters_best_model(self):
"""
Check if parameters of best model make sense
"""
allowed = set(['file', 'time range', 'magnitude range', 'legend',
'rcParams', 'second Y scale', 'interactive'])
unknown = set(self._plots['best model'].keys()) - allowed
if len(unknown) > 0:
raise ValueError(
'Unknown settings for "best model": {:}'.format(unknown))
self._set_time_range_for_plot('best model')
if 'magnitude range' in self._plots['best model']:
text = self._plots['best model']['magnitude range'].split()
if len(text) != 2:
raise ValueError(
"'magnitude range' for 'best model' should specify 2 " +
"values (begin and end); got: " +
str(self._plots['best model']['magnitude range']))
mag_0 = float(text[0])
mag_1 = float(text[1])
if mag_1 > mag_0:
raise ValueError(
"Incorrect 'magnitude range' for 'best model':\n" +
text[0] + " " + text[1])
self._plots['best model']['magnitude range'] = [mag_0, mag_1]
for key in ['legend', 'rcParams', 'second Y scale']:
if key in self._plots['best model']:
if not isinstance(self._plots['best model'][key], dict):
msg = ('The value of {:} (in best model settings)'
'must be a dictionary, but you provided {:}')
args = [key, type(self._plots['best model'][key])]
raise TypeError(msg.format(*args))
if 'interactive' in self._plots['best model']:
self._check_plots_parameters_best_model_interactive()
if 'second Y scale' in self._plots['best model']:
self._check_plots_parameters_best_model_Y_scale()
def _set_time_range_for_plot(self, plot_type):
"""
set time range for best model or triangle plots
"""
if 'time range' not in self._plots[plot_type]:
return
text = self._plots[plot_type]['time range'].split()
if len(text) != 2:
msg = ("'time range' for {:} plot should specify 2 values "
"(begin and end); got: {:}")
raise ValueError(
msg.format(plot_type, self._plots[plot_type]['time range']))
t_0 = float(text[0])
t_1 = float(text[1])
if t_1 < t_0:
raise ValueError("Incorrect 'time range' for " + plot_type +
"plot:\n" + text[0] + " " + text[1])
self._plots[plot_type]['time range'] = [t_0, t_1]
def _check_plots_parameters_best_model_interactive(self):
"""
Check if there is no problem with interactive best plot
"""
def _check_plots_parameters_best_model_Y_scale(self):
"""
Check if parameters for second Y scale make sense.
This function assumes that the second Y scale will be plotted.
"""
settings = self._plots['best model']['second Y scale']
allowed = set(['color', 'label', 'labels', 'magnifications'])
unknown = set(settings.keys()) - allowed
if len(unknown) > 0:
raise ValueError(
'Unknown settings for "second Y scale" in '
'"best model": {:}'.format(unknown))
if not isinstance(settings['magnifications'], list):
if settings['magnifications'] != 'optimal':
raise TypeError(
'"best model" -> "second Y scale" -> "magnifications" has '
'to be a list or "optimal", not ' +
str(type(settings['magnifications'])))
else:
for value in settings['magnifications']:
if not isinstance(value, (int, float)):
raise TypeError(
'Wrong value in magnifications: ' + str(value))
if 'labels' not in settings:
if settings['magnifications'] != 'optimal':
settings['labels'] = [
str(x) for x in settings['magnifications']]
else:
settings['labels'] = []
else:
if settings['magnifications'] == 'optimal':
raise ValueError(
'In "best model" -> "second Y scale", labels can not be '
'provided if "magnifications" is defined as "optimal"')
if not isinstance(settings['labels'], list):
raise TypeError(
'"best model" -> "second Y scale" -> "labels" has to be '
'a list, not ' + str(type(settings['labels'])))
if len(settings['labels']) != len(settings['magnifications']):
raise ValueError(
'In "best model" -> "second Y scale", labels and '
'magnifications must be lists of the same length')
def _check_plots_parameters_trajectory(self):
"""
Check if parameters of trajectory plot make sense
"""
allowed = set(['file', 'time range'])
unknown = set(self._plots['trajectory'].keys()) - allowed
if len(unknown) > 0:
raise ValueError(
'Unknown settings for "trajectory" plot: {:}'.format(unknown))
self._set_time_range_for_plot('trajectory')
def _check_plots_parameters_triangle(self):
"""
Check if parameters of triangle plot make sense
"""
allowed = set(['file', 'shift t_0'])
unknown = set(self._plots['triangle'].keys()) - allowed
if len(unknown) > 0:
raise ValueError(
'Unknown settings for "triangle" plot: {:}'.format(unknown))
self._parse_plots_parameter_shift_t_0(self._plots['triangle'])
def _parse_plots_parameter_shift_t_0(self, settings):
"""
Check if 'shift t_0' is provided and parse it
"""
if 'shift t_0' not in settings:
return
value = settings['shift t_0']
if not isinstance(value, bool):
raise TypeError(
'For triangle and trace plots, the value of "shift t_0" key '
'must be of bool type; you provided: ' + str(type(value)))
self._shift_t_0 = value
def _check_plots_parameters_trace(self):
"""
Check if parameters of trace plot make sense
"""
allowed = set(['file', 'shift t_0'])
unknown = set(self._plots['trace'].keys()) - allowed
if len(unknown) > 0:
raise ValueError(
'Unknown settings for "trace" plot: {:}'.format(unknown))
if self._fit_method in ["MultiNest", "UltraNest"]:
raise ValueError(
f'Trace plot cannot be requested for {self._fit_method}.')
self._parse_plots_parameter_shift_t_0(self._plots['trace'])
def _check_model_parameters(self):
"""
Check parameters of the MulensModel.Model provided by the user
directly.
"""
if self._model_parameters is None:
self._model_parameters = dict()
allowed = {
'coords', 'default method', 'methods', 'methods parameters',
'methods source 1', 'methods source 2',
'parameters', 'values', 'limb darkening u'}
keys = set(self._model_parameters.keys())
not_allowed = keys - allowed
if len(not_allowed) > 0:
raise ValueError(
'model keyword is a dict with keys not allowed: ' +
str(not_allowed))
for key in {'methods', 'methods source 1', 'methods source 2'}:
if key in self._model_parameters:
self._model_parameters[key] = self._parse_methods(