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

cylammarco / WDPhotTools / 29038978583

09 Jul 2026 05:58PM UTC coverage: 96.155% (+0.5%) from 95.688%
29038978583

Pull #51

github

web-flow
Merge 4c9ec2fba into 59f833d41
Pull Request #51: Sanitise extrapolation and examples

297 of 316 new or added lines in 3 files covered. (93.99%)

3776 of 3927 relevant lines covered (96.15%)

1.92 hits per line

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

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

4
"""Handling the formatting of different atmosphere models"""
5

6
import os
2✔
7

8
import numpy as np
2✔
9
from scipy.interpolate import CloughTocher2DInterpolator
2✔
10
from scipy.interpolate import RBFInterpolator
2✔
11

12

13
class AtmosphereModelReader(object):
2✔
14
    """Handling the formatting of different atmosphere models"""
15

16
    def __init__(self):
2✔
17
        super(AtmosphereModelReader, self).__init__()
2✔
18

19
        self.this_file = os.path.dirname(os.path.abspath(__file__))
2✔
20

21
        self.model_list = {
2✔
22
            "montreal_co_da_20": "Bedard et al. 2020 CO DA",
23
            "montreal_co_db_20": "Bedard et al. 2020 CO DB",
24
            "lpcode_he_da_07": "Panei et al. 2007 He DA",
25
            "lpcode_co_da_07": "Panei et al. 2007 CO DA",
26
            "lpcode_he_da_09": "Althaus et al. 2009 He DA",
27
            "lpcode_co_da_10_z001": "Renedo et al. 2010 CO DA Z=0.01",
28
            "lpcode_co_da_10_z0001": "Renedo et al. 2010 CO DA Z=0.001",
29
            "lpcode_co_da_15_z00003": "Althaus et al. 2015 DA Z=0.00003",
30
            "lpcode_co_da_15_z0001": "Althaus et al. 2015 DA Z=0.0001",
31
            "lpcode_co_da_15_z0005": "Althaus et al. 2015 DA Z=0.0005",
32
            "lpcode_co_db_17_z00005": "Althaus et al. 2017 DB Y=0.4",
33
            "lpcode_co_db_17_z0001": "Althaus et al. 2017 DB Y=0.4",
34
            "lpcode_co_db_17": "Camisassa et al. 2017 DB",
35
            "lpcode_one_da_07": "Althaus et al. 2007 ONe DA",
36
            "lpcode_one_da_19": "Camisassa et al. 2019 ONe DA",
37
            "lpcode_one_db_19": "Camisassa et al. 2019 ONe DB",
38
            "lpcode_da_22": "Althaus et al. 2013 He DA, Camisassa et al. 2016 CO DA,  Camisassa et al. 2019 ONe DA",
39
            "lpcode_db_22": "Camisassa et al. 2017 CO DB, " + "Camisassa et al. 2019 ONe DB",
40
        }
41

42
        # DA atmosphere
43
        filepath_da = os.path.join(
2✔
44
            os.path.dirname(os.path.abspath(__file__)),
45
            "wd_photometry/Table_DA_13012021.txt",
46
        )
47

48
        # DB atmosphere
49
        filepath_db = os.path.join(
2✔
50
            os.path.dirname(os.path.abspath(__file__)),
51
            "wd_photometry/Table_DB_13012021.txt",
52
        )
53

54
        # Prepare the array column dtype
55
        self.column_key = np.array(
2✔
56
            [
57
                "Teff",
58
                "logg",
59
                "mass",
60
                "Mbol",
61
                "BC",
62
                "U",
63
                "B",
64
                "V",
65
                "R",
66
                "I",
67
                "J",
68
                "H",
69
                "Ks",
70
                "Y_mko",
71
                "J_mko",
72
                "H_mko",
73
                "K_mko",
74
                "W1",
75
                "W2",
76
                "W3",
77
                "W4",
78
                "S36",
79
                "S45",
80
                "S58",
81
                "S80",
82
                "u_sdss",
83
                "g_sdss",
84
                "r_sdss",
85
                "i_sdss",
86
                "z_sdss",
87
                "g_ps1",
88
                "r_ps1",
89
                "i_ps1",
90
                "z_ps1",
91
                "y_ps1",
92
                "G2",
93
                "G2_BP",
94
                "G2_RP",
95
                "G3",
96
                "G3_BP",
97
                "G3_RP",
98
                "FUV",
99
                "NUV",
100
                "age",
101
            ]
102
        )
