• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cylammarco / WDPhotTools / 18437379572

22 Sep 2025 03:09PM UTC coverage: 96.39% (-0.3%) from 96.668%
18437379572

push

github

web-flow
Merge pull request #40 from cylammarco/dev

Dev

133 of 139 new or added lines in 4 files covered. (95.68%)

12 existing lines in 1 file now uncovered.

3177 of 3296 relevant lines covered (96.39%)

1.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.82
/src/WDPhotTools/cooling_model_reader.py
1
#!/usr/bin/env python3
2
# -*- coding: utf-8 -*-
3

4
"""Handling the formatting of different cooling models"""
2✔
5

6
import io
2✔
7
import glob
2✔
8
import os
2✔
9

10
import numpy as np
2✔
11
from scipy.interpolate import CloughTocher2DInterpolator
2✔
12
from scipy.interpolate import RBFInterpolator
2✔
13

14

15
class CoolingModelReader(object):
2✔
16
    """A reader object to handle the input of different cooling models"""
17

18
    def __init__(self):
2✔
19
        super(CoolingModelReader, self).__init__()
2✔
20

21
        self.this_file = os.path.dirname(os.path.abspath(__file__))
2✔
22

23
        self.model_list = {
2✔
24
            "montreal_co_da_20": "Bedard et al. 2020 CO DA",
25
            "montreal_co_db_20": "Bedard et al. 2020 CO DB",
26
            "lpcode_he_da_07": "Panei et al. 2007 He DA",
27
            "lpcode_he_da_09": "Althaus et al. 2009 He DA",
28
            "lpcode_co_da_07": "Panei et al. 2007 CO DA",
29
            "lpcode_co_da_10_z001": "Renedo et al. 2010 CO DA Z=0.01",
30
            "lpcode_co_da_10_z0001": "Renedo et al. 2010 CO DA Z=0.001",
31
            "lpcode_co_da_15_z00003": "Althaus et al. 2015 DA Z=0.00003",
32
            "lpcode_co_da_15_z0001": "Althaus et al. 2015 DA Z=0.0001",
33
            "lpcode_co_da_15_z0005": "Althaus et al. 2015 DA Z=0.0005",
34
            "lpcode_co_db_17_z00005": "Althaus et al. 2017 DB Y=0.4",
35
            "lpcode_co_db_17_z0001": "Althaus et al. 2017 DB Y=0.4",
36
            "lpcode_co_db_17": "Camisassa et al. 2017 DB",
37
            "lpcode_one_da_07": "Althaus et al. 2007 ONe DA",
38
            "lpcode_one_da_19": "Camisassa et al. 2019 ONe DA",
39
            "lpcode_one_db_19": "Camisassa et al. 2019 ONe DB",
40
            "lpcode_da_22": "Althaus et al. 2013 He DA, Camisassa et al. 2016 CO DA,  Camisassa et al. 2019 ONe DA",
41
            "lpcode_db_22": "Camisassa et al. 2017 CO DB, Camisassa et al. 2019 ONe DB",
42
            "basti_co_da_10": "Salaris et al. 2010 CO DA",
43
            "basti_co_db_10": "Salaris et al. 2010 CO DB",
44
            "basti_co_da_10_nps": "Salaris et al. 2010 CO DA, no phase separation",
45
            "basti_co_db_10_nps": "Salaris et al. 2010 CO DB, no phase separation",
46
            "mesa_one_da_18": "Lauffer et al. 2018 ONe DA",
47
            "mesa_one_db_18": "Lauffer et al. 2018 ONe DB",
48
        }
49

50
        self.low_mass_cooling_model_list = [
2✔
51
            "montreal_co_da_20",
52
            "montreal_co_db_20",
53
            "lpcode_he_da_07",
54
            "lpcode_co_da_07",
55
            "lpcode_he_da_09",
56
            "lpcode_da_22",
57
            None,
58
        ]
59

60
        self.intermediate_mass_cooling_model_list = [
2✔
61
            "montreal_co_da_20",
62
            "montreal_co_db_20",
63
            "lpcode_co_da_10_z001",
64
            "lpcode_co_da_10_z0001",
65
            "lpcode_co_da_15_z00003",
66
            "lpcode_co_da_15_z0001",
67
            "lpcode_co_da_15_z0005",
68
            "lpcode_co_db_17_z0001",
69
            "lpcode_co_db_17_z00005",
70
            "lpcode_co_da_17_y04",
71
            "lpcode_co_db_17",
72
            "lpcode_da_22",
73
            "lpcode_db_22",
74
            "basti_co_da_10",
75
            "basti_co_db_10",
76
            "basti_co_da_10_nps",
77
            "basti_co_db_10_nps",
78
            None,
79
        ]
80

81
        self.high_mass_cooling_model_list = [
2✔
82
            "montreal_co_da_20",
83
            "montreal_co_db_20",
84
            "lpcode_one_da_07",
85
            "lpcode_one_da_19",
86
            "lpcode_one_db_19",
87
            "lpcode_da_22",
88
            "lpcode_db_22",
89
            "basti_co_da_10",
90
            "basti_co_db_10",
91
            "basti_co_da_10_nps",
92
            "basti_co_db_10_nps",
93
            "mesa_one_da_18",
94
            "mesa_one_db_18",
95
            None,
96
        ]
97

98
        # Default to montreal_co_da_20
99
        self.cooling_models = {
2✔
100
            "low_mass_cooling_model": "montreal_co_da_20",
101
            "intermediate_mass_cooling_model": "montreal_co_da_20",
102
            "high_mass_cooling_model": "montreal_co_da_20",
103
        }
104

105
        self.mass = None
2✔
106
        self.age = None
2✔
107
        self.luminosity = None
2✔
108
        self.cooling_model_grid = None
2✔
109
        self.cooling_interpolator = None
2✔
110
        self.cooling_rate_interpolator = None
2✔
111
        self.dLdt = None
2✔
112

113
    def list_cooling_model(self, print_to_screen=True):
2✔
114
        """
115
        Print the formatted list of available cooling models.
116

117
        Parameters
118
        ----------
119
        print_to_screen: bool (Default: True)
120
            Set to True to print the list of cooling models to screen.
121

122
        Returns
123
        -------
124
        model_list:
125
            The names and references of the cooling models.
126

127
        """
128

129
        if print_to_screen:
2✔
130
            for i in self.model_list.items():
2✔
131
                print(f"Model: {i[0]}, Reference: {i[1]}")
2✔
132

133
        return self.model_list.items()
2✔
134

135
    def list_cooling_parameters(self, model, print_to_screen=True):
2✔
136
        """
137
        Print the formatted list of parameters available for the specified cooling models.
138

139
        Parameters
140
        ----------
141
        model: str
142
            Name of the cooling model as in the `model_list`.
143
        print_to_screen: bool (Default: True)
144
            Set to True to print the cooling model parameters to screen.
145

146
        Returns
147
        -------
148
        mass:
149
            WD mass available in the specified model.
150
        column_names:
151
            Available parameters in the specified model.
152
        column_units:
153
            Unites of the parameters in the specified model.
154

155
        """
156

157
        mass, _, column_names, column_units = self.get_cooling_model(model)
2✔
158

159
        if print_to_screen:
2✔
160
            print("Available WD mass: {mass}")
2✔
161

162
            for i, j in zip(column_names.items(), column_units.items()):
2✔
163
                print(f"Parameter: {i[1]}, Column Name: {i[0]}, Unit: {j[1]}")
2✔
164

165
        return mass, column_names.items(), column_units.items()
2✔
166

167
    def get_cooling_model(self, model, mass_range="all"):
2✔
168
        """
169
        Choose the specified cooling model for the chosen mass range.
170

171
        Parameters
172
        ----------
173
        model: str
174
            Name of the cooling model as in the `model_list`.
175
        mass_range: str (Default: 'all')
176
            The mass range in which the cooling model should return. The ranges are defined as <0.5, 0.5-1.0 and
177
            >1.0 solar masses.
178

179
        """
180

181
        if model in ["montreal_co_da_20", "montreal_co_db_20"]:
2✔
182
            mass, cooling_model, column_names, column_units = self._bedard20_formatter(model, mass_range)
2✔
183

184
        elif model in ["lpcode_he_da_07", "lpcode_co_da_07"]:
2✔
185
            mass, cooling_model, column_names, column_units = self._panei07_formatter(model)
2✔
186

187
        elif model == "lpcode_he_da_09":
2✔
188
            mass, cooling_model, column_names, column_units = self._althaus09_formatter(mass_range)
2✔
189

190
        elif model in ["lpcode_co_db_17_z00005", "lpcode_co_db_17_z0001"]:
2✔
191
            mass, cooling_model, column_names, column_units = self._althaus17_formatter(model, mass_range)
2✔
192

193
        elif model in ["lpcode_co_da_10_z001", "lpcode_co_da_10_z0001"]:
2✔
194
            mass, cooling_model, column_names, column_units = self._renedo10_formatter(model)
2✔
195

196
        elif model in ["lpcode_co_da_15_z00003", "lpcode_co_da_15_z0001", "lpcode_co_da_15_z0005"]:
2✔
197
            mass, cooling_model, column_names, column_units = self._althaus15_formatter(model)
2✔
198

199
        elif model == "lpcode_co_db_17":
2✔
200
            mass, cooling_model, column_names, column_units = self._camisassa17_formatter()
2✔
201

