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

cylammarco / WDPhotTools / 20978266610

14 Jan 2026 12:54AM UTC coverage: 95.519% (-0.6%) from 96.096%
20978266610

push

github

web-flow
v0.0.13 (#49)

* handling numpy 2 (#47)

* Compat: use numpy generic types for v1/v2 (np.floating/np.integer) in src; no stylistic changes

* Allow NumPy 2 in install: relax to >=1.23,<3; update README to state v1+v2 support

* updated array handling

* removed test output figure

* updated setup.cfg to include numpy2

* use asarray

* use np.where

* use where instead of argwhere; wrapped interpolator outputs with np.asarray(...).itme() or reshape(-1)[0].

* pack values in correct array shape for atmosphere interpolator

* Tests/coverage improvements (#48)

* tests: increase coverage and add edge-case tests across modules

* added more tests

* final fixes to the minimisation function and the interpolation functions. updated example scripts and tests.

* updated test

* avoid extrapolation in both ct and rbf interpolator for the atmosphere model

* fixed extrapolation issue

299 of 337 new or added lines in 11 files covered. (88.72%)

4 existing lines in 3 files now uncovered.

3389 of 3548 relevant lines covered (95.52%)

1.91 hits per line

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

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

4
"""Handling the formatting of different cooling models"""
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
        allow_extrapolation=False,
1709
    ):
1710
        """
1711
        Compute the callable CloughTocher2DInterpolator taking (logL, m) and returning the cooling time of the WDs. It
1712
        needs to use float64 or it runs into float-point error at very faint lumnosity.
1713

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

1727
        """
1728

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

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

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

1745
        # Gather all the models in different mass ranges
1746

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

1878
                if not allow_extrapolation:
2✔
1879
                    _x_0[_x_0 < lum_min] = lum_min
2✔
1880
                    _x_0[_x_0 > lum_max] = lum_max
2✔
1881
                    _x_1[_x_1 < mass_min] = mass_min
2✔
1882
                    _x_1[_x_1 > mass_max] = mass_max
2✔
1883

1884
                length0 = _x_0.size
2✔
1885

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

1888
            self.cooling_interpolator = cooling_interpolator
2✔
1889

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

1893
        self.dLdt = self._itp2d_gradient(self.cooling_interpolator, np.log10(self.luminosity), self.mass)
2✔
1894

1895
        finite_mask = np.isfinite(self.dLdt)
2✔
1896

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

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

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

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

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

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

NEW
1930
                if not allow_extrapolation:
×
NEW
1931
                    _x_0[_x_0 < lum_min] = lum_min
×
NEW
1932
                    _x_0[_x_0 > lum_max] = lum_max
×
NEW
1933
                    _x_1[_x_1 < mass_min] = mass_min
×
NEW
1934
                    _x_1[_x_1 > mass_max] = mass_max
×
1935

1936
                length0 = _x_0.size
×
1937

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

1940
            self.cooling_rate_interpolator = cooling_rate_interpolator
2✔
1941

1942
        else:
1943
            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