103
        self.column_key_formatted = np.array(
2✔
104
            [
105
                r"T$_{\mathrm{eff}}$",
106
                "log(g)",
107
                "Mass",
108
                r"M$_{\mathrm{bol}}$",
109
                "BC",
110
                r"$U$",
111
                r"$B$",
112
                r"$V$",
113
                r"$R$",
114
                r"$I$",
115
                r"$J$",
116
                r"$H$",
117
                r"$K_{\mathrm{s}}$",
118
                r"$Y_{\mathrm{MKO}}$",
119
                r"$J_{\mathrm{MKO}}$",
120
                r"$H_{\mathrm{MKO}}$",
121
                r"$K_{\mathrm{MKO}}$",
122
                r"$W_{1}$",
123
                r"$W_{2}$",
124
                r"$W_{3}$",
125
                r"$W_{4}$",
126
                r"$S_{36}$",
127
                r"$S_{45}$",
128
                r"$S_{58}$",
129
                r"$S_{80}$",
130
                r"u$_{\mathrm{SDSS}}$",
131
                r"$g_{\mathrm{SDSS}}$",
132
                r"$r_{\mathrm{SDSS}}$",
133
                r"$i_{\mathrm{SDSS}}$",
134
                r"$z_{\mathrm{SDSS}}$",
135
                r"$g_{\mathrm{PS1}}$",
136
                r"$r_{\mathrm{PS1}}$",
137
                r"$i_{\mathrm{PS1}}$",
138
                r"$z_{\mathrm{PS1}}$",
139
                r"$y_{\mathrm{PS1}}$",
140
                r"$G_{\mathrm{DR2}}$",
141
                r"$G_{\mathrm{BP, DR2}}$",
142
                r"$G_{\mathrm{RP, DR2}}$",
143
                r"$G_{\mathrm{DR3}}$",
144
                r"$G_{\mathrm{BP, DR3}}$",
145
                r"$G_{\mathrm{RP, DR3}}$",
146
                "FUV",
147
                "NUV",
148
                "Age",
149
            ]
150
        )
151
        self.column_key_unit = np.array(
2✔
152
            [
153
                "K",
154
                r"(cm/s$^2$)",
155
                r"M$_\odot$",
156
                "mag",
157
                "mag",
158
                "mag",
159
                "mag",
160
                "mag",
161
                "mag",
162
                "mag",
163
                "mag",
164
                "mag",
165
                "mag",
166
                "mag",
167
                "mag",
168
                "mag",
169
                "mag",
170
                "mag",
171
                "mag",
172
                "mag",
173
                "mag",
174
                "mag",
175
                "mag",
176
                "mag",
177
                "mag",
178
                "mag",
179
                "mag",
180
                "mag",
181
                "mag",
182
                "mag",
183
                "mag",
184
                "mag",
185
                "mag",
186
                "mag",
187
                "mag",
188
                "mag",
189
                "mag",
190
                "mag",
191
                "mag",
192
                "mag",
193
                "mag",
194
                "mag",
195
                "mag",
196
                "yr",
197
            ]
198
        )
199
        self.column_key_wavelength = np.array(
2✔
200
            [
201
                0.0,
202
                0.0,
203
                0.0,
204
                0.0,
205
                0.0,
206
                3585.0,
207
                4371.0,
208
                5478.0,
209
                6504.0,
210
                8020.0,
211
                12350.0,
212
                16460.0,
213
                21600.0,
214
                10310.0,
215
                12500.0,
216
                16360.0,
217
                22060.0,
218
                33682.0,
219
                46179.0,
220
                120717.0,
221
                221944.0,
222
                35378.0,
223
                44780.0,
224
                56962.0,
225
                77978.0,
226
                3557.0,
227
                4702.0,
228
                6175.0,
229
                7491.0,
230
                8946.0,
231
                4849.0,
232
                6201.0,
233
                7535.0,
234
                8674.0,
235
                9628.0,
236
                6229.0,
237
                5037.0,
238
                7752.0,
239
                6218.0,
240
                5110.0,
241
                7769.0,
242
                1535.0,
243
                2301.0,
244
                0.0,
245
            ]
246
        )
247

248
        self.column_names = {}
2✔
249
        self.column_units = {}
2✔
250
        self.column_wavelengths = {}
2✔
251
        for i, j, k, _l in zip(
2✔
252
            self.column_key,
253
            self.column_key_formatted,
254
            self.column_key_unit,
255
            self.column_key_wavelength,
256
        ):
257
            self.column_names[i] = j
2✔
258
            self.column_units[i] = k
2✔
259
            self.column_wavelengths[i] = _l
2✔
260

261
        self.column_type = np.array(([np.float64] * len(self.column_key)))
2✔
262
        self.dtype = list(zip(self.column_key, self.column_type))
2✔
263

264
        # Load the synthetic photometry file in a recarray
265
        self.model_da = np.loadtxt(filepath_da, skiprows=2, dtype=self.dtype)
2✔
266
        self.model_db = np.loadtxt(filepath_db, skiprows=2, dtype=self.dtype)
2✔
267

268
        self.model_da["age"][self.model_da["age"] <= 1.0] += 1.0
2✔
269
        self.model_db["age"][self.model_db["age"] <= 1.0] += 1.0
2✔
270

271
    def list_atmosphere_parameters(self):
2✔
272
        """
273
        Print the formatted list of parameters available from the atmophere
274
        models.
275

276
        """
277

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

281
    def interp_am(
2✔
282
        self,
283
        dependent="G3",
284
        atmosphere="H",
285
        independent=["logg", "Mbol"],
286
        logg=8.0,
287
        interpolator="CT",
288
        kwargs_for_RBF={},
289
        kwargs_for_CT={},
290
        allow_extrapolation=False,
291
    ):
292
        """
293
        This function interpolates the grid of synthetic photometry and a few
294
        other physical properties as a function of 2 independent variables,
295
        the Default choices are 'logg' and 'Mbol'.
296

297
        Parameters
298
        ----------
299
        dependent: str (Default: 'G3')
300
            The value to be interpolated over. Choose from:
301
            'Teff', 'logg', 'mass', 'Mbol', 'BC', 'U', 'B', 'V', 'R', 'I', 'J', 'H', 'Ks', 'Y_mko', 'J_mko', 'H_mko',
302
            'K_mko', 'W1', 'W2', 'W3', 'W4', 'S36', 'S45', 'S58', 'S80', 'u_sdss', 'g_sdss', 'r_sdss', 'i_sdss',
303
            'z_sdss', 'g_ps1', 'r_ps1', 'i_ps1', 'z_ps1', 'y_ps1', 'G2', 'G2_BP', 'G2_RP', 'G3', 'G3_BP', 'G3_RP',
304
            'FUV', 'NUV', 'age'.
305
        atmosphere: str (Default: 'H')
306
            The atmosphere type, 'H' or 'He'.
307
        independent: list (Default: ['logg', 'Mbol'])
308
            The parameters to be interpolated over for dependent.
309
        logg: float (Default: 8.0)
310
            Only used if independent is of length 1.
311
        interpolator: str (Default: 'RBF')
312
            Choose between 'RBF' and 'CT'.
313
        kwargs_for_RBF: dict (Default: {"neighbors": None, "smoothing": 0.0, "kernel": "thin_plate_spline",
314
            "epsilon": None, "degree": None,})
315
            Keyword argument for the interpolator. See `scipy.interpolate.RBFInterpolator`.
316
        kwargs_for_CT: dict (Default: {'fill_value': -np.inf, 'tol': 1e-10, 'maxiter': 100000})
317
            Keyword argument for the interpolator. See `scipy.interpolate.CloughTocher2DInterpolator`.
318
        allow_extrapolation: bool (Default: False)
319
            If True, allow smooth extrapolation up to 50% outside each interpolation axis span. Extrapolated values
320
            are sanitised to avoid non-finite and physically impossible outputs for positive-definite parameters.
321

322
        Returns
323
        -------
324
            A callable function of CloughTocher2DInterpolator.
325

326
        """
327
        _kwargs_for_RBF = {
2✔
328
            "neighbors": None,
329
            "smoothing": 0.0,
330
            "kernel": "thin_plate_spline",
331
            "epsilon": None,
332
            "degree": None,
333
        }
334
        _kwargs_for_RBF.update(**kwargs_for_RBF)
2✔
335

336
        _kwargs_for_CT = {
2✔
337
            "fill_value": np.inf,
338
            "tol": 1e-10,
339
            "maxiter": 100000,
340
            "rescale": True,
341
        }
342
        _kwargs_for_CT.update(**kwargs_for_CT)
2✔
343

344
        max_extrapolation_fraction = 0.5
2✔
345

346
        # DA atmosphere
347
        if atmosphere.lower() in ["h", "hydrogen", "da"]:
2✔
348
            model = self.model_da
2✔
349

350
        # DB atmosphere
351
        elif atmosphere.lower() in ["he", "helium", "db"]:
2✔
352
            model = self.model_db
2✔
353

354
        else:
355
            raise ValueError(
2✔
356
                'Please choose from "h", "hydrogen", "da", "he", "helium" or "db" as the atmophere type, you have '
357
                "provided {}.format(atmosphere.lower())"
358
            )
359

360
        _dependent_values = np.asarray(model[dependent], dtype=float).reshape(-1)
2✔
361
        _finite_dependent = _dependent_values[np.isfinite(_dependent_values)]
2✔
362
        _finite_positive_dependent = _finite_dependent[_finite_dependent > 0.0]
2✔
363
        _dependent_floor = (
2✔
364
            float(np.nanmin(_finite_positive_dependent))
365
            if _finite_positive_dependent.size
366
            else float(np.finfo(float).tiny)
367
        )
368
        _dependent_fallback = float(np.nanmedian(_finite_dependent)) if _finite_dependent.size else 0.0
2✔
369
        _positive_dependent = {"Teff", "logg", "mass", "age"}
2✔
370

371
        def _clip_with_fraction(values, value_min, value_max, fraction):
2✔
372
            span = max(float(value_max - value_min), float(np.finfo(float).eps))
2✔
373
            return np.clip(values, value_min - span * fraction, value_max + span * fraction)
2✔
374

375
        def _sanitize_output(values):
2✔
376
            out = np.asarray(values, dtype=float).reshape(-1)
2✔
377
            if dependent in _positive_dependent:
2✔
378
                out[~np.isfinite(out)] = _dependent_floor
2✔
379
                out = np.maximum(out, _dependent_floor)
2✔
380
            else:
381
                out[~np.isfinite(out)] = _dependent_fallback
2✔
382
            return out
2✔
383

384
        def _sanitize_extrapolated(values, mask_oob):
2✔
385
            out = np.asarray(values, dtype=float).reshape(-1)
2✔
386
            mask_oob = np.asarray(mask_oob, dtype=bool).reshape(-1)
2✔
387
            sanitize_mask = mask_oob | (~np.isfinite(out))
2✔
388
            if np.any(sanitize_mask):
2✔
389
                out[sanitize_mask] = _sanitize_output(out[sanitize_mask])
2✔
390
            return out
2✔
391

392
        independent = np.asarray(independent, dtype=object).reshape(-1)
2✔
393

394
        independent_list = self.column_key
2✔
395
        independent_list_lower_cases = np.char.lower(independent_list)
2✔
396

397
        # If only performing a 1D interpolation, the logg has to be assumed.
398
        if len(independent) == 1:
2✔
399
            if independent[0].lower() in independent_list_lower_cases:
2✔
400
                independent = np.array(("logg", independent[0]))
2✔
401

402
            else:
403
                raise ValueError(
×
404
                    "When ony interpolating in 1-dimension, the independent variable has to be one of: Teff, mass, "
405
                    "Mbol, or age."
406
                )
407

408
            _independent_arg_0 = np.where(independent[0].lower() == independent_list_lower_cases)[0][0]
2✔
409
            _independent_arg_1 = np.where(independent[1].lower() == independent_list_lower_cases)[0][0]
2✔
410

411
            independent = np.array([independent_list[_independent_arg_0], independent_list[_independent_arg_1]])
2✔
412

413
            arg_0 = np.asarray(model[independent[0]], dtype=float).reshape(-1)
2✔
414
            arg_1_raw = np.asarray(model[independent[1]], dtype=float).reshape(-1)
2✔
415

416
            arg_1_min = float(np.nanmin(arg_1_raw))
2✔
417
            arg_1_max = float(np.nanmax(arg_1_raw))
2✔
418
            arg_1_floor = (
2✔
419
                float(np.nanmin(arg_1_raw[arg_1_raw > 0.0])) if np.any(arg_1_raw > 0.0) else float(np.finfo(float).tiny)
420
            )
421

422
            if independent[1] in ["Teff", "age"]:
2✔
423
                arg_1 = np.log10(np.maximum(arg_1_raw, arg_1_floor))
2✔
424
            else:
425
                arg_1 = arg_1_raw
2✔
426

427
            if interpolator.lower() == "ct":
2✔
428
                # Interpolate with the scipy CloughTocher2DInterpolator
429
                _atmosphere_interpolator = CloughTocher2DInterpolator(
2✔
430
                    (arg_0, arg_1),
431
                    model[dependent],
432
                    **_kwargs_for_CT,
433
                )
434
                _rbf_extrapolator = None
2✔
435
                if allow_extrapolation:
2✔
NEW
436
                    _rbf_extrapolator = RBFInterpolator(
×
437
                        np.stack((arg_0, arg_1), -1),
438
                        model[dependent],
439
                        **_kwargs_for_RBF,
440
                    )
441

442
                def atmosphere_interpolator(_x):
2✔
443
                    _x_raw = np.asarray(_x).reshape(-1).astype(float)
2✔
444
                    mask_oob = (_x_raw < arg_1_min) | (_x_raw > arg_1_max)
2✔
445
                    if allow_extrapolation:
2✔
NEW
446
                        _x_eval = _clip_with_fraction(_x_raw, arg_1_min, arg_1_max, max_extrapolation_fraction)
×
447
                    else:
448
                        _x_eval = np.clip(_x_raw, arg_1_min, arg_1_max)
2✔
449

450
                    if independent[1] in ["Teff", "age"]:
2✔
451
                        _x_eval = np.log10(np.maximum(_x_eval, arg_1_floor))
2✔
452

453
                    _logg = np.full(_x_eval.size, logg, dtype=float)
2✔
454
                    out = np.asarray(_atmosphere_interpolator(_logg, _x_eval)).reshape(-1)
2✔
455

456
                    if allow_extrapolation:
2✔
NEW
457
                        bad = ~np.isfinite(out)
×
NEW
458
                        if np.any(bad):
×
NEW
459
                            out[bad] = np.asarray(
×
460
                                _rbf_extrapolator(np.column_stack((_logg[bad], _x_eval[bad])))
461
                            ).reshape(-1)
NEW
462
                        out = _sanitize_extrapolated(out, mask_oob)
×
463

464
                    return out
2✔
465

466
            elif interpolator.lower() == "rbf":
2✔
467
                # Interpolate with the scipy RBFInterpolator
468
                _atmosphere_interpolator = RBFInterpolator(
2✔
469
                    np.stack((arg_0, arg_1), -1),
470
                    model[dependent],
471
                    **_kwargs_for_RBF,
472
                )
473

474
                def atmosphere_interpolator(_x):
2✔
475
                    _x_raw = np.asarray(_x).reshape(-1).astype(float)
2✔
476
                    length = _x_raw.size
2✔
477
                    _logg = np.full(length, logg, dtype=float)
2✔
478
                    mask_oob = (_x_raw < arg_1_min) | (_x_raw > arg_1_max)
2✔
479

480
                    if not allow_extrapolation:
2✔
481
                        _x_eval = np.clip(_x_raw, arg_1_min, arg_1_max)
2✔
482
                    else:
NEW
483
                        _x_eval = _clip_with_fraction(_x_raw, arg_1_min, arg_1_max, max_extrapolation_fraction)
×
484

485
                    if independent[1] in ["Teff", "age"]:
2✔
486
                        _x_eval = np.log10(np.maximum(_x_eval, arg_1_floor))
2✔
487

488
                    out = np.asarray(_atmosphere_interpolator(np.column_stack((_logg, _x_eval)))).reshape(-1)
2✔
489
                    if allow_extrapolation:
2✔
NEW
490
                        out = _sanitize_extrapolated(out, mask_oob)
×
491
                    return out
2✔
492

493
            else:
494
                raise ValueError("Interpolator should be CT or RBF, {interpolator} is given.")
2✔
495

496
        # If a 2D grid is to be interpolated, normally is the logg and another
497
        # parameter
498
        elif len(independent) == 2:
2✔
499
            _independent_arg_0 = np.where(independent[0].lower() == independent_list_lower_cases)[0][0]
2✔
500
            _independent_arg_1 = np.where(independent[1].lower() == independent_list_lower_cases)[0][0]
2✔
501

502
            independent = np.array([independent_list[_independent_arg_0], independent_list[_independent_arg_1]])
2✔
503

504
            arg_0_raw = np.asarray(model[independent[0]], dtype=float).reshape(-1)
2✔
505
            arg_1_raw = np.asarray(model[independent[1]], dtype=float).reshape(-1)
2✔
506

507
            arg_0_min = float(np.nanmin(arg_0_raw))
2✔
508
            arg_0_max = float(np.nanmax(arg_0_raw))
2✔
509
            arg_1_min = float(np.nanmin(arg_1_raw))
2✔
510
            arg_1_max = float(np.nanmax(arg_1_raw))
2✔
511
            arg_0_floor = (
2✔
512
                float(np.nanmin(arg_0_raw[arg_0_raw > 0.0])) if np.any(arg_0_raw > 0.0) else float(np.finfo(float).tiny)
513
            )
514
            arg_1_floor = (
2✔
515
                float(np.nanmin(arg_1_raw[arg_1_raw > 0.0])) if np.any(arg_1_raw > 0.0) else float(np.finfo(float).tiny)
516
            )
517

518
            if independent[0] in ["Teff", "age"]:
2✔
519
                arg_0 = np.log10(np.maximum(arg_0_raw, arg_0_floor))
2✔
520
            else:
521
                arg_0 = arg_0_raw
2✔
522

523
            if independent[1] in ["Teff", "age"]:
2✔
524
                arg_1 = np.log10(np.maximum(arg_1_raw, arg_1_floor))
2✔
525
            else:
526
                arg_1 = arg_1_raw
2✔
527

528
            def _prepare_two_input_arrays(x0, x1=None):
2✔
529
                if x1 is None:
2✔
530
                    arr = np.asarray(x0).reshape(-1)
2✔
531
                    if arr.size >= 2:
2✔
532
                        x_0, x_1 = arr[0], arr[1]
2✔
NEW
533
                    elif arr.size == 1:
×
NEW
534
                        x_0, x_1 = arr[0], arr[0]
×
535
                    else:
NEW
536
                        x_0, x_1 = -np.inf, -np.inf
×
537
                else:
538
                    x_0, x_1 = x0, x1
2✔
539

540
                if isinstance(x_0, (float, int, np.integer)):
2✔
541
                    length0 = 1
2✔
542
                else:
543
                    length0 = np.asarray(x_0).size
2✔
544

545
                if isinstance(x_1, (float, int, np.integer)):
2✔
546
                    length1 = 1
2✔
547
                else:
548
                    length1 = np.asarray(x_1).size
2✔
549

550
                if length0 == length1:
2✔
551
                    pass
2✔
552
                elif (length0 == 1) and (length1 > 1):
2✔
553
                    x_0 = [x_0] * length1
2✔
554
                    length0 = length1
2✔
NEW
555
                elif (length0 > 1) and (length1 == 1):
×
NEW
556
                    x_1 = [x_1] * length0
×
NEW
557
                    length1 = length0
×
558
                else:
NEW
559
                    raise ValueError(
×
560
                        "Either one variable is a float, int or of size 1, or two variables should have the same "
561
                        "size."
562
                    )
563

564
                return (
2✔
565
                    np.asarray(x_0).reshape(-1).astype(float),
566
                    np.asarray(x_1).reshape(-1).astype(float),
567
                )
568

569
            def _transform_coordinates(x_0, x_1):
2✔
570
                mask_oob = (x_0 < arg_0_min) | (x_0 > arg_0_max) | (x_1 < arg_1_min) | (x_1 > arg_1_max)
2✔
571
                if allow_extrapolation:
2✔
572
                    _x_0 = _clip_with_fraction(x_0, arg_0_min, arg_0_max, max_extrapolation_fraction)
2✔
573
                    _x_1 = _clip_with_fraction(x_1, arg_1_min, arg_1_max, max_extrapolation_fraction)
2✔
574
                else:
575
                    _x_0 = np.clip(x_0, arg_0_min, arg_0_max)
2✔
576
                    _x_1 = np.clip(x_1, arg_1_min, arg_1_max)
2✔
577

578
                if independent[0] in ["Teff", "age"]:
2✔
579
                    _x_0 = np.log10(np.maximum(_x_0, arg_0_floor))
2✔
580
                if independent[1] in ["Teff", "age"]:
2✔
581
                    _x_1 = np.log10(np.maximum(_x_1, arg_1_floor))
2✔
582

583
                return _x_0, _x_1, mask_oob
2✔
584

585
            if interpolator.lower() == "ct":
2✔
586
                # Interpolate with the scipy CloughTocher2DInterpolator
587
                _atmosphere_interpolator = CloughTocher2DInterpolator(
2✔
588
                    (arg_0, arg_1),
589
                    model[dependent],
590
                    **_kwargs_for_CT,
591
                )
592
                _rbf_extrapolator = None
2✔
593
                if allow_extrapolation:
2✔
594
                    _rbf_extrapolator = RBFInterpolator(
2✔
595
                        np.stack((arg_0, arg_1), -1),
596
                        model[dependent],
597
                        **_kwargs_for_RBF,
598
                    )
599

600
                def atmosphere_interpolator(x0, x1=None):
2✔
601
                    x_0, x_1 = _prepare_two_input_arrays(x0, x1)
2✔
602
                    _x_0, _x_1, mask_oob = _transform_coordinates(x_0, x_1)
2✔
603
                    out = _atmosphere_interpolator(_x_0, _x_1)
2✔
604
                    out = np.asarray(out).reshape(-1)
2✔
605

606
                    if allow_extrapolation:
2✔
607
                        bad = ~np.isfinite(out)
2✔
608
                        if np.any(bad):
2✔
609
                            out[bad] = np.asarray(_rbf_extrapolator(np.column_stack((_x_0[bad], _x_1[bad])))).reshape(
2✔
610
                                -1
611
                            )
612
                        out = _sanitize_extrapolated(out, mask_oob)
2✔
613
                    elif np.any(mask_oob):
2✔
614
                        out[mask_oob] = -np.inf
2✔
615

616
                    return out
2✔
617

618
            elif interpolator.lower() == "rbf":
2✔
619
                # Interpolate with the scipy RBFInterpolator
620
                _atmosphere_interpolator = RBFInterpolator(
2✔
621
                    np.stack((arg_0, arg_1), -1),
622
                    model[dependent],
623
                    **_kwargs_for_RBF,
624
                )
625

626
                def atmosphere_interpolator(x0, x1=None):
2✔
627
                    x_0, x_1 = _prepare_two_input_arrays(x0, x1)
2✔
628
                    _x_0, _x_1, mask_oob = _transform_coordinates(x_0, x_1)
2✔
629
                    out = _atmosphere_interpolator(np.column_stack((_x_0, _x_1)))
2✔
630
                    out = np.asarray(out).reshape(-1)
2✔
631
                    if allow_extrapolation:
2✔
632
                        out = _sanitize_extrapolated(out, mask_oob)
2✔
633
                    elif np.any(mask_oob):
2✔
634
                        out[mask_oob] = -np.inf
2✔
635
                    return out
2✔
636

637
            else:
638
                raise ValueError("This should never happen.")
2✔
639

640
        else:
641
            raise TypeError("Please provide ONE varaible name as a string or list, or TWO varaible names in a list.")
2✔
642

643
        return atmosphere_interpolator
2✔
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