202
        elif model in ["basti_co_da_10", "basti_co_db_10", "basti_co_da_10_nps", "basti_co_db_10_nps"]:
2✔
203
            mass, cooling_model, column_names, column_units = self._salaris10_formatter(model, mass_range)
2✔
204

205
        elif model == "lpcode_one_da_07":
2✔
206
            mass, cooling_model, column_names, column_units = self._althaus07_formatter()
2✔
207

208
        elif model in ["lpcode_one_da_19", "lpcode_one_db_19"]:
2✔
209
            mass, cooling_model, column_names, column_units = self._camisassa19_formatter(model)
2✔
210

211
        elif model in ["mesa_one_da_18", "mesa_one_db_18"]:
2✔
212
            mass, cooling_model, column_names, column_units = self._lauffer18_formatter(model)
2✔
213

214
        elif model == "lpcode_da_22":
2✔
215
            mass, cooling_model, column_names, column_units = self._lpcode22_da_formatter()
2✔
216

217
        elif model == "lpcode_db_22":
2✔
218
            mass, cooling_model, column_names, column_units = self._lpcode22_db_formatter()
2✔
219

220
        elif model is None:
2✔
221
            mass = np.array(())
2✔
222
            cooling_model = np.array(())
2✔
223
            column_names = {}
2✔
224
            column_units = {}
2✔
225

226
        else:
227
            raise ValueError("Invalid model name.")
2✔
228

229
        return mass, cooling_model, column_names, column_units
2✔
230

231
    def _althaus07_formatter(self):
2✔
232
        """
233
        A formatter to load the Althaus et al. 2007 WD cooling model
234

235
        """
236

