forked from martinmcallister/lfit_python
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathCVModel.py
More file actions
1077 lines (876 loc) · 35.3 KB
/
CVModel.py
File metadata and controls
1077 lines (876 loc) · 35.3 KB
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
"""
Subclasses from the `model` module, that actually comprise the tree
structure of the model fed to emcee. The `trunk` is an LCModel or GPLCModel
node, with child Bands, that have XEclipse leaves to evaluate the CV lightcurve
fit to the data. Data is stored in the Lightcurve class.
"""
import os
import configobj
import george
import lfit
import numpy as np
from trm import roche
from model import Node, Param, extract_par_and_key
BIG = 9e99
class Lightcurve:
"""This object keeps track of the observational data.
Can be generated from a file, with Lightcurve.from_calib(file)."""
def __init__(self, name, x, y, ye, w=None):
"""Here, hold this."""
self.name = name
self.fname = None
if w is None:
w = np.mean(np.diff(x)) * np.ones_like(x) / 2.0
self.x = x
self.y = y
self.ye = ye
self.w = w
@property
def n_data(self):
return self.x.shape[0]
@classmethod
def from_calib(cls, fname, name=None):
"""Read in a calib file, of the format;
phase flux error
and treat lines with # as commented out.
"""
delimiters = " ,|"
for delimiter in delimiters:
try:
data = np.loadtxt(fname, delimiter=delimiter, comments="#")
break
except ValueError:
print(
"Couldn't split the calib file with '{}', trying next delimiter...".format(
delimiter
)
)
try:
phase, flux, error = data.T
except ValueError:
# we probably have a Tseries style file
phase, _, flux, error, bmask = data.T
# Filter out nans.
mask = np.where(np.isnan(flux) == 0)
phase = phase[mask]
flux = flux[mask]
error = error[mask]
width = np.mean(np.diff(phase)) * np.ones_like(phase) / 2.0
# Set the name of this eclipse as the filename of the data file.
if name is None:
_, name = os.path.split(fname)
lc = cls(name, phase, flux, error, width)
lc.fname = fname
return lc
def trim(self, lo, hi):
"""Trim the data, so that all points are in the x range lo > xi > hi"""
xt = self.x
mask = (xt > lo) & (xt < hi)
self.x = self.x[mask]
self.y = self.y[mask]
self.ye = self.ye[mask]
self.w = self.w[mask]
# Subclasses.
class SimpleEclipse(Node):
"""Subclass of Node, specifically for storing a single eclipse.
Uses the simple BS model.
Lightcurve data is stored on this level.
Inputs:
-------
lightcurve; Lightcurve:
A Lightcurve object, containing data
label; str:
A label to apply to the node. Mostly used when searching trees.
parameter_objects; list(Param), or Param:
The parameter objects that correspond to this node. Single Param is
also accepted.
parent; Node, optional:
The parent of this node.
children; list(Node), or Node:
The children of this node. Single Node is also accepted
"""
# Define this subclasses parameters
node_par_names = ("dFlux", "sFlux", "rdisc", "scale", "az", "fis", "dexp", "phi0")
def __init__(self, lightcurve, *args, **kwargs):
super().__init__(*args, **kwargs)
# If the lightcurve is a Lightcurve object, save it. Otherwise,
# read it in from the file.
if isinstance(lightcurve, Lightcurve):
self.lc = lightcurve
elif isinstance(lightcurve, str):
self.lc = Lightcurve.from_calib(lightcurve)
else:
msg = "Argument lightcurve is not a string or Lightcurve! "
msg += "Got {}".format(lightcurve)
raise TypeError(msg)
# Create the CV object
self.cv = lfit.CV(self.cv_parlist)
self.log(
"SimpleEclipse.__init__", "Successfully ran the SimpleEclipse initialiser."
)
def calcFlux(self):
"""Fetch the CV parameter vector, and generate it's model lightcurve"""
self.log("SimpleEclipse.calcFlux", "Doing calcFlux")
# Get the model CV lightcurve across our data.
try:
flx = self.cv.calcFlux(self.cv_parlist, self.lc.x, self.lc.w)
except Exception as e:
print(repr(e))
msg = "Error: {}; parlist: {}".format(str(e), repr(self.cv_parlist))
print(msg)
self.log("SimpleEclipse.calcFlux", msg)
flx = np.nan
self.log(
"SimpleEclipse.calcFlux",
"Computed a lightcurve flux: \n{}\n\n\n".format(flx),
)
return flx
def calcComponents(self):
"""Return a list of the component fluxes as well as the total
returns:
(tot_flx, wdFlux, sFlux, rsFlux, dFlux)
"""
flx = self.cv.calcFlux(self.cv_parlist, self.lc.x, self.lc.w)
return flx, self.cv.ywd, self.cv.ys, self.cv.yrs, self.cv.yd
def chisq(self):
"""Return the chisq of this eclipse, given current params."""
self.log("SimpleEclipse.chisq", "Doing chisq")
flx = self.calcFlux()
# If the model gets any nans, return inf
if np.any(np.isnan(flx)):
if self.DEBUG:
print(
"Node returned some ({}/{} data) nans.".format(
np.sum(np.isnan(flx)), flx.shape[0]
)
)
self.log(
"SimpleEclipse.chisq",
"I computed a flux that contains nans. Returning an inf chisq.",
)
return BIG
# Calculate the chisq of this model.
chisq = ((self.lc.y - flx) / self.lc.ye) ** 2
chisq = np.sum(chisq)
self.log("SimpleEclipse.chisq", "Computed a chisq of {}".format(chisq))
return chisq
def ln_like(self):
"""Calculate the chisq of this eclipse, against the data stored in its
lightcurve object.
If plot is True, also plot the data in a figure."""
self.log("SimpleEclipse.ln_like", "Returning an ln_like that is (-0.5 * chisq)")
chisq = self.chisq()
self.log(
"SimpleEclipse.ln_like", "Returning a ln_like of {}".format(-0.5 * chisq)
)
return -0.5 * chisq
def ln_prior(self, verbose=False, *args, **kwargs):
"""At the eclipse level, three constrains must be validated for each
leaf of the tree.
- Is the disc large enough to precess? We can't handle superhumping!
- Is the BS scale unreasonably large, enough to cause the disc model
to be inaccurate?
- Is the azimuth of the BS out of range?
If other constraints on the level of the individual eclipse are
necessary, they should go in here.
"""
self.log(
"SimpleEclipse.ln_prior", "Checking that the values construct a valid CV!"
)
# Before we start, I'm going to collect the necessary parameters. By
# only calling this once, we save a little effort.
ancestor_param_dict = self.ancestor_param_dict
##############################################
# ~~ Is the disc large enough to precess? ~~ #
##############################################
# Defined the maximum size of the disc before it starts precessing, as
# a fraction of Roche Radius
rdisc_max_a = 0.46
# get the location of the L1 point from q
q = ancestor_param_dict["q"].currVal
try:
xl1 = roche.xl1(q)
except AssertionError:
if verbose:
print("Failed to get the L1 point!")
return -np.inf
# Get the rdisc, scaled to the Roche Radius
rdisc = ancestor_param_dict["rdisc"].currVal
rdisc_a = rdisc * xl1
if verbose:
print("rDisc: {:.4f} || Max: {:.4f}".format(rdisc_a, rdisc_max_a))
if rdisc_a > rdisc_max_a:
if verbose:
msg = "The disc radius of {} is large enough to precess! Value: {:.3f}".format(
self.name, rdisc
)
print(msg)
self.log(
"SimpleEclipse.ln_prior",
"The disc radius is too large. Returning ln_prior = -np.inf",
)
return -np.inf
##############################################
# ~~~~~ Is the BS scale physically OK? ~~~~~ #
##############################################
# We're gonna check to see if the BS scle is #
# in the range rwd/4 < (BS scale) < rwd*3. #
# If it isn't, then it's either too #
# concentrated to make sense, or so large #
# that our approximation of a smooth disc is #
# no longer a good idea. #
#
# We'll also check the BS scale is less than #
# 1/10th, since otherwise the bright spot #
# might extend more than one separation from #
# the spot position. #
##############################################
# Get the WD radius.
rwd = ancestor_param_dict["rwd"].currVal
# Enforce the BS scale being within these limits
rmax = min(1 / 10, rwd * 4.0)
rmin = rwd / 4.0
scale = ancestor_param_dict["scale"].currVal
if verbose:
print("Scale: {:.4f} || Limits: {:.4f} -> {:.4f}".format(scale, rmin, rmax))
if scale > rmax or scale < rmin:
if verbose:
print(
"Leaf {} has a BS scale that lies outside valid range!".format(
self.name
)
)
print("Rwd: {:.3f}".format(rwd))
print("Scale: {:.3f}".format(scale))
print("Range: {:.3f} - {:.3f}".format(rmin, rmax))
self.log(
"SimpleEclipse.ln_prior",
"The BS is too large to be accurately modelled. Returning ln_prior = -np.inf",
)
return -np.inf
##############################################
# ~~~~~ Does the stream miss the disc? ~~~~~ #
##############################################
azimuth_slop = 45.0
try:
# q, rdisc_a were previously retrieved
az = ancestor_param_dict["az"].currVal
# If the stream does not intersect the disc, this throws an error
x, y, _, _ = roche.bspot(q, rdisc_a)
# Find the tangent to the disc
alpha = np.degrees(np.arctan2(y, x))
# If alpha is negative, the BS lags the disc.
# However, the angle has to be less than 90 still!
if alpha < 0:
alpha = 90 - alpha
# Disc tangent
tangent = alpha + 90
# Calculate the min and max azimuths, using the tangent and slop
minaz = max(0, tangent - azimuth_slop)
maxaz = min(178, tangent + azimuth_slop)
if az < minaz or az > maxaz:
if verbose:
print(
"Leaf {} has an azimuth out of tolerance! Az: {:.3f}, min: {:.3f}, max: {:.3f}".format(
self.name, az, minaz, maxaz
)
)
self.log(
"SimpleEclipse.ln_prior",
"Azimuth is out of range. Returning ln_prior = -np.inf",
)
return -np.inf
except Exception as err:
if verbose:
print(err)
print(
"The mass stream of leaf {} does not intersect the disc!".format(
self.name
)
)
self.log(
"SimpleEclipse.ln_prior",
"The mass stream does not intersect the disc, returning ln_prior = -np.inf",
)
return -np.inf
self.log(
"SimpleEclipse.ln_prior", "Passed validity checks at {}.".format(self.name)
)
# If we pass all that, then calculate the ln_prior normally
lnp = super().ln_prior(verbose=verbose, *args, **kwargs)
self.log("SimpleEclipse.ln_prior", "Computed a ln_prior of {}".format(lnp))
return lnp
@property
def cv_parnames(self):
names = [
"wdFlux",
"dFlux",
"sFlux",
"rsFlux",
"q",
"dphi",
"rdisc",
"ulimb",
"rwd",
"scale",
"az",
"fis",
"dexp",
"phi0",
]
return names
@property
def cv_parlist(self):
"""Construct the parameter list needed by the CV"""
par_name_list = self.cv_parnames
param_dict = self.ancestor_param_dict
try:
parlist = [param_dict[key].currVal for key in par_name_list]
except KeyError as error:
print("Parameter dict:")
print("{")
for key, val in param_dict.items():
print(" {}: {}".format(key, val))
print("}")
raise error
self.log(
"SimpleEclipse.cv_parlist",
"Constructed a cv_parlist of:\n{}".format(parlist),
)
return parlist
class ComplexEclipse(SimpleEclipse):
"""Subclass of Node, specifically for storing a single eclipse.
Uses the complex BS model.
Lightcurve data is stored on this level.
Inputs:
-------
lightcurve; Lightcurve:
A Lightcurve object, containing data
label; str:
A label to apply to the node. Mostly used when searching trees.
parameter_objects; list(Param), or Param:
The parameter objects that correspond to this node. Single Param is
also accepted.
parent; Node, optional:
The parent of this node.
children; list(Node), or Node:
The children of this node. Single Node is also accepted
"""
node_par_names = (
"dFlux",
"sFlux",
"rdisc",
"scale",
"az",
"fis",
"dexp",
"phi0",
"exp1",
"exp2",
"yaw",
"tilt",
)
@property
def cv_parnames(self):
names = [
"wdFlux",
"dFlux",
"sFlux",
"rsFlux",
"q",
"dphi",
"rdisc",
"ulimb",
"rwd",
"scale",
"az",
"fis",
"dexp",
"phi0",
"exp1",
"exp2",
"tilt",
"yaw",
]
return names
def ln_prior(self, verbose=False, *args, **kwargs):
"""Constraints on the prior for a single eclipse are handled here."""
self.log(
"ComplexEclipse.ln_prior", "Checking that the values construct a valid CV!"
)
# Before we start, I'm going to collect the necessary parameters. By
# only calling this once, we save a little effort.
ancestor_param_dict = self.ancestor_param_dict
##############################################
# ~~~~ Is the bright spot max sensible? ~~~~ #
##############################################
# The location of the bright spot max is #
# (in units of scale lengths) given by #
# pow(exp1/exp2, 1/exp2) #
exp1 = ancestor_param_dict["exp1"].currVal
exp2 = ancestor_param_dict["exp2"].currVal
bs_max = pow(exp1 / exp2, 1 / exp2)
if bs_max > 5:
msg = "The bright spot max is more than 5 scale lengths from impact region, returning ln_prior = -np.inf"
self.log("ComplexEclipse.ln_prior", msg)
return -np.inf
# we're OK - check the params in common with simpleeclispes
return SimpleEclipse.ln_prior(self, verbose=verbose, *args, **kwargs)
class Band(Node):
"""Subclass of Node, specific to observation bands. Contains the eclipse
objects taken in this band.
Inputs:
-------
label; str:
A label to apply to the node. Mostly used when searching trees.
parameter_objects; list(Param), or Param:
The parameter objects that correspond to this node. Single Param is
also accepted.
parent; Node, optional:
The parent of this node.
children; list(Node), or Node:
The children of this node. Single Node is also accepted
"""
# What kind of parameters are we storing here?
node_par_names = ("wdFlux", "rsFlux", "ulimb")
@property
def eclipses(self):
return list(self.search_node_type("Eclipse"))
class LCModel(Node):
"""Top layer Node class. Contains Bands, which contain Eclipses.
Inputs:
-------
label; str:
A label to apply to the node. Mostly used when searching trees.
parameter_objects; list(Param), or Param:
The parameter objects that correspond to this node. Single Param is
also accepted.
parent; Node, optional:
The parent of this node.
children; list(Node), or Node:
The children of this node. Single Node is also accepted
"""
# Set the parameter names for this layer
node_par_names = ("q", "dphi", "rwd")
@property
def eclipses(self):
return list(self.search_node_type("Eclipse"))
def ln_prior(self, verbose=False):
"""Before we calculate the ln_prior of myself or my children, I check
that my parameters are valid. I check that dphi is not too large for
the current value of q.
If other constraints on the core parameters become necessary, they
should go here. If these tests fail, -np.inf is immediately returned.
"""
self.log("LCModel.ln_prior", "Checking global parameters for validity.")
lnp = 0.0
# Check that dphi is within limits
tol = 1e-6
dphi = getattr(self, "dphi").currVal
q = getattr(self, "q").currVal
if q <= 0:
return -np.inf
try:
# Get the value of dphi that we WOULD have at an
# inclination of 90 degrees
maxphi = roche.findphi(q, 90.0)
# If dphi is out of range, return negative inf.
if dphi > (maxphi - tol):
if verbose:
msg = "{} has a dphi out of tolerance!\nq: {:.3f}"
msg += "\ndphi: {:.3f}, max: {:.3f} - {:.3g}"
msg += "\nReturning inf.\n\n"
msg.format(self.name, q, dphi, maxphi, tol)
print(msg)
self.log(
"LCModel.ln_prior",
"dphi is out of range. Returning ln_prior = -np.inf",
)
return -np.inf
except Exception as error:
# If we get here, then roche couldn't find a dphi for this q.
# That's bad!
if verbose:
msg = "Failed to calculate a value of dphi at node {} || Exception: {}"
print(msg.format(self.name, repr(error)))
self.log(
"LCModel.ln_prior",
"Failed to calculate a value of dphi. Returning ln_prior = -np.inf",
)
return -np.inf
self.log("LCModel.ln_prior", "Passed parameter value validity checks.")
# Then, if we pass this, move on to the 'normal' ln_prior calculation.
lnp += super().ln_prior(verbose=verbose)
self.log("LCModel.ln_prior", "Returning a ln_prior of {}".format(lnp))
return lnp
class GPLCModel(LCModel):
"""This is a subclass of the LCModel class. It uses the Gaussian Process.
This version will, rather than evaluating via chisq, evaluate the
likelihood of the model by calculating the residuals between the model
and the data and computing the likelihood of those data given certain
Gaussian Process hyper-parameters.
These parameters require some explaination.
tau_gp:
the timescale of the covariance matrix
ln_ampin_gp:
The base amplitude of the covariance matrix
ln_ampout_gp:
The additional amplitude of the covariance matrix, when the WD
is visible
"""
# Add the GP params
node_par_names = LCModel.node_par_names
node_par_names += ("ln_ampin_gp", "ln_ampout_gp", "tau_gp")
class SimpleGPEclipse(SimpleEclipse):
# Set the initial values of q, rwd, and dphi. These will be used to
# caclulate the location of the GP changepoints. Setting to initially
# unrealistically high values will ensure that the first time
# calcChangepoints is called, the changepoints are calculated.
_olddphi = 9e99
_oldq = 9e99
_oldrwd = 9e99
# _dist_cp is initially set to whatever, it will be overwritten anyway.
_dist_cp = 9e99
def calcChangepoints(self):
"""Caclulate the WD ingress and egresses, i.e. where we want to switch
on or off the extra GP amplitude.
Requires an eclipse object, since this is specific to a given phase
range.
"""
self.log("SimpleGPEclipse.calcChangepoints", "Calculating GP changepoints")
# Also get object for dphi, q and rwd as this is required to determine
# changepoints
pardict = self.ancestor_param_dict
dphi = pardict["dphi"]
q = pardict["q"]
rwd = pardict["rwd"]
phi0 = pardict["phi0"]
# Have they changed significantly?
# If not, dont bother recalculating dist_cp
dphi_change = np.fabs(self._olddphi - dphi.currVal) / dphi.currVal
q_change = np.fabs(self._oldq - q.currVal) / q.currVal
rwd_change = np.fabs(self._oldrwd - rwd.currVal) / rwd.currVal
# Check to see if our model parameters have changed enough to
# significantly change the location of the changepoints.
if (dphi_change > 1.2) or (q_change > 1.2) or (rwd_change > 1.2):
self.log(
"SimpleGPEclipse.calcChangepoints",
"The GP changepoint locations have chnged significantly enough to warrant a recalculation...",
)
# Calculate inclination
inc = roche.findi(q.currVal, dphi.currVal)
# Calculate wd contact phases 3 and 4
phi3, phi4 = roche.wdphases(q.currVal, inc, rwd.currVal, ntheta=10)
# Calculate length of wd egress
dpwd = phi4 - phi3
# Distance from changepoints to mideclipse
dist_cp = (dphi.currVal + dpwd) / 2.0
# save these values for speed
self._dist_cp = dist_cp
self._oldq = q.currVal
self._olddphi = dphi.currVal
self._oldrwd = rwd.currVal
else:
self.log("SimpleGPEclipse.calcChangepoints", "Using old values of dist_cp")
# Use the old values
dist_cp = self._dist_cp
# Find location of all changepoints
min_ecl = int(np.floor(self.lc.x.min()))
max_ecl = int(np.ceil(self.lc.x.max()))
eclipses = [
e
for e in range(min_ecl, max_ecl + 1)
if np.logical_and(e > self.lc.x.min(), e < 1 + self.lc.x.max())
]
changepoints = []
for e in eclipses:
# When did the last eclipse end?
egress = (e - 1) + dist_cp + phi0.currVal
# When does this eclipse start?
ingress = e - dist_cp + phi0.currVal
changepoints.append([egress, ingress])
self.log(
"SimpleGPEclipse.calcChangepoints",
"Computed GP changepoints as:\n{}".format(changepoints),
)
return changepoints
def create_GP(self):
"""Constructs a kernel, which is used to create Gaussian processes.
Creates kernels for both inside and out of eclipse,
works out the location of any changepoints present, constructs a single
(mixed) kernel and uses this kernel to create GPs
Requires an Eclipse object to create the GP for."""
self.log("SimpleGPEclipse.create_GP", "Creating a new GP")
# Get objects for ln_ampin_gp, ln_ampout_gp, tau_gp and find the exponential
# of their current values
pardict = self.ancestor_param_dict
ln_ampin = pardict["ln_ampin_gp"]
ln_ampout = pardict["ln_ampout_gp"]
tau = pardict["tau_gp"]
ampin_gp = np.exp(ln_ampin.currVal)
ampout_gp = np.exp(ln_ampout.currVal)
tau_gp = tau.currVal
# Calculate kernels for both out of and in eclipse WD eclipse
# Kernel inside of WD has smaller amplitude than that of outside
# eclipse.
# First, get the changepoints
changepoints = self.calcChangepoints()
# We need to make a fairly complex kernel.
# Global flicker
self.log("SimpleGPEclipse.create_GP", "Constructing a new kernel")
kernel = ampin_gp * george.kernels.Matern32Kernel(tau_gp**2)
# inter-eclipse flicker
for gap in changepoints:
kernel += ampout_gp * george.kernels.Matern32Kernel(tau_gp**2, block=gap)
# Use that kernel to make a GP object
georgeGP = george.GP(kernel, solver=george.HODLRSolver)
self.log("SimpleGPEclipse.create_GP", "Successfully created a new GP!")
return georgeGP
def ln_like(self):
"""The GP sits at the top of the tree. It replaces the LCModel
class. When the evaluate function is called, this class should
hijack it, calculate the residuals of all the eclipses in the tree,
and find the likelihood of each of those residuals given the current GP
hyper-parameters.
Inputs:
-------
label; str:
A label to apply to the node. Mostly used when searching trees.
parameter_objects; list(Param), or Param:
The parameter objects that correspond to this node. Single Param is
also accepted.
parent; Node, optional:
The parent of this node.
children; list(Node), or Node:
The children of this node. Single Node is also accepted
"""
self.log("SimpleGPEclipse.ln_like", "Computing ln_like for a GP")
# For each eclipse, I want to know the log likelihood of its residuals
gp_ln_like = 0.0
# Get the residuals of the model
residuals = self.lc.y - self.calcFlux()
# Did the model turn out ok?
if np.any(np.isinf(residuals)) or np.any(np.isnan(residuals)):
if self.DEBUG:
msg = "GP ln_like computed inf or nan residuals for the model. Returning -np.inf for the likelihood."
self.log("SimpleGPEclipse.ln_like", msg)
return -np.inf
# Create the GP of this eclipse
gp = self.create_GP()
# Compute the GP
gp.compute(self.lc.x, self.lc.ye)
# The 'quiet' argument tells the GP to return -inf when you get
# an invalid kernel, rather than throwing an exception.
gp_ln_like = gp.log_likelihood(residuals, quiet=True)
if self.DEBUG:
self.log(
"SimpleGPEclipse.ln_like",
"GP computed a ln_like of {}".format(gp_ln_like),
)
return gp_ln_like
class ComplexGPEclipse(SimpleGPEclipse):
# Exactly as the simple GP Eclipse, but this time with the extra 4 params.
node_par_names = ComplexEclipse.node_par_names
@property
def cv_parnames(self):
names = [
"wdFlux",
"dFlux",
"sFlux",
"rsFlux",
"q",
"dphi",
"rdisc",
"ulimb",
"rwd",
"scale",
"az",
"fis",
"dexp",
"phi0",
"exp1",
"exp2",
"tilt",
"yaw",
]
return names
def construct_model(input_file, debug=False, nodata=False):
"""Takes an input filename, and parses it into a model tree.
Inputs:
-------
input_file, str:
The input.dat file to be parsed
debug, bool:
Enable the debugging flag for the Nodes. Debugging will be written to
a file.
Output:
-------
model root node
"""
input_dict = configobj.ConfigObj(input_file)
# Do we use the complex model? Do we use the GP?
is_complex = bool(int(input_dict["complex"]))
use_gp = bool(int(input_dict["useGP"]))
# neclipses no longer strictly necessary, but can be used to limit the
# maximum number of fitted eclipses
try:
neclipses = int(input_dict["neclipses"])
except KeyError:
# Read in all the available eclipses
neclipses = 9999
if debug:
print("Input Dict yielded the following immediately interesting params:")
print("is_complex: ", is_complex)
print("use_gp: ", use_gp)
print("neclipses: ", neclipses)
print()
# # # # # # # # # # # # # # # # #
# Get the initial model setup # #
# # # # # # # # # # # # # # # # #
# Start by creating the overall Node. Gather the parameters:
if use_gp:
core_par_names = GPLCModel.node_par_names
core_pars = [
Param.fromString(name, input_dict[name]) for name in core_par_names
]
if debug:
print("Using the GP!")
print("Core params:")
for par, val in zip(core_par_names, core_pars):
print(" -> par: {:<10s} -- value: {:.3f}".format(par, val.currVal))
# and make the model object with no children
model = GPLCModel("core", core_pars, DEBUG=debug)
else:
core_par_names = LCModel.node_par_names
core_pars = [
Param.fromString(name, input_dict[name]) for name in core_par_names
]
if debug:
print("Not using the GP!")
print("Core params:")
for par, val in zip(core_par_names, core_pars):
print(" -> par: {:<10s} -- value: {:.3f}".format(par, val.currVal))
# and make the model object with no children
model = LCModel("core", core_pars, DEBUG=debug)
# # # # # # # # # # # # # # # # #
# # # Now do the band names # # #
# # # # # # # # # # # # # # # # #
# Collect the bands and their params. Add them total model.
band_par_names = Band.node_par_names
if debug:
print("\nThe bands have these parameters: {}".format(band_par_names))
if not use_gp:
# Use the Eclipse class to find the parameters we're interested in
if is_complex:
ecl_pars = ComplexEclipse.node_par_names
if debug:
print("Using the complex BS model")
else:
ecl_pars = SimpleEclipse.node_par_names
if debug:
print("Using the simple BS model")
else:
# Use the Eclipse class to find the parameters we're interested in
if is_complex:
ecl_pars = ComplexGPEclipse.node_par_names
if debug:
print("Using the complex BS model, with a gaussian process")
else:
ecl_pars = SimpleGPEclipse.node_par_names
if debug:
print("Using the simple BS model, with a gaussian process")
# I care about the order in which eclipses and bands are defined.
# Collect that order here.
defined_bands = []
defined_eclipses = []
with open(input_file, "r") as input_file_obj:
for line in input_file_obj:
line = line.strip().split()
if len(line):
key = line[0]
# Check that the key starts with any of the band pars
if np.any([key.startswith(par) for par in band_par_names]):
# Strip off the variable part, and keep only the label
_, key = extract_par_and_key(key)
if key not in defined_bands:
defined_bands.append(key)
# Check that the key starts with any of the eclipse pars
if np.any([key.startswith(par) for par in ecl_pars]):
# Strip off the variable part, and keep only the label
_, key = extract_par_and_key(key)
if key not in defined_eclipses:
defined_eclipses.append(key)
if debug:
print("\nI found the following bands defined in the input dict:")
print(defined_bands)
print("\nI found the following eclipses defined in the input dict:")
print(defined_eclipses)
# Collect the band params into their Band objects.
for label in defined_bands:
band_pars = []
# Build the Param objects for this band
for par in band_par_names:
# Construct the parameter key and retrieve the string
key = "{}_{}".format(par, label)
string = input_dict[key]