237
        filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/althaus07/*.dat"))
2✔
238

239
        # Prepare the array column dtype
240
        column_key = np.array(
2✔
241
            (
242
                "lum",
243
                "logg",
244
                "B-V",
245
                "V-R",
246
                "V-K",
247
                "R-I",
248
                "J-H",
249
                "H-K",
250
                "V-I",
251
                "U-V",
252
                "BC",
253
                "dmag_v",
254
                "age",
255
            )
256
        )
257
        column_key_formatted = np.array(
2✔
258
            (
259
                "Luminosity",
260
                "log(g)",
261
                r"$B-V$",
262
                r"$V-R$",
263
                r"$V-K$",
264
                r"$R-I$",
265
                r"$J-H$",
266
                r"$H-K$",
267
                r"$V-I$",
268
                r"$U-V$",
269
                "$Bolometric Correction$",
270
                r"$V$",
271
                "$log(Age)$",
272
            )
273
        )
274
        column_key_unit = np.array([r"L$_{\odot}$", "(cgs)"] + ["mag"] * 10 + ["(yr)"])
2✔
275
        column_type = np.array(([np.float64] * len(column_key)))
2✔
276
        dtype = list(zip(column_key, column_type))
2✔
277

278
        column_names = {}
2✔
279
        column_units = {}
2✔
280
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
281
            column_names[i] = j
2✔
282
            column_units[i] = k
2✔
283

284
        # Get the mass from the file name
285
        mass = np.array([i.split("_")[-1][:3] for i in filelist]).astype(np.float64) / 100000.0
2✔
286

287
        # Create an empty array for holding the cooling models
288
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
289

290
        for i, filepath in enumerate(filelist):
2✔
291
            cooling_model[i] = np.loadtxt(filepath, skiprows=1, dtype=dtype)
2✔
292

293
            # Convert the luminosity into erg/s
294
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
295

296
            # Convert the age to yr
297
            cooling_model[i]["age"] = 10.0 ** cooling_model[i]["age"]
2✔
298

299
        return mass, cooling_model, column_names, column_units
2✔
300

301
    def _althaus09_formatter(self, mass_range="all"):
2✔
302
        """
303
        A formatter to load the Althaus et al. 2009 WD cooling model
304

305
        Parameters
306
        ----------
307
        mass_range: str (Default: 'all')
308
            The mass range in which the cooling model should return. The ranges are defined as <0.5, 0.5-1.0 and
309
            >1.0 solar masses.
310

311
        """
312

313
        filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/althaus09/z.*"))
2✔
314

315
        # Prepare the array column dtype
316
        column_key = np.array(
2✔
317
            (
318
                "Teff",
319
                "logg",
320
                "lum",
321
                "age",
322
                "BC",
323
                "M_V",
324
                "U",
325
                "B",
326
                "V",
327
                "R",
328
                "I",
329
                "J",
330
                "H",
331
                "K",
332
                "L",
333
                "U-B",
334
                "B-V",
335
                "V-R",
336
                "V-K",
337
                "V-I",
338
                "R-I",
339
                "J-H",
340
                "H-K",
341
                "K-L",
342
            )
343
        )
344
        column_key_formatted = np.array(
2✔
345
            (
346
                r"T$_{\mathrm{eff}}$",
347
                "log(g)",
348
                "Luminosity",
349
                "$log(Age)$",
350
                "$Bolometric Correction$",
351
                r"$V$",
352
                r"$U$",
353
                r"$B$",
354
                r"$V$",
355
                r"$R$",
356
                r"$I$",
357
                r"$J$",
358
                r"$H$",
359
                r"$K$",
360
                r"$L$",
361
                r"$U-B$",
362
                r"$B-V$",
363
                r"$V-R$",
364
                r"$V-K$",
365
                r"$V-I$",
366
                r"$R-I$",
367
                r"$J-H$",
368
                r"$H-K$",
369
                r"$K-L$",
370
            )
371
        )
372
        column_key_unit = np.array(["K", r"(cm/s$^2$)", r"L$_{\odot}$", "(yr)"] + ["mag"] * 20)
2✔
373
        column_type = np.array(([np.float64] * len(column_key)))
2✔
374
        dtype = list(zip(column_key, column_type))
2✔
375

376
        column_names = {}
2✔
377
        column_units = {}
2✔
378
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
379
            column_names[i] = j
2✔
380
            column_units[i] = k
2✔
381

382
        # Get the mass from the file name
383
        mass = np.array([i.split(".")[-2] for i in filelist]).astype(np.float64) / 100000.0
2✔
384

385
        if mass_range == "all":
2✔
386
            pass
2✔
387
        elif mass_range == "low":
2✔
388
            mask_low = mass < 0.5
2✔
389
            mass = mass[mask_low]
2✔
390
            filelist = np.array(filelist)[mask_low]
2✔
391
        else:
392
            raise ValueError("Unknown mass range requested. Please choose from 'all' or 'low' for althaus09 models.")
×
393

394
        # Create an empty array for holding the cooling models
395
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
396

397
        for i, filepath in enumerate(filelist):
2✔
398
            cooling_model[i] = np.loadtxt(filepath, dtype=dtype)
2✔
399

400
            # Convert the luminosity into erg/s
401
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
402

403
            # Convert the age to yr
404
            cooling_model[i]["age"] *= 1e9
2✔
405

406
        return mass, cooling_model, column_names, column_units
2✔
407

408
    def _althaus15_formatter(self, model):
2✔
409
        """
410
        A formatter to load the Althaus et al. 2015 WD cooling model
411

412
        Parameters
413
        ----------
414
        model: str
415
            Name of the cooling model as in the `model_list`.
416

417
        """
418

419
        # Z=0.00003 models
420
        if model == "lpcode_co_da_15_z00003":
2✔
421
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/althaus15/Z=3d-5/*.trk"))
2✔
422

423
        # Z=0.0001 models
424
        if model == "lpcode_co_da_15_z0001":
2✔
425
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/althaus15/Z=1d-4/*.trk"))
2✔
426

427
        # Z=0.0005 models
428
        if model == "lpcode_co_da_15_z0005":
2✔
429
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/althaus15/Z=5d-4/*.trk"))
2✔
430

431
        # Prepare the array column dtype
432
        column_key = np.array(
2✔
433
            (
434
                "lum",
435
                "Teff",
436
                "Tc",
437
                "Roc",
438
                "Hc",
439
                "Hec",
440
                "Con_s",
441
                "Con_c",
442
                "age",
443
                "mass",
444
                "mdot",
445
                "model_no",
446
                "Lpp",
447
                "Lcno",
448
                "LHe",
449
                "LCC",
450
                "dSdt",
451
                "Lnu",
452
                "MHtot",
453
                "HeBuf",
454
                "mass_Hfc",
455
                "mass_Hefc",
456
                "logg",
457
                "Rsun",
458
                "LH",
459
                "ps",
460
            )
461
        )
462
        column_key_formatted = np.array(
2✔
463
            (
464
                "Luminosity",
465
                r"log(T$_{\mathrm{eff}})$",
466
                r"T$_{\mathrm{c}}$",
467
                r"$\rho_c$",
468
                r"X$_c$",
469
                r"Y$_c$",
470
                "Outer Convective Zone",
471
                "Inner Convective Zone",
472
                "log(Age)",
473
                "Mass",
474
                "log(Rate of Change of Mass)",
475
                "Model Number",
476
                r"log($L_{PP}$)",
477
                r"log($L_{CNO}$)",
478
                r"log($L_{He}$)",
479
                r"log($L_{CC}$)",
480
                r"$\int\frac{\D{S}}{\D{t}}$",
481
                r"log($L_{\nu}$)",
482
                r"log(M$_{H, tot}$)",
483
                r"log(Mass$_{\mathrm{He Buffer}}$)",
484
                r"log(Mass$_{\mathrm{H-free Core}}$)",
485
                r"log(Mass$_{\mathrm{He-free Core}}$)",
486
                "log(g)",
487
                r"Radius",
488
                "Latent Heat",
489
                "Phase Separation",
490
            )
491
        )
492
        column_key_unit = np.array(
2✔
493
            [
494
                r"L$_{\odot}$",
495
                "(K)",
496
                r"($10^6$ K)",
497
                r"(g/cm$^3$)",
498
                "",
499
                "",
500
                "%",
501
                "%",
502
                "($10^6$ K)",
503
                r"M$_\odot$",
504
                r"(M$_\odot$ / yr)",
505
                "",
506
            ]
507
            + [r"L$_{\odot}$"] * 4
508
            + ["", r"L$_{\odot}$"]
509
            + [r"M$_{\odot}$"] * 4
510
            + [r"(cm/s$2^$)", r"R$_{\odot}$"]
511
            + ["erg/s"] * 2
512
        )
513
        column_type = np.array(([np.float64] * len(column_key)))
2✔
514
        dtype = list(zip(column_key, column_type))
2✔
515

516
        column_names = {}
2✔
517
        column_units = {}
2✔
518
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
519
            column_names[i] = j
2✔
520
            column_units[i] = k
2✔
521

522
        # Get the mass from the file name
523
        mass = np.array([i.split(".")[-2][-5:] for i in filelist]).astype(np.float64) / 100000.0
2✔
524

525
        # Create an empty array for holding the cooling models
526
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
527

528
        for i, filepath in enumerate(filelist):
2✔
529
            cooling_model[i] = np.loadtxt(filepath, skiprows=2, dtype=dtype)
2✔
530

531
            # Convert the luminosity into erg/s
532
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
533

534
            # Convert the age to yr
535
            cooling_model[i]["age"] = 10.0 ** cooling_model[i]["age"] * 1e6
2✔
536
            cooling_model[i]["age"] -= min(cooling_model[i]["age"])
2✔
537

538
        return mass, cooling_model, column_names, column_units
2✔
539

540
    def _althaus17_formatter(self, model, mass_range="all"):
2✔
541
        """
542
        A formatter to load the Althaus et al. 2017 WD cooling model
543

544
        Parameters
545
        ----------
546
        model: str
547
            Name of the cooling model as in the `model_list`.
548
        mass_range: str (Default: 'all')
549
            The mass range in which the cooling model should return. The ranges are defined as <0.5, 0.5-1.0 and
550
            >1.0 solar masses.
551

552
        """
553

554
        # Y=0.4, Z=0.001 models
555
        if model == "lpcode_co_db_17_z00005":
2✔
556
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/althaus17/*d4.trk"))
2✔
557

558
        # Y=0.4, Z=0.0005 models
559
        if model == "lpcode_co_db_17_z0001":
2✔
560
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/althaus17/*d3.trk"))
2✔
561

562
        # Prepare the array column dtype
563
        column_key = np.array(
2✔
564
            (
565
                "lum",
566
                "Teff",
567
                "Tc",
568
                "Roc",
569
                "Hc",
570
                "Hec",
571
                "Con_s",
572
                "Con_c",
573
                "age",
574
                "mass",
575
                "mdot",
576
                "model_no",
577
                "Lpp",
578
                "Lcno",
579
                "LHe",
580
                "LCC",
581
                "dSdt",
582
                "Lnu",
583
                "MHtot",
584
                "HeBuf",
585
                "mass_Hfc",
586
                "mass_Hefc",
587
                "logg",
588
                "Rsun",
589
                "LH",
590
                "ps",
591
            )
592
        )
593
        column_key_formatted = np.array(
2✔
594
            (
595
                "Luminosity",
596
                r"log(T$_{\mathrm{eff}})$",
597
                r"T$_{\mathrm{c}}$",
598
                r"$\rho_c$",
599
                r"X$_c$",
600
                r"Y$_c$",
601
                "Outer Convective Zone",
602
                "Inner Convective Zone",
603
                "log(Age)",
604
                "Mass",
605
                "log(Rate of Change of Mass)",
606
                "Model Number",
607
                r"log($L_{PP}$)",
608
                r"log($L_{CNO}$)",
609
                r"log($L_{He}$)",
610
                r"log($L_{CC}$)",
611
                r"$\int\frac{\D{S}}{\D{t}}$",
612
                r"log($L_{\nu}$)",
613
                r"log(M$_{H, tot}$)",
614
                r"Mass$_{\mathrm{He Buffer}}$",
615
                r"Mass$_{\mathrm{H-free Core}}$",
616
                r"Mass$_{\mathrm{He-free Core}}$",
617
                "log(g)",
618
                "Radius",
619
                "Latent Heat",
620
                "Phase Separation",
621
            )
622
        )
623
        column_key_unit = np.array(
2✔
624
            [
625
                r"L$_{\odot}$",
626
                "(K)",
627
                r"($10^6$ K)",
628
                r"(g/cm$^3$)",
629
                "",
630
                "",
631
                "%",
632
                "%",
633
                "($10^6$ K)",
634
                r"M$_\odot$",
635
                r"(M$_\odot$ / yr)",
636
                "",
637
            ]
638
            + [r"L$_{\odot}$"] * 4
639
            + ["", r"L$_{\odot}$"]
640
            + [r"M$_{\odot}$"] * 4
641
            + [r"(cm/s$^2$)", r"R$_{\odot}$"]
642
            + ["erg/s"] * 2
643
        )
644
        column_type = np.array(([np.float64] * len(column_key)))
2✔
645
        dtype = list(zip(column_key, column_type))
2✔
646

647
        column_names = {}
2✔
648
        column_units = {}
2✔
649
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
650
            column_names[i] = j
2✔
651
            column_units[i] = k
2✔
652

653
        # Get the mass from the file name
654
        mass = np.array([i.split(os.sep)[-1].split("_")[0] for i in filelist]).astype(np.float64)
2✔
655
        wd_mass = np.zeros_like(mass)
2✔
656

657
        # Create an empty array for holding the cooling models
658
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
659

660
        for i, filepath in enumerate(filelist):
2✔
661
            cooling_model[i] = np.loadtxt(filepath, skiprows=1, dtype=dtype)
2✔
662

663
            # Convert the luminosity into erg/s
664
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
665

666
            # Convert the age to yr
667
            wd_mass[i] = cooling_model[i]["mass"][0]
2✔
668
            cooling_model[i]["age"] = 10.0 ** cooling_model[i]["age"] * 1e6
2✔
669
            cooling_model[i]["age"] -= min(cooling_model[i]["age"])
2✔
670

671
        if mass_range == "all":
2✔
672
            pass
2✔
673
        elif mass_range == "low":
2✔
674
            mask_low = mass < 0.5
×
675
            wd_mass = wd_mass[mask_low]
×
676
            cooling_model = cooling_model[mask_low]
×
677
        elif mass_range == "intermediate":
2✔
678
            mask_intermediate = (mass >= 0.5) & (mass <= 1.0)
2✔
679
            wd_mass = wd_mass[mask_intermediate]
2✔
680
            cooling_model = cooling_model[mask_intermediate]
2✔
681
        else:
682
            raise ValueError(
×
683
                "Unknown mass range requested. Please choose from"
684
                "'all', 'low' or 'intermediate' for althaus17 models."
685
            )
686

687
        return wd_mass, cooling_model, column_names, column_units
2✔
688

689
    def _bedard20_formatter(self, model, mass_range="all"):
2✔
690
        """
691
        A formatter to load the Bedard et al. 2020 WD cooling model from
692
        http://www.astro.umontreal.ca/~bergeron/CoolingModels/
693

694
        The thick and thin models are for DA and DB WD, respectively.
695

696
        Parameters
697
        ----------
698
        model: str
699
            Name of the cooling model as in the `model_list`.
700
        mass_range: str (Default: 'all')
701
            The mass range in which the cooling model should return. The ranges are defined as <0.5, 0.5-1.0 and
702
            >1.0 solar masses.
703

704
        """
705

706
        # DA models
707
        if model == "montreal_co_da_20":
2✔
708
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/bedard20/*thick*"))
2✔
709

710
        # DB models
711
        if model == "montreal_co_db_20":
2✔
712
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/bedard20/*thin*"))
2✔
713

714
        # Prepare the array column dtype
715
        column_key = np.array(
2✔
716
            (
717
                "step",
718
                "Teff",
719
                "logg",
720
                "r",
721
                "age",
722
                "lum",
723
                "logTc",
724
                "logPc",
725
                "logrhoc",
726
                "MxM",
727
                "logqx",
728
                "lumnu",
729
                "logH",
730
                "logHe",
731
                "logC",
732
                "logO",
733
            )
734
        )
735
        column_key_formatted = np.array(
2✔
736
            (
737
                "Step",
738
                r"T$_{\mathrm{eff}}$",
739
                "log(g)",
740
                "Radius",
741
                "Age",
742
                "Luminosity",
743
                r"log(T$_{\mathrm{c}}$)",
744
                r"log(P$_{\mathrm{c}}$)",
745
                r"log($\rho_c$)",
746
                "Mass Fraction of Crystallisation",
747
                "Location of The Crystallization Front",
748
                r"$L_{\nu}$",
749
                r"log(Mass Fraction$_{H}$",
750
                r"log(Mass Fraction$_{He}$",
751
                r"log(Mass Fraction$_{C}$",
752
                r"log(Mass Fraction$_{O}$",
753
            )
754
        )
755
        column_key_unit = np.array(
2✔
756
            [
757
                "",
758
                "K",
759
                r"(cm/s$^2$)",
760
                "cm",
761
                "yr",
762
                "erg/s",
763
                "(K)",
764
                "(K)",
765
                r"(g/cm$^3$)",
766
            ]
767
            + [""] * 2
768
            + ["erg/s"]
769
            + [""] * 4
770
        )
771
        column_type = np.array(([np.float64] * len(column_key)))
2✔
772
        dtype = list(zip(column_key, column_type))
2✔
773

774
        column_names = {}
2✔
775
        column_units = {}
2✔
776
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
777
            column_names[i] = j
2✔
778
            column_units[i] = k
2✔
779

780
        # Get the mass from the file name
781
        mass = np.array([i.split("_")[2] for i in filelist]).astype(np.float64) / 100.0
2✔
782

783
        if mass_range == "all":
2✔
784
            pass
2✔
785
        elif mass_range == "low":
2✔
786
            mask_low = mass < 0.5
2✔
787
            mass = mass[mask_low]
2✔
788
            filelist = np.array(filelist)[mask_low]
2✔
789
        elif mass_range == "intermediate":
2✔
790
            mask_intermediate = (mass >= 0.5) & (mass <= 1.0)
2✔
791
            mass = mass[mask_intermediate]
2✔
792
            filelist = np.array(filelist)[mask_intermediate]
2✔
793
        elif mass_range == "high":
2✔
794
            mask_high = mass > 1.0
2✔
795
            mass = mass[mask_high]
2✔
796
            filelist = np.array(filelist)[mask_high]
2✔
797
        else:
798
            raise ValueError(
×
799
                "Unknown mass range requested. Please choose from 'all', 'low', 'intermediate' or 'high' for "
800
                "bedard20 models."
801
            )
802

803
        # Create an empty array for holding the cooling models
804
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
805

806
        for i, filepath in enumerate(filelist):
2✔
807
            with open(filepath, encoding="ascii") as infile:
2✔
808
                count = -5
2✔
809
                cooling_model_text = ""
2✔
810
                for line_i in infile:
2✔
811
                    count += 1
2✔
812

813
                    if count <= 0:
2✔
814
                        continue
2✔
815

816
                    if count % 3 != 0:
2✔
817
                        cooling_model_text += line_i.rstrip("\n")
2✔
818
                    else:
819
                        cooling_model_text += line_i
2✔
820

821
            cooling_model[i] = np.loadtxt(io.StringIO(cooling_model_text), dtype=dtype)
2✔
822

823
        return mass, cooling_model, column_names, column_units
2✔
824

825
    def _camisassa17_formatter(self):
2✔
826
        """
827
        A formatter to load the Camisassa et al. 2017 WD cooling model
828

829
        The progenitor lifetime is taken off based on the extrapolation from Table 1
830
        https://iopscience.iop.org/article/10.3847/0004-637X/823/2/158
831

832
        """
833

834
        # Y=0.4, Z=0.0005 models
835
        filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/camisassa17/*.trk"))
2✔
836

837
        # Prepare the array column dtype
838
        column_key = np.array(
2✔
839
            (
840
                "lum",
841
                "Teff",
842
                "Tc",
843
                "Roc",
844
                "Hc",
845
                "Hec",
846
                "Con_s",
847
                "Con_c",
848
                "age",
849
                "mass",
850
                "mdot",
851
                "model_no",
852
                "Lpp",
853
                "Lcno",
854
                "LHe",
855
                "LCC",
856
                "logG",
857
                "Lnu",
858
                "MHtot",
859
                "HeBuf",
860
                "mass_Hfc",
861
                "mass_Hefc",
862
                "logg",
863
                "Rsun",
864
                "LH",
865
                "SF",
866
            )
867
        )
868
        column_key_formatted = np.array(
2✔
869
            (
870
                "Luminosity",
871
                r"log(T$_{\mathrm{eff}})$",
872
                r"T$_{\mathrm{c}}$",
873
                r"$\rho_c$",
874
                r"X$_c$",
875
                r"Y$_c$",
876
                "Outer Convective Zone",
877
                "Inner Convective Zone",
878
                "log(Age)",
879
                "Mass",
880
                "log(Rate of Change of Mass)",
881
                "Model Number",
882
                r"log($L_{PP}$)",
883
                r"log($L_{CNO}$)",
884
                r"log($L_{He}$)",
885
                r"log($L_{CC}$)",
886
                r"log($L_{G}$)",
887
                r"log($L_{\nu}$)",
888
                r"log(M$_{H, tot}$)",
889
                r"log(HeBuf)",
890
                r"Mass$_{H-free Core}$",
891
                r"Mass$_{He-free Core}$",
892
                "log(g)",
893
                r"Radius",
894
                "Latent Heat",
895
                "Phase Separation",
896
            )
897
        )
898
        column_key_unit = np.array(
2✔
899
            [r"L$_{\odot}$", "(K)", r"($10^6$ K)", r"(g/cm$^3$)"]
900
            + [""] * 2
901
            + ["%"] * 2
902
            + [r"($10^6$ K)", r"M$_\odot$", r"(M$_\odot$ / yr)", ""]
903
            + [r"L$_{\odot}$"] * 6
904
            + [r"M$_{\odot}$"] * 4
905
            + [r"(cm/s$^2$)", r"R$_{\odot}$"]
906
            + ["erg/s"] * 2
907
        )
908
        column_type = np.array(([np.float64] * len(column_key)))
2✔
909
        dtype = list(zip(column_key, column_type))
2✔
910

911
        column_names = {}
2✔
912
        column_units = {}
2✔
913
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
914
            column_names[i] = j
2✔
915
            column_units[i] = k
2✔
916

917
        # Get the mass from the file name
918
        mass = np.array([i.split(os.sep)[-1][:3] for i in filelist]).astype(np.float64) / 100.0
2✔
919

920
        # Create an empty array for holding the cooling models
921
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
922

923
        for i, filepath in enumerate(filelist):
2✔
924
            cooling_model[i] = np.loadtxt(filepath, skiprows=1, dtype=dtype)
2✔
925

926
            # Convert the luminosity into erg/s
927
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
928

929
            # Convert the age to yr
930
            cooling_model[i]["age"] = 10.0 ** cooling_model[i]["age"] * 1e6
2✔
931
            cooling_model[i]["age"] -= min(cooling_model[i]["age"])
2✔
932

933
        return mass, cooling_model, column_names, column_units
2✔
934

935
    def _camisassa19_formatter(self, model):
2✔
936
        """
937
        A formatter to load the Camisassa et al. 2019 ultramassive WD cooling model.
938

939
        Some columns populated with 'I' are replaced with the nearest values.
940

941
        Parameters
942
        ----------
943
        model: str
944
            Name of the cooling model as in the `model_list`.
945

946
        """
947

948
        # DA model
949
        if model == "lpcode_one_da_19":
2✔
950
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/camisassa19/*hrich.dat"))
2✔
951

952
        # DB model
953
        if model == "lpcode_one_db_19":
2✔
954
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/camisassa19/*hdef.dat"))
2✔
955

956
        # Prepare the array column dtype
957
        column_key = np.array(
2✔
958
            (
959
                "lum",
960
                "Teff",
961
                "Tc",
962
                "Roc",
963
                "Hc",
964
                "Hec",
965
                "Con_s",
966
                "Con_c",
967
                "age",
968
                "mass",
969
                "mdot",
970
                "Lnu",
971
                "MHtot",
972
                "logg",
973
                "Rsun",
974
                "LH",
975
                "sf",
976
            )
977
        )
978
        column_key_formatted = np.array(
2✔
979
            (
980
                "Luminosity",
981
                r"log(T$_{\mathrm{eff}})$",
982
                r"T$_{\mathrm{c}}$",
983
                r"$\rho_c$",
984
                r"X$_c$",
985
                r"Y$_c$",
986
                "Outer Convective Zone",
987
                "Inner Convective Zone",
988
                "log(Age)",
989
                "Mass",
990
                "log(Rate of Change of Mass)",
991
                r"log($L_{\nu}$)",
992
                r"log(M$_{H, tot}$)",
993
                "log(g)",
994
                r"Radius",
995
                "Latent Heat",
996
                "Phase Separation",
997
            )
998
        )
999
        column_key_unit = np.array(
2✔
1000
            [r"L$_{\odot}$", "(K)", r"($10^6$ K)", r"(g/cm$^3$)"]
1001
            + [""] * 2
1002
            + ["%"] * 2
1003
            + [
1004
                r"M$_\odot$",
1005
                r"(M$_\odot$ / yr)",
1006
                r"L$_{\odot}$",
1007
                r"M$_{\odot}$",
1008
                r"(cm/s$^2$)",
1009
                r"R$_{\odot}$",
1010
            ]
1011
            + ["erg/s"]
1012
        )
1013
        column_type = np.array(([np.float64] * len(column_key)))
2✔
1014
        dtype = list(zip(column_key, column_type))
2✔
1015

1016
        column_names = {}
2✔
1017
        column_units = {}
2✔
1018
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
1019
            column_names[i] = j
2✔
1020
            column_units[i] = k
2✔
1021

1022
        # Get the mass from the file name
1023
        mass = np.array([i.split(os.sep)[-1][:3] for i in filelist]).astype(np.float64) / 100.0
2✔
1024

1025
        # Create an empty array for holding the cooling models
1026
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
1027

1028
        for i, filepath in enumerate(filelist):
2✔
1029
            cooling_model[i] = np.loadtxt(filepath, skiprows=2, dtype=dtype)
2✔
1030

1031
            # Convert the luminosity into erg/s
1032
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
1033

1034
            # Convert the age to yr
1035
            cooling_model[i]["age"] = 10.0 ** cooling_model[i]["age"] * 1e6
2✔
1036
            cooling_model[i]["age"] -= min(cooling_model[i]["age"])
2✔
1037

1038
        return mass, cooling_model, column_names, column_units
2✔
1039

1040
    def _lauffer18_formatter(self, model):
2✔
1041
        """
1042
        A formatter to load the Lauffer et al. 2018 WD cooling model
1043

1044
        Parameters
1045
        ----------
1046
        model: str
1047
            Name of the cooling model as in the `model_list`.
1048

1049
        """
1050

1051
        # H models
1052
        if model == "mesa_one_da_18":
2✔
1053
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/lauffer18/H_*.dat"))
2✔
1054

1055
        # He models
1056
        if model == "mesa_one_db_18":
2✔
1057
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/lauffer18/He_*.dat"))
2✔
1058

1059
        # Prepare the array column dtype
1060
        column_key = np.array(("Teff", "lum", "logg", "Rsun", "mass", "age", "total_age"))
2✔
1061
        column_key_formatted = np.array(
2✔
1062
            (
1063
                r"log(T$_{\mathrm{eff}})$",
1064
                "Luminosity",
1065
                "log(g)",
1066
                r"Radius",
1067
                "Mass",
1068
                "log(Cooling Age)",
1069
                "log(Total Age)",
1070
            )
1071
        )
1072
        column_key_unit = np.array(
2✔
1073
            [
1074
                "(K)",
1075
                r"L$_{\odot}$",
1076
                r"(cm/s$^2$)",
1077
                r"R$_{\odot}$",
1078
                r"M$_\odot$",
1079
            ]
1080
            + [r"(Gyr)"] * 2
1081
        )
1082
        column_type = np.array(([np.float64] * len(column_key)))
2✔
1083
        dtype = list(zip(column_key, column_type))
2✔
1084

1085
        column_names = {}
2✔
1086
        column_units = {}
2✔
1087
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
1088
            column_names[i] = j
2✔
1089
            column_units[i] = k
2✔
1090

1091
        # Get the mass from the file name
1092
        mass = np.array([i.split("-M")[-1][:-4] for i in filelist]).astype(np.float64)
2✔
1093

1094
        # Create an empty array for holding the cooling models
1095
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
1096

1097
        for i, filepath in enumerate(filelist):
2✔
1098
            cooling_model[i] = np.loadtxt(filepath, skiprows=1, dtype=dtype)
2✔
1099

1100
            # Convert the luminosity into erg/s
1101
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
1102

1103
            # Convert the age to yr
1104
            cooling_model[i]["age"] *= 1e9
2✔
1105

1106
        return mass, cooling_model, column_names, column_units
2✔
1107

1108
    def _lpcode22_da_formatter(self):
2✔
1109
        """
1110
        A formatter to load the LPCODE collated DA cooling model grid.
1111

1112
        """
1113

1114
        filelist = glob.glob(os.path.join(self.this_file, "wd_cooling", "lpcode22", "DA", "*.trk"))
2✔
1115

1116
        # Prepare the array column dtype
1117
        column_key = np.array(
2✔
1118
            (
1119
                "Teff",
1120
                "lum",
1121
                "logg",
1122
                "age",
1123
                "Rsun",
1124
                "Mbol",
1125
                "F070W",
1126
                "F090W",
1127
                "F115W",
1128
                "F150W",
1129
                "F200W",
1130
                "F277W",
1131
                "F356W",
1132
                "F444W",
1133
                "F164N",
1134
                "F187N",
1135
                "F212N",
1136
                "F323N",
1137
                "F405N",
1138
                "G",
1139
                "BP",
1140
                "RP",
1141
                "U",
1142
                "B",
1143
                "V",
1144
                "R",
1145
                "I",
1146
                "J",
1147
                "H",
1148
                "K",
1149
                "L",
1150
                "FUV",
1151
                "NUV",
1152
                "u",
1153
                "g",
1154
                "r",
1155
                "i",
1156
                "z",
1157
                "F220W",
1158
                "F250W",
1159
                "F330W",
1160
                "F344N",
1161
                "F435W",
1162
                "F475W",
1163
                "F502N",
1164
                "F550M",
1165
                "F555W",
1166
                "F606W",
1167
                "F625W",
1168
                "F658N",
1169
                "F660N",
1170
                "F775W",
1171
                "F814W",
1172
                "F850LP",
1173
                "F892N",
1174
            )
1175
        )
1176
        column_key_formatted = np.array(
2✔
1177
            (
1178
                r"log(T$_{\mathrm{eff}})$",
1179
                "log(Luminosity)",
1180
                "log(g)",
1181
                "log(Cooling Age)",
1182
                "Radius",
1183
                r"M$_{\mathrm{bol}}$",
1184
                "F070W",
1185
                "F090W",
1186
                "F115W",
1187
                "F150W",
1188
                "F200W",
1189
                "F277W",
1190
                "F356W",
1191
                "F444W",
1192
                "F164N",
1193
                "F187N",
1194
                "F212N",
1195
                "F323N",
1196
                "F405N",
1197
                "G",
1198
                "BP",
1199
                "RP",
1200
                "U",
1201
                "B",
1202
                "V",
1203
                "R",
1204
                "I",
1205
                "J",
1206
                "H",
1207
                "K",
1208
                "L",
1209
                "FUV",
1210
                "NUV",
1211
                "u",
1212
                "g",
1213
                "r",
1214
                "i",
1215
                "z",
1216
                "F220W",
1217
                "F250W",
1218
                "F330W",
1219
                "F344N",
1220
                "F435W",
1221
                "F475W",
1222
                "F502N",
1223
                "F550M",
1224
                "F555W",
1225
                "F606W",
1226
                "F625W",
1227
                "F658N",
1228
                "F660N",
1229
                "F775W",
1230
                "F814W",
1231
                "F850LP",
1232
                "F892N",
1233
            )
1234
        )
1235
        column_key_unit = np.array(
2✔
1236
            [
1237
                "log(K)",
1238
                r"log(L/L$_{\odot}$)",
1239
                r"log(cm/s$^2$)",
1240
                "log(yr)",
1241
                r"R$_{\odot}$",
1242
            ]
1243
            + ["mag"] * 50
1244
        )
1245
        column_type = np.array(([np.float64] * len(column_key)))
2✔
1246
        dtype = list(zip(column_key, column_type))
2✔
1247

1248
        column_names = {}
2✔
1249
        column_units = {}
2✔
1250
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
1251
            column_names[i] = j
2✔
1252
            column_units[i] = k
2✔
1253

1254
        # Get the mass from the file name
1255
        mass = np.array([i.split("Msun")[0].split(os.path.sep)[-1] for i in filelist]).astype(np.float64)
2✔
1256

1257
        # Create an empty array for holding the cooling models
1258
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
1259

1260
        for i, filepath in enumerate(filelist):
2✔
1261
            cooling_model[i] = np.loadtxt(filepath, skiprows=2, dtype=dtype)
2✔
1262

1263
            # Convert the luminosity into erg/s
1264
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
1265

1266
            # Convert the age to yr
1267
            cooling_model[i]["age"] *= 1.0e9
2✔
1268

1269
        return mass, cooling_model, column_names, column_units
2✔
1270

1271
    def _lpcode22_db_formatter(self):
2✔
1272
        """
1273
        A formatter to load the LPCODE collated DB cooling model grid.
1274

1275
        """
1276

1277
        filelist = glob.glob(os.path.join(self.this_file, "wd_cooling", "lpcode22", "DB", "*.trk"))
2✔
1278

1279
        # Prepare the array column dtype
1280
        column_key = np.array(("Teff", "lum", "logg", "age", "Rsun", "Mbol", "G", "BP", "RP"))
2✔
1281
        column_key_formatted = np.array(
2✔
1282
            (
1283
                r"log(T$_{\mathrm{eff}})$",
1284
                "log(Luminosity)",
1285
                "log(g)",
1286
                "log(Cooling Age)",
1287
                "Radius",
1288
                r"M$_{\mathrm{bol}}$",
1289
                "G",
1290
                "BP",
1291
                "RP",
1292
            )
1293
        )
1294
        column_key_unit = np.array(
2✔
1295
            [
1296
                "log(K)",
1297
                r"log(L/L$_{\odot}$)",
1298
                r"log(cm/s$^2$)",
1299
                "log(yr)",
1300
                r"R$_{\odot}$",
1301
            ]
1302
            + ["mag"] * 4
1303
        )
1304
        column_type = np.array(([np.float64] * len(column_key)))
2✔
1305
        dtype = list(zip(column_key, column_type))
2✔
1306

1307
        column_names = {}
2✔
1308
        column_units = {}
2✔
1309
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
1310
            column_names[i] = j
2✔
1311
            column_units[i] = k
2✔
1312

1313
        # Get the mass from the file name
1314
        mass = np.array([i.split("Msun")[0].split(os.path.sep)[-1] for i in filelist]).astype(np.float64)
2✔
1315

1316
        # Create an empty array for holding the cooling models
1317
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
1318

1319
        for i, filepath in enumerate(filelist):
2✔
1320
            cooling_model[i] = np.loadtxt(filepath, skiprows=2, dtype=dtype)
2✔
1321

1322
            # Convert the luminosity into erg/s
1323
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
1324

1325
            # Convert the age to yr
1326
            cooling_model[i]["age"] *= 1.0e9
2✔
1327

1328
        return mass, cooling_model, column_names, column_units
2✔
1329

1330
    def _panei07_formatter(self, model):
2✔
1331
        """
1332
        A formatter to load the Panei et al. 2007 WD cooling model
1333

1334
        Parameters
1335
        ----------
1336
        model: str
1337
            Name of the cooling model as in the `model_list`.
1338

1339
        """
1340

1341
        # He core models
1342
        if model == "lpcode_he_da_07":
2✔
1343
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/panei07/*He.SDSS"))
2✔
1344

1345
        # CO core models
1346
        if model == "lpcode_co_da_07":
2✔
1347
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/panei07/*CO.SDSS"))
2✔
1348

1349
        # Prepare the array column dtype
1350
        column_key = np.array(("Teff", "logg", "lum", "age", "u", "g", "r", "i", "z"))
2✔
1351
        column_key_formatted = np.array(
2✔
1352
            (
1353
                r"log(T$_{\mathrm{eff}})$",
1354
                "log(g)",
1355
                "Luminosity",
1356
                "log(Age)",
1357
                "u",
1358
                "g",
1359
                "r",
1360
                "i",
1361
                "z",
1362
            )
1363
        )
1364
        column_key_unit = np.array(["(K)", r"(cm/s$^2$)", r"L$_{\odot}$", r"(Gyr)"] + ["mag"] * 5)
2✔
1365
        column_type = np.array(([np.float64] * len(column_key)))
2✔
1366
        dtype = list(zip(column_key, column_type))
2✔
1367

1368
        column_names = {}
2✔
1369
        column_units = {}
2✔
1370
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
1371
            column_names[i] = j
2✔
1372
            column_units[i] = k
2✔
1373

1374
        # Get the mass from the file name
1375
        mass = np.array([i.split(".")[-2][:5] for i in filelist]).astype(np.float64) / 100000.0
2✔
1376

1377
        # Create an empty array for holding the cooling models
1378
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
1379

1380
        for i, filepath in enumerate(filelist):
2✔
1381
            cooling_model[i] = np.loadtxt(filepath, skiprows=1, dtype=dtype)
2✔
1382

1383
            # Convert the luminosity into erg/s
1384
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
1385

1386
            # Convert the age to yr
1387
            cooling_model[i]["age"] *= 1e9
2✔
1388

1389
        return mass, cooling_model, column_names, column_units
2✔
1390

1391
    def _renedo10_formatter(self, model):
2✔
1392
        """
1393
        A formatter to load the Renedo et al. 2010 WD cooling model from
1394
        http://evolgroup.fcaglp.unlp.edu.ar/TRACKS/tracks_cocore.html
1395

1396
        Two metallicity for DA are available: Z=0.01 and Z=0.001
1397

1398
        Parameters
1399
        ----------
1400
        model: str
1401
            Name of the cooling model as in the `model_list`.
1402

1403
        """
1404

1405
        # Solar metallicity model
1406
        if model == "lpcode_co_da_10_z001":
2✔
1407
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/renedo10/*z001.trk"))
2✔
1408

1409
        # Low metallicity model
1410
        if model == "lpcode_co_da_10_z0001":
2✔
1411
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/renedo10/*z0001.trk"))
2✔
1412

1413
        # Prepare the array column dtype
1414
        column_key = np.array(
2✔
1415
            (
1416
                "lum",
1417
                "Teff",
1418
                "logTc",
1419
                "logrhoc",
1420
                "age",
1421
                "mass",
1422
                "lumpp",
1423
                "lumcno",
1424
                "lumhe",
1425
                "lumnu",
1426
                "logH",
1427
                "logg",
1428
                "rsun",
1429
            )
1430
        )
1431
        column_key_formatted = np.array(
2✔
1432
            (
1433
                "log(Luminosity)",
1434
                r"log(T$_{\mathrm{eff}})$",
1435
                r"log(T$_{\mathrm{c}})$",
1436
                r"log($\rho_{\mathrm{c}})$",
1437
                "log(Age)",
1438
                "Mass",
1439
                r"log($L_{PP}$)",
1440
                r"log($L_{CNO}$)",
1441
                r"log($L_{He}$)",
1442
                r"log($L_{\nu}$)",
1443
                r"log(M$_{H, tot}$)",
1444
                "log(g)",
1445
                "Radius",
1446
            )
1447
        )
1448
        column_key_unit = np.array(
2✔
1449
            ["erg/s", "(K)", "(K)", r"(g/cm$^3$)", r"(Gyr)", r"M$_{\odot}$"]
1450
            + [r"L$_{\odot}$"] * 4
1451
            + [r"M$_{\odot}$", r"(cm/s$^2$)", r"E$_{\odot}$"]
1452
        )
1453
        column_type = np.array(([np.float64] * len(column_key)))
2✔
1454
        dtype = list(zip(column_key, column_type))
2✔
1455

1456
        column_names = {}
2✔
1457
        column_units = {}
2✔
1458
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
1459
            column_names[i] = j
2✔
1460
            column_units[i] = k
2✔
1461

1462
        # Get the mass from the file name
1463
        mass = np.array([i.split("_")[1][-4:] for i in filelist]).astype(np.float64) / 1000.0
2✔
1464

1465
        # Create an empty array for holding the cooling models
1466
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
1467

1468
        for i, filepath in enumerate(filelist):
2✔
1469
            cooling_model[i] = np.loadtxt(filepath, skiprows=1, dtype=dtype)
2✔
1470

1471
            # Convert the luminosity into erg/s
1472
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
1473

1474
            # Convert the age to yr
1475
            cooling_model[i]["age"] *= 1e6
2✔
1476

1477
        return mass, cooling_model, column_names, column_units
2✔
1478

1479
    def _salaris10_formatter(self, model, mass_range="all"):
2✔
1480
        """
1481
        A formatter to load the Salaris et al. 2010 WD cooling model from
1482

1483
        Parameters
1484
        ----------
1485
        model: str
1486
            Name of the cooling model as in the `model_list`.
1487
        mass_range: str (Default: 'all')
1488
            The mass range in which the cooling model should return. The ranges are defined as <0.5, 0.5-1.0 and
1489
            >1.0 solar masses.
1490

1491
        """
1492

1493
        # DA model with phase separation
1494
        if model == "basti_co_da_10":
2✔
1495
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/salaris10/*DAsep.sdss"))
2✔
1496

1497
        # DB model with phase separation
1498
        if model == "basti_co_db_10":
2✔
1499
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/salaris10/*DBsep.sdss"))
2✔
1500

1501
        # DA model without phase separation
1502
        if model == "basti_co_da_10_nps":
2✔
1503
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/salaris10/*DAnosep.sdss"))
2✔
1504

1505
        # DB model without phase separation
1506
        if model == "basti_co_db_10_nps":
2✔
1507
            filelist = glob.glob(os.path.join(self.this_file, "wd_cooling/salaris10/*DBnosep.sdss"))
2✔
1508

1509
        # Prepare the array column dtype
1510
        column_key = np.array(("age", "mass", "Teff", "lum", "u", "g", "r", "i", "z"))
2✔
1511
        column_key_formatted = np.array(
2✔
1512
            (
1513
                "log(Age)",
1514
                "Mass",
1515
                r"log(T$_{\mathrm{eff}})$",
1516
                "Luminosity",
1517
                "u",
1518
                "g",
1519
                "r",
1520
                "i",
1521
                "z",
1522
            )
1523
        )
1524
        column_key_unit = np.array(["(Gyr)", r"M$_{\odot}$", "(K)", r"L$_{\odot}$"] + ["mag"] * 5)
2✔
1525
        column_type = np.array(([np.float64] * len(column_key)))
2✔
1526
        dtype = list(zip(column_key, column_type))
2✔
1527

1528
        column_names = {}
2✔
1529
        column_units = {}
2✔
1530
        for i, j, k in zip(column_key, column_key_formatted, column_key_unit):
2✔
1531
            column_names[i] = j
2✔
1532
            column_units[i] = k
2✔
1533

1534
        # Get the mass from the file name
1535
        mass = np.array([i.split("COOL")[-1][:3] for i in filelist]).astype(np.float64) / 100.0
2✔
1536

1537
        if mass_range == "all":
2✔
1538
            pass
2✔
1539
        elif mass_range == "intermediate":
2✔
1540
            mask_intermediate = (mass >= 0.5) & (mass <= 1.0)
2✔
1541
            mass = mass[mask_intermediate]
2✔
1542
            filelist = np.array(filelist)[mask_intermediate]
2✔
1543
        elif mass_range == "high":
2✔
1544
            mask_high = mass > 1.0
2✔
1545
            mass = mass[mask_high]
2✔
1546
            filelist = np.array(filelist)[mask_high]
2✔
1547
        else:
1548
            raise ValueError(
×
1549
                "Unknown mass range requested. Please choose from 'all', 'intermediate' or 'high' for bedard20 models."
1550
            )
1551

1552
        # Create an empty array for holding the cooling models
1553
        cooling_model = np.array(([""] * len(mass)), dtype="object")
2✔
1554

1555
        for i, filepath in enumerate(filelist):
2✔
1556
            cooling_model[i] = np.loadtxt(filepath, skiprows=1, dtype=dtype)
2✔
1557

1558
            # Convert the luminosity into erg/s
1559
            cooling_model[i]["lum"] = 10.0 ** cooling_model[i]["lum"] * 3.826e33
2✔
1560

1561
            # Convert the age to yr
1562
            cooling_model[i]["age"] = 10.0 ** cooling_model[i]["age"]
2✔
1563

1564
        return mass, cooling_model, column_names, column_units
2✔
1565

1566
    def set_low_mass_cooling_model(self, model):
2✔
1567
        """
1568
        Set the WD cooling model.
1569

1570
        Parameters
1571
        ----------
1572
        model: str (Default: 'montreal_co_da_20')
1573
            Choice of WD cooling model:
1574

1575
            1. 'montreal_co_da_20' - Bedard et al. 2020 CO DA
1576
            2. 'montreal_co_db_20' - Bedard et al. 2020 CO DB
1577
            3. 'lpcode_he_da_07' - Panei et al. 2007 He DA
1578
            4. 'lpcode_co_da_07' - Panei et al. 2007 CO DA
1579
            5. 'lpcode_he_da_09' - Althaus et al. 2009 He DA
1580
            6. 'lpcode_da_20' - Althaus et al. 2013, Camisassa et al. 2016, Camisassa et al. 2019
1581

1582
            The naming convention follows this format:
1583
            [model]_[core composition]_[atmosphere]_[publication year] where a few models continue to have extra
1584
            property description terms trailing after the year, currently they are either the progenitor metallicity or
1585
            the (lack of) phase separation in the evolution model.
1586

1587
        """
1588

1589
        if model in self.low_mass_cooling_model_list:
2✔
1590
            self.cooling_models["low_mass_cooling_model"] = model
2✔
1591

1592
        else:
1593
            raise ValueError("Please provide a valid model.")
2✔
1594

1595
    def set_intermediate_mass_cooling_model(self, model):
2✔
1596
        """
1597
        Set the WD cooling model.
1598

1599
        Parameters
1600
        ----------
1601
        model: str (Default: 'montreal_co_da_20')
1602
            Choice of WD cooling model:
1603

1604
            1. 'montreal_co_da_20' - Bedard et al. 2020 CO DA
1605
            2. 'montreal_co_db_20' - Bedard et al. 2020 CO DB
1606
            3. 'lpcode_co_da_10_z001' - Renedo et al. 2010 CO DA Z=0.01
1607
            4. 'lpcode_co_da_10_z0001' - Renedo et al. 2010 CO DA Z=0.001
1608
            5. 'lpcode_co_da_15_z00003' - Althaus et al. 2015 DA Z=0.00003
1609
            6. 'lpcode_co_da_15_z0001' - Althaus et al. 2015 DA Z=0.0001
1610
            7. 'lpcode_co_da_15_z0005' - Althaus et al. 2015 DA Z=0.0005
1611
            8. 'lpcode_co_da_17_y04' - Althaus et al. 2017 DB Y=0.4
1612
            9. 'lpcode_co_db_17' - Camisassa et al. 2017 DB
1613
            10. 'lpcode_da_20' - Althaus et al. 2013, Camisassa et al. 2016, Camisassa et al. 2019
1614
            11. 'lpcode_db_20' - Camisassa et al. 2017, Camisassa et al. 2019
1615
            12. 'basti_co_da_10' - Salaris et al. 2010 CO DA
1616
            13. 'basti_co_db_10' - Salaris et al. 2010 CO DB
1617
            14. 'basti_co_da_10_nps' - Salaris et al. 2010 CO DA, no phase separation
1618
            15. 'basti_co_db_10_nps' - Salaris et al. 2010 CO DB, no phase separation
1619

1620
            The naming convention follows this format:
1621
            [model]_[core composition]_[atmosphere]_[publication year] where a few models continue to have extra
1622
            property description terms trailing after the year, currently they are either the progenitor metallicity or
1623
            the (lack of) phase separation in the evolution model.
1624

1625
        """
1626

1627
        if model in self.intermediate_mass_cooling_model_list:
2✔
1628
            self.cooling_models["intermediate_mass_cooling_model"] = model
2✔
1629

1630
        else:
1631
            raise ValueError("Please provide a valid model.")
2✔
1632

1633
    def set_high_mass_cooling_model(self, model):
2✔
1634
        """
1635
        Set the WD cooling model.
1636

1637
        Parameters
1638
        ----------
1639
        model: str (Default: 'montreal_co_da_20')
1640
            Choice of WD cooling model:
1641

1642
            1. 'montreal_co_da_20' - Bedard et al. 2020 CO DA
1643
            2. 'montreal_co_db_20' - Bedard et al. 2020 CO DB
1644
            3. 'lpcode_one_da_07' - Althaus et al. 2007 ONe DA
1645
            4. 'lpcode_one_da_19' - Camisassa et al. 2019 ONe DA
1646
            5. 'lpcode_one_db_19' - Camisassa et al. 2019 ONe DB
1647
            6. 'lpcode_da_20' - Althaus et al. 2013, Camisassa et al. 2016, Camisassa et al. 2019
1648
            7. 'lpcode_db_20' - Camisassa et al. 2017, Camisassa et al. 2019
1649
            8. 'basti_co_da_10' - Salaris et al. 2010 CO DA
1650
            9. 'basti_co_db_10' - Salaris et al. 2010 CO DB
1651
            10. 'basti_co_da_10_nps' - Salaris et al. 2010 CO DA, no phase separation
1652
            11. 'basti_co_db_10_nps' - Salaris et al. 2010 CO DB, no phase separation
1653
            12. 'mesa_one_da_18' - Lauffer et al. 2018 ONe DA
1654
            13. 'mesa_one_db_18' - Lauffer et al. 2018 ONe DB
1655

1656
            The naming convention follows this format:
1657
            [model]_[core composition]_[atmosphere]_[publication year] where a few models continue to have extra
1658
            property description terms trailing after the year, currently they are either the progenitor metallicity or
1659
            the (lack of) phase separation in the evolution model.
1660

1661
        """
1662

1663
        if model in self.high_mass_cooling_model_list:
2✔
1664
            self.cooling_models["high_mass_cooling_model"] = model
2✔
1665

1666
        else:
1667
            raise ValueError("Please provide a valid model.")
2✔
1668

1669
    def _itp2d_gradient(self, _f, val1, val2, frac=1e-6):
2✔
1670
        """
1671
        A function to find the gradient in the direction in the first dimension of a 2D function at a given coordinate.
1672

1673
        Parameters
1674
        ----------
1675
        f: callable function
1676
            A 2D function
1677
        val1: float
1678
            The first input value accepted by f. The gradient is computed in this direction.
1679
        val2: float
1680
            The first input value accepted by f.
1681
        frac: float (Default: 1e-6)
1682
            The small fractional increment of val1.
1683

1684
        Return
1685
        ------
1686
        Gradient in the direction of val1.
1687

1688
        """
1689

1690
        if not callable(_f):
2✔
1691
            raise TypeError("f has to be a callable function.")
×
1692

1693
        increment = val1 * frac / 2.0
2✔
1694
        grad = np.asarray((_f(val1 + increment, val2) - _f(val1 - increment, val2)) / (increment * 2.0)).reshape(-1)
2✔
1695

1696
        # cooling((L+1), m) - cooling(L, m) is always negative
1697
        grad[grad > 0.0] = 0.0
2✔
1698
        grad[np.isnan(grad)] = 0.0
2✔
1699

1700
        return grad
2✔
1701

1702
    def compute_cooling_age_interpolator(
2✔
1703
        self,
1704
        interpolator="CT",
1705
        scaling_factor=1.0,
1706
        kwargs_for_RBF={},
1707
        kwargs_for_CT={},
1708
    ):
1709
        """
1710
        Compute the callable CloughTocher2DInterpolator taking (logL, m) and returning the cooling time of the WDs. It
1711
        needs to use float64 or it runs into float-point error at very faint lumnosity.
1712

1713
        Parameters
1714
        ----------
1715
        interpolator: str (Default: 'CT')
1716
            Choose between 'RBF' and 'CT'.
1717
        scaling_factor: float (Default: 1.0)
1718
            A scaling factor to scale the cooling time, >1 means takes more time to reach the same lumniosity. This
1719
            is useful for bootstrapping for uncertainties.
1720
        kwargs_for_RBF: dict (Default: {"neighbors": None, "smoothing": 0.0, "kernel": "thin_plate_spline",
1721
            "epsilon": None, "degree": None,})
1722
            Keyword argument for the interpolator. See `scipy.interpolate.RBFInterpolator`.
1723
        kwargs_for_CT: dict (Default: {'fill_value': -np.inf, 'tol': 1e-10, 'maxiter': 100000})
1724
            Keyword argument for the interpolator. See `scipy.interpolate.CloughTocher2DInterpolator`.
1725

1726
        """
1727

1728
        # Set the low mass cooling model, i.e. M < 0.5 M_sun
1729
        mass_low, cooling_model_low, _, _ = self.get_cooling_model(
2✔
1730
            self.cooling_models["low_mass_cooling_model"], mass_range="low"
1731
        )
1732

1733
        # Set the intermediate mass cooling model, i.e. 0.5 < M < 1.0 M_sun
1734
        mass_intermediate, cooling_model_intermediate, _, _ = self.get_cooling_model(
2✔
1735
            self.cooling_models["intermediate_mass_cooling_model"],
1736
            mass_range="intermediate",
1737
        )
1738

1739
        # Set the high mass cooling model, i.e. 1.0 < M_sun
1740
        mass_high, cooling_model_high, _, _ = self.get_cooling_model(
2✔
1741
            self.cooling_models["high_mass_cooling_model"], mass_range="high"
1742
        )
1743

1744
        # Gather all the models in different mass ranges
1745

1746
        if mass_low.size == 0:
2✔
1747
            luminosity_low = np.array(())
2✔
1748
            age_low = np.array(())
2✔
1749

1750
        else:
1751
            # Reshaping the WD mass array to match the shape of the other two.
1752
            mass_low = (
2✔
1753
                np.concatenate(
1754
                    np.array(
1755
                        [[mass_low[i]] * len(model["age"]) for i, model in enumerate(cooling_model_low)],
1756
                        dtype=object,
1757
                    )
1758
                )
1759
                .T.ravel()
1760
                .astype(np.float64)
1761
            )
1762

1763
            # The luminosity of the WD at the corresponding mass and age
1764
            luminosity_low = np.concatenate([i["lum"] for i in cooling_model_low]).reshape(-1).astype(np.float64)
2✔
1765

1766
            # The luminosity of the WD at the corresponding mass and luminosity
1767
            age_low = np.concatenate([i["age"] for i in cooling_model_low]).reshape(-1).astype(np.float64)
2✔
1768

1769
        if mass_intermediate.size == 0:
2✔
1770
            luminosity_intermediate = np.array(())
×
1771
            age_intermediate = np.array(())
×
1772

1773
        else:
1774
            # Reshaping the WD mass array to match the shape of the other two.
1775
            mass_intermediate = (
2✔
1776
                np.concatenate(
1777
                    np.array(
1778
                        [
1779
                            [mass_intermediate[i]] * len(model["age"])
1780
                            for i, model in enumerate(cooling_model_intermediate)
1781
                        ],
1782
                        dtype=object,
1783
                    )
1784
                )
1785
                .T.ravel()
1786
                .astype(np.float64)
1787
            )
1788

1789
            # The luminosity of the WD at the corresponding mass and age
1790
            luminosity_intermediate = (
2✔
1791
                np.concatenate([i["lum"] for i in cooling_model_intermediate]).reshape(-1).astype(np.float64)
1792
            )
1793

1794
            # The luminosity of the WD at the corresponding mass and luminosity
1795
            age_intermediate = (
2✔
1796
                np.concatenate([i["age"] for i in cooling_model_intermediate]).reshape(-1).astype(np.float64)
1797
            )
1798

1799
        if mass_high.size == 0:
2✔
1800
            luminosity_high = np.array(())
×
1801
            age_high = np.array(())
×
1802

1803
        else:
1804
            # Reshaping the WD mass array to match the shape of the other two.
1805
            mass_high = (
2✔
1806
                np.concatenate(
1807
                    np.array(
1808
                        [[mass_high[i]] * len(model["age"]) for i, model in enumerate(cooling_model_high)],
1809
                        dtype=object,
1810
                    )
1811
                )
1812
                .T.ravel()
1813
                .astype(np.float64)
1814
            )
1815

1816
            # The luminosity of the WD at the corresponding mass and age
1817
            luminosity_high = np.concatenate([i["lum"] for i in cooling_model_high]).reshape(-1).astype(np.float64)
2✔
1818

1819
            # The luminosity of the WD at the corresponding mass and luminosity
1820
            age_high = np.concatenate([i["age"] for i in cooling_model_high]).reshape(-1).astype(np.float64)
2✔
1821

1822
        self.cooling_model_grid = np.concatenate((cooling_model_low, cooling_model_intermediate, cooling_model_high))
2✔
1823

1824
        self.mass = np.concatenate((mass_low, mass_intermediate, mass_high))
2✔
1825
        self.luminosity = np.concatenate((luminosity_low, luminosity_intermediate, luminosity_high))
2✔
1826
        self.age = np.concatenate((age_low, age_intermediate, age_high)) * scaling_factor
2✔
1827

1828
        # Configure interpolator for the cooling models
1829
        _kwargs_for_CT = {
2✔
1830
            "fill_value": float("-inf"),
1831
            "tol": 1e-10,
1832
            "maxiter": 100000,
1833
            "rescale": True,
1834
        }
1835
        _kwargs_for_CT.update(**kwargs_for_CT)
2✔
1836

1837
        _kwargs_for_RBF = {
2✔
1838
            "neighbors": None,
1839
            "smoothing": 0.0,
1840
            "kernel": "thin_plate_spline",
1841
            "epsilon": None,
1842
            "degree": None,
1843
        }
1844
        _kwargs_for_RBF.update(**kwargs_for_RBF)
2✔
1845

1846
        if interpolator.lower() == "ct":
2✔
1847
            # Interpolate with the scipy CloughTocher2DInterpolator
1848
            self.cooling_interpolator = CloughTocher2DInterpolator(
2✔
1849
                (np.log10(self.luminosity), self.mass),
1850
                self.age,
1851
                **_kwargs_for_CT,
1852
            )
1853

1854
        elif interpolator.lower() == "rbf":
2✔
1855
            # Interpolate with the scipy RBFInterpolator
1856
            _cooling_interpolator = RBFInterpolator(
2✔
1857
                np.stack((np.log10(self.luminosity), self.mass), -1),
1858
                self.age,
1859
                **_kwargs_for_RBF,
1860
            )
1861

1862
            lum_min = np.nanmin(np.log10(self.luminosity))
2✔
1863
            lum_max = np.nanmax(np.log10(self.luminosity))
2✔
1864
            mass_min = np.nanmin(self.mass)
2✔
1865
            mass_max = np.nanmax(self.mass)
2✔
1866

1867
            def cooling_interpolator(x_0, x_1):
2✔
1868
                _x_0 = np.array(x_0)
2✔
1869
                _x_1 = np.array(x_1)
2✔
1870

1871
                if (_x_0.size == 1) & (_x_1.size > 1):
2✔
1872
                    _x_0 = np.repeat(_x_0, _x_1.size)
2✔
1873

1874
                if (_x_1.size == 1) & (_x_0.size > 1):
2✔
NEW
1875
                    _x_1 = np.repeat(_x_1, _x_0.size)
×
1876

1877
                _x_0[_x_0 < lum_min] = lum_min
2✔
1878
                _x_0[_x_0 > lum_max] = lum_max
2✔
1879
                _x_1[_x_1 < mass_min] = mass_min
2✔
1880
                _x_1[_x_1 > mass_max] = mass_max
2✔
1881

1882
                length0 = _x_0.size
2✔
1883

1884
                return _cooling_interpolator(np.array([_x_0, _x_1], dtype="object").T.reshape(length0, 2))
2✔
1885

1886
            self.cooling_interpolator = cooling_interpolator
2✔
1887

1888
        else:
1889
            raise ValueError(f"Interpolator should be CT or RBF, {interpolator} is given.")
2✔
1890

1891
        self.dLdt = self._itp2d_gradient(self.cooling_interpolator, np.log10(self.luminosity), self.mass)
2✔
1892

1893
        finite_mask = np.isfinite(self.dLdt)
2✔
1894

1895
        if interpolator.lower() == "ct":
2✔
1896
            self.cooling_rate_interpolator = CloughTocher2DInterpolator(
2✔
1897
                (
1898
                    np.log10(self.luminosity)[finite_mask],
1899
                    self.mass[finite_mask],
1900
                ),
1901
                self.dLdt[finite_mask],
1902
                **_kwargs_for_CT,
1903
            )
1904

1905
        elif interpolator.lower() == "rbf":
2✔
1906
            # Interpolate with the scipy RBFInterpolator
1907
            _cooling_rate_interpolator = RBFInterpolator(
2✔
1908
                np.stack((np.log10(self.luminosity)[finite_mask], self.mass[finite_mask]), -1),
1909
                self.dLdt[finite_mask],
1910
                **_kwargs_for_RBF,
1911
            )
1912

1913
            lum_min = np.nanmin(np.log10(self.luminosity))
2✔
1914
            lum_max = np.nanmax(np.log10(self.luminosity))
2✔
1915
            mass_min = np.nanmin(self.mass)
2✔
1916
            mass_max = np.nanmax(self.mass)
2✔
1917

1918
            def cooling_rate_interpolator(x_0, x_1):
2✔
1919
                _x_0 = np.asarray(x_0)
×
1920
                _x_1 = np.asarray(x_1)
×
1921

1922
                if (_x_0.size == 1) & (_x_1.size > 1):
×
1923
                    _x_0 = np.repeat(_x_0, _x_1.size)
×
1924

1925
                if (_x_1.size == 1) & (_x_0.size > 1):
×
1926
                    _x_0 = np.repeat(_x_1, _x_0.size)
×
1927

1928
                _x_0[_x_0 < lum_min] = lum_min
×
1929
                _x_0[_x_0 > lum_max] = lum_max
×
1930
                _x_1[_x_1 < mass_min] = mass_min
×
1931
                _x_1[_x_1 > mass_max] = mass_max
×
1932

1933
                length0 = _x_0.size
×
1934

1935
                return _cooling_rate_interpolator(np.asarray([_x_0, _x_1], dtype="object").T.reshape(length0, 2))
×
1936

1937
            self.cooling_rate_interpolator = cooling_rate_interpolator
2✔
1938

1939
        else:
1940
            raise ValueError("Interpolator should be CT or RBF, {interpolator} is given.")
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc