D47calib API documentation

# D47calib

Generate, combine, display and apply Δ47 calibrations

This library provides support for:

- computing Δ47 calibrations by applying OGLS regression to sets of (T, Δ47) observations
- combining Δ47 datasets to produce a combined calibration
- various methods useful for creating Δ47 calibration plots
- Using Δ47 calibrations to convert between T and Δ47, keeping track of covariance between inputs and/or uncertainties/covariance originating from calibration uncertainties. This may be done within Python code or by using a simple command-line interface (e.g., `D47calib input.csv > output.csv`).

# 1. Calibrations included

`D47calib` provides the following pre-built calibrations:

##### `breitenbach_2018`:

Cave pearls analyzed by Breitenbach et al. (2018).

Raw data were obtained from the original study’s supplementary information. The original publication processed data according to two sessions, each 4-5 months long, separated by 2 months. After reprocessing the original raw data using D47crunch, visual inspection of the standardization residuals defined revealed the presence of substantial drifts in both sessions. We thus assigned modified session boundaries defining four continuous measurement periods separated by 21 to 52 days, with new session lengths ranging from 24 to 80 days. The original data was not modified in any other way. Formation temperatures are from table 1 of the original study. We assigned arbitrary 95 % uncertainties of ±1 °C, which seem reasonable for cave environments.

##### `peral_2018`:

Planktic foraminifera analyzed by Peral et al. (2018), reprocessed by Daëron & Gray (2023).

Peral et al. [2018] reported Δ47 values of foraminifera from core-tops, both planktic and benthic, whose calcification temperature estimates were recently reassessed by Daëron & Gray [2023]. Here we only consider Peral et al.’s planktic data, excluding two benthic samples (cf Daëron & Gray for reasons why we only consider planktic samples for now). In our reprocessing, as in the original study, “samples” are defined by default as a unique combination of core site, species, and size fraction. Δ47 values are then standardized in the usual way, before using `D47crunch.combine_samples()` to combine all size fractions with the same core and species, except for G. inflata samples (cf Daëron & Gray and accompanying GitHub repository). By properly accounting for analytical error covariance between the Δ47 values to combine, this two-step approach avoids underestimating the final standardization errors. This

##### `jautzy_2020`:

Synthetic calcites analyzed by Jautzy et al. (2020).

Jautzy et al. reported data from a continuous period spanning 10 months, and used a moving-window approach to standardize their measurements. We assigned sessions defined, whenever possible, as periods of one or more complete weeks enclosing one of more unknown sample analyses. The resulting Δ47 residuals, on the order of 40 ppm (1SD), do not display evidence of instrumental drift. Formation temperatures are from table S2 of the original study. We assigned arbitrary 95 % uncertainties of ±1 °C, which seem reasonable for laboratory experiments.

##### `anderson_2021_mit`:

Various natural and synthetic carbonates analyzed at MIT by Anderson et al. (2021).

Raw IRMS data and temperature constraints were obtained from the original study’s supple- mentary information (tables S01 and S02). When reprocesseded the IRMS data we made no changes to the session defintions, but we excluded sessions 5 and 25 because they did not include any unknown sample analyses.

##### `anderson_2021_lsce`:

Slow-growing mammillary calcite from Devils Hole and Laghetto Basso analyzed at LSCE by Anderson et al. (2021).

Raw IRMS data is from the original study’s supplementary information (SI-S02). Temperature contraints are from table 1 in Daëron et al. (2019).

##### `fiebig_2021`:

Inorganic calcites analyzed by Fiebig et al. (2021).

Temperature contraints are duplicated from the earlier publications where the corresponding samples were first described [Daëron et al., 2019; Jautzy et al., 2020; Anderson et al., 2021]. Raw IRMS data and were obtained from the original study’s supplementary information, and processed as described by Fiebig et al. [2021], jointly using (a) heated and 25 °C-equilibrated CO2 to constrain the scrambling effect and compositional nonlinearity associated with each session, and (b) ETH-1 and ETH-2 reference materials to anchor unknown samples to the I-CDES scale.

##### `huyghe_2022`:

Marine calcitic bivalves analyzed by Huyghe et al. (2022).

Huyghe et al. reported Δ47 values of modern calcitic bivalves collected from localities with good environmental constraints. As was done in the original publication, different bivalve individuals were initially treated as distinct analytical samples. In some sites with strong seasonality, individuals were sub-sampled into winter-calcified a summer-calcified fractions. Δ47 values were then standardized in the usual way, before using `D47crunch.combine_samples()` method to combine all samples from the same locality. Calcification temperature estimates are from the original study.

##### `devils_laghetto_2023`:

Combined data set of slow-growing mammillary calcite from Devils Hole and Laghetto Basso, analyzed both at LSCE by Anderson et al. (2021) and at GU by Fiebig et al. (2021).

##### `ogls_2023`:

Combined data set including all of the above. For a detailed discussion of goodness-of-fit and regression uncertainties, see Daëron & Vermeesch (2023).

# 2. Command-line interface

`D47calib` also provides a command-line interface (CLI) for converting between Δ47 and temperature values, computing uncertainties for each computed value (and how these uncertainties are correlated with each other) from different sources (from calibration errors alone, from measurement errors alone, and from both). The computed uncertainties are provided as standard errors, correlation matrix and/or covariance matrix. Input and output files may be comma-separated, tab-separated, or printed out as visually aligned data columns.

## 2.1 Simple examples

Start with the simplest input file possible (here named `input.csv`):

    D47
    0.567

Then process it:

    D47calib input.csv

This prints out:

      D47      T  T_SE_from_calib  T_correl_from_calib  T_SE_from_input  T_correl_from_input  T_SE_from_both  T_correl_from_both
    0.567  34.20             0.38                1.000             0.00                1.000            0.38               1.000

- `T` is the temperature corresponding to a `D47` value of 0.567 ‰ according to the default calibration (`ogls_2023`).
- `T_SE_from_calib` is the standard error on `T` from the calibration uncertainty
- `T_correl_from_calib` is the correlation matrix for the `T_SE_from_calib` values. Because here there is only one value, this is a 1-by-1 matrix with a single value of one, which is not very exciting.
- `T_SE_from_input` is the standard error on `T` from the measurement uncertainties on `D47`. Because these are not specified here, `T_SE_from_input` is equal to zero.
- `T_correl_from_input` is, predictably, the correlation matrix for the `T_SE_from_input` values. Because here there is only one value, this is a 1-by-1 matrix with a single value of one, you know the drill.
- `T_SE_from_both` is the standard error on `T` obtained by combining the two previously considered sources of uncertainties.
- `T_correl_from_both` is what you expect it to be if you've been reading so far. Can you guess why it is a 1-by-1 matrix with a single value of one?

### 2.1.1 Adding `D47` measurement uncertainties

This can be done by adding a column to `input.csv`:

    D47,D47_SE
    0.567,0.008

Because this is not very human-friendly, we'll replace the comma separators by whitespace. We'll also add a column listing sample names:

    Sample   D47    D47_SE
    FOO-1  0.567   0.008

Then process it. We're adding an option (`-i ' '`, or `--delimiter-in ' '`) specifying that we're no longer using commas but whitespaces as delimiters:

    D47calib -i ' ' input.csv

This yields:

    Sample   D47  D47_SE      T  T_SE_from_calib  T_correl_from_calib  T_SE_from_input  T_correl_from_input  T_SE_from_both  T_correl_from_both
    FOO-1  0.567   0.008  34.20             0.38                1.000             2.91                1.000            2.94               1.000

You can see that `T_SE_from_input` is now much larger than `T_SE_from_calib`, and that the combined `T_SE_from_both` is equal to the quadratic sum of `T_SE_from_calib` and `T_SE_from_input`.

### 2.1.2 Converting more than one measurement

Let's add lines to our input file:

    Sample   D47  D47_SE
    FOO-1  0.567   0.008
    BAR-2  0.575   0.009
    BAZ-3  0.582   0.007

Which yields:

    Sample   D47  D47_SE      T  T_SE_from_calib  T_correl_from_calib                T_SE_from_input  T_correl_from_input                T_SE_from_both  T_correl_from_both
    FOO-1  0.567   0.008  34.20             0.38                1.000  0.996  0.987             2.91                1.000  0.000  0.000            2.94               1.000  0.015  0.019
    BAR-2  0.575   0.009  31.33             0.37                0.996  1.000  0.997             3.18                0.000  1.000  0.000            3.21               0.015  1.000  0.017
    BAZ-3  0.582   0.007  28.89             0.36                0.987  0.997  1.000             2.42                0.000  0.000  1.000            2.44               0.019  0.017  1.000

A notable change are the 3-by-3 correlation matrices, which tell us how the `T` errors or these three measurements covary. `T_correl_from_calib` shows that the `T_SE_from_calib` errors are strongly correlated, because the three `D47` values are close to each other. `T_correl_from_input` indicates statistically independent `T_SE_from_input` errors. This may be true or not, but it is the expected result because our input file does not include any information on how the `D47_SE` errors may covary (see below how this additional info may be specified). Thus in this case `D47calib` assumes that the `D47` values are statistically independent (gentle reminder: this is often not the case, see below).

Note that because `T_SE_from_input` errors are much larger than `T_SE_from_calib` errors, the combined `T_SE_from_both` errors are only weakly correlated, as seen in `T_correl_from_both`.

### 2.1.3 Accounting for correlations in `D47` errors

Because Δ47 measurements performed in the same analytical session(s) are not statistically independent, we may add to `input.csv` a correlation matrix describing how `D47_SE` errors covary.

One simple way to compute this correlation matrix is to use the `save_D47_correl()` method from the `D47crunch` library (PyPI, GitHub, Zenodo) described by Daëron (2021).

    Sample   D47  D47_SE   D47_correl
    FOO-1  0.567   0.008   1.00  0.25  0.25
    BAR-2  0.575   0.009   0.25  1.00  0.25
    BAZ-3  0.582   0.007   0.25  0.25  1.00

This yields:

    Sample   D47  D47_SE  D47_correl                  T  T_SE_from_calib  T_correl_from_calib                T_SE_from_input  T_correl_from_input                T_SE_from_both  T_correl_from_both
    FOO-1  0.567   0.008        1.00  0.25  0.25  34.20             0.38                1.000  0.996  0.987             2.91                1.000  0.250  0.250            2.94               1.000  0.261  0.264
    BAR-2  0.575   0.009        0.25  1.00  0.25  31.33             0.37                0.996  1.000  0.997             3.18                0.250  1.000  0.250            3.21               0.261  1.000  0.263
    BAZ-3  0.582   0.007        0.25  0.25  1.00  28.89             0.36                0.987  0.997  1.000             2.42                0.250  0.250  1.000            2.44               0.264  0.263  1.000

What changed ? We now have propagated `D47_correl` into `T_correl_from_input`, and this is accounted for in the combined correlation matrix `T_correl_from_both`. Within the framework of our initial assumptions (multivariate Gaussian errors, first-order linear propagation of uncertainties...), this constitutes the “best” (or rather, the most “information-complete”) description of uncertainties constraining our final `T` estimates.

With increasing number of measurements, these correlation matrices become quite large, so that it becomes useless to print them out visually. To facilitate using the output of `D47calib` as an input to another piece of software, one may use the `-j` or `--delimiter-out` option to use machine-readable delimiters such as commas or tabs, and the `'-o'` or `--output-file` option to save the output as a file instead of printing it out:

    D47calib -i ' ' -j ',' -o output.csv input.csv

This will create the following `output.csv` file:

    Sample,D47,D47_SE,D47_correl,,,T,T_SE_from_calib,T_correl_from_calib,,,T_SE_from_input,T_correl_from_input,,,T_SE_from_both,T_correl_from_both,,
    FOO-1,0.567,0.008,1.00,0.25,0.25,34.20,0.38,1.000,0.996,0.987,2.91,1.000,0.250,0.250,2.94,1.000,0.261,0.264
    BAR-2,0.575,0.009,0.25,1.00,0.25,31.33,0.37,0.996,1.000,0.997,3.18,0.250,1.000,0.250,3.21,0.261,1.000,0.263
    BAZ-3,0.582,0.007,0.25,0.25,1.00,28.89,0.36,0.987,0.997,1.000,2.42,0.250,0.250,1.000,2.44,0.264,0.263,1.000

Hint for Mac users: Quick Look (or “spacebar preview”, i.e. what happens when you select a file in the Finder and press the spacebar once) provides you with a nice view of a csv file when you just want to check the results visually, as long as you use a comma delimiter.

### 2.1.4 Converting from `T` to `D47`

Everything described above works in the other direction as well, without changing anything to the command-line instruction:

    T   T_SE
    0    0.5
    10   1.0
    20   2.0

Yields:

     T  T_SE     D47  D47_SE_from_calib  D47_correl_from_calib                D47_SE_from_input  D47_correl_from_input                D47_SE_from_both  D47_correl_from_both
     0   0.5  0.6798             0.0016                  1.000  0.969  0.848             0.0020                  1.000  0.000  0.000            0.0025                 1.000  0.210  0.091
    10   1.0  0.6424             0.0013                  0.969  1.000  0.952             0.0035                  0.000  1.000  0.000            0.0038                 0.210  1.000  0.056
    20   2.0  0.6090             0.0011                  0.848  0.952  1.000             0.0063                  0.000  0.000  1.000            0.0064                 0.091  0.056  1.000

## 2.2 Integration with `D47crunch`

Starting with the following input file `rawdata.csv`:

    UID Sample  Session d45 d46 d47 d48 d49
    1   BAR-2   Session_01  -9.941731   10.985508   0.208381    21.832546   10.707292
    2   FOO-1   Session_01  -0.848380   2.872996    1.535960    5.431873    4.665655
    3   ETH-1   Session_01  6.009875    10.711152   16.087994   21.275325   27.780042
    4   ETH-3   Session_01  5.726647    11.144602   16.634507   22.275401   28.306614
    5   BAZ-3   Session_01  -5.394955   7.938127    1.887702    15.671857   9.739724
    6   ETH-1   Session_01  6.050787    10.800497   16.232192   21.429453   27.780042
    7   ETH-2   Session_01  -5.981599   -6.011356   -12.728742  -12.335559  -18.023381
    8   ETH-1   Session_01  5.994522    10.810755   16.181039   21.445703   27.780042
    9   ETH-2   Session_01  -5.991066   -5.968789   -12.685206  -12.219412  -18.023381
    10  ETH-2   Session_01  -5.973121   -5.894178   -12.599303  -12.037594  -18.023381
    11  ETH-3   Session_01  5.734551    11.043266   16.556578   22.005474   28.306614
    12  ETH-3   Session_01  5.755765    11.179326   16.689870   22.294132   28.306614
    13  ETH-3   Session_02  5.756394    11.109906   16.635806   22.189179   28.306614
    14  ETH-3   Session_02  5.720443    11.187928   16.689130   22.308575   28.306614
    15  BAR-2   Session_02  -9.951732   10.964153   0.171443    21.779172   10.707292
    16  ETH-2   Session_02  -5.993596   -6.086738   -12.816033  -12.452397  -18.023381
    17  ETH-3   Session_02  5.717666    11.175023   16.654427   22.251497   28.306614
    18  ETH-2   Session_02  -5.952660   -5.887239   -12.582248  -12.081588  -18.023381
    19  BAZ-3   Session_02  -5.337619   7.818571    1.818572    15.400966   9.739724
    20  ETH-2   Session_02  -5.983050   -5.953794   -12.679809  -12.224144  -18.023381
    21  ETH-1   Session_02  6.029949    10.682183   16.080186   21.182471   27.780042
    22  FOO-1   Session_02  -0.835316   2.868058    1.539147    5.483628    4.665655
    23  ETH-1   Session_02  6.019913    10.773652   16.165645   21.309536   27.780042
    24  ETH-1   Session_02  5.995178    10.702934   16.073811   21.169324   27.780042

The following script will read thart raw data, fully process it, convert the standardized output to temperatures, and save the final results to a file named `output.csv`:

    D47crunch rawdata.csv
    D47calib -o output.csv -j '>' output/D47_correl.csv

With the contents of `output.csv` being:

    Sample     D47  D47_SE  D47_correl                      T  T_SE_from_calib  T_correl_from_calib                 T_SE_from_input  T_correl_from_input                T_SE_from_both  T_correl_from_both
     BAR-2  0.6777  0.0066      1.0000  0.3586  0.2798   0.51             0.41                1.000  0.731  -0.036             1.68                1.000  0.359  0.280            1.73               1.000  0.373  0.262
     BAZ-3  0.5894  0.0060      0.3586  1.0000  0.2473  26.34             0.35                0.731  1.000   0.654             2.02                0.359  1.000  0.247            2.05               0.373  1.000  0.263
     FOO-1  0.4873  0.0056      0.2798  0.2473  1.0000  68.21             0.69               -0.036  0.654   1.000             2.82                0.280  0.247  1.000            2.90               0.262  0.263  1.000

If a simpler output is required, just add the `--ignore-correl` or `-g` option to the second line above, which should yield:

    Sample     D47  D47_SE      T  T_SE_from_calib  T_SE_from_input  T_SE_from_both
     BAR-2  0.6777  0.0066   0.51             0.41             1.68            1.73
     BAZ-3  0.5894  0.0060  26.34             0.35             2.02            2.05
     FOO-1  0.4873  0.0056  68.21             0.69             2.82            2.90

## 2.3 Further customizing the CLI

A complete list of options is provided by `D47calib --help`.

### 2.3.1 Using covariance instead of correlation matrix as input

Just provide `D47_covar` (or `T_covar` when converting in the other direction) in the input file instead of `D47_SE` and `D47_correl`.

### 2.3.2 Reporting covariance instead of correlation matrices in the output

Use the `--return-covar` option.

### 2.3.3 Reporting neither covariances nor correlations in the output

If you don't care about all this covariance nonsense, or just wish for an output that does't hurt your eyes, you can use the `--ignore-correl` option. Standard errors will still be reported.

### 2.3.4 Excluding or only including certain lines (samples) from the input

To filter the samples (lines) to process using `--exclude-samples` and `--include-samples`, first add a `Sample` column to the input data, assign a sample name to each line.

Then to exclude some samples, provide the `--exclude-samples` option with the name of a file where each line is one sample to exclude.

To exclude all samples except those listed in a file, provide the `--include-samples` option with the name of that file, where each line is one sample to include.

### 2.3.5 Changing the numerical precision of the output

This is controlled by the following options:

- `--T-precision` or `-p` (default: 2): All `T` and `T_SE_*` values
- `--D47-precision` or `-q` (default: 4): All `D47` and `D47_SE_*` values
- `--correl-precision` or `-r` (default: 3): All `*_correl_*` values
- `--covar-precision` or `-s` (default: 3): All `*_covar_*` values

### 2.3.6 Using a different Δ47 calibration

You may use a different calibration than the default `ogls_2023` using the `--calib` or `-c` option. Any predefeined calibration from the `D47calib` library is a valid option.

You may also specify an arbitrary polynomial function of inverse `T`, by creating a file (e.g., `calib.csv`) with the following format:

    degree    coef        covar
    0       0.1741   2.4395e-05  -0.0262821       5.634934
    1      -17.889   -0.0262821    32.17712       -7223.86
    2        42614     5.634934    -7223.86    1654633.996

Then using `-c calib.csv` will use this new calibration.

If you don't know/care about the covariance of the calibration coefficients, just leave out the covar terms:

    degree    coef
    0       0.1741
    1      -17.889
    2        42614

In this case, all `*_SE_from_calib` outputs will be equal to zero, but the `*_from_input` uncertainties will still be valid (and identical to `*_from_both` uncertainties, since we are ignoring calibration uncertainties).

View Source

       1"""
       2Generate, combine, display and apply Δ47 calibrations
       3
       4This library provides support for:
       5
       6- computing Δ47 calibrations by applying OGLS regression to sets of (T, Δ47) observations
       7- combining Δ47 datasets to produce a combined calibration
       8- various methods useful for creating Δ47 calibration plots
       9- Using Δ47 calibrations to convert between T and Δ47, keeping track of covariance between inputs
      10and/or uncertainties/covariance originating from calibration uncertainties. This may be done within
      11Python code or by using a simple command-line interface (e.g., `D47calib input.csv > output.csv`).
      12
      13.. include:: ../../docpages/calibs.md
      14.. include:: ../../docpages/cli.md
      15
      16* * *
      17"""
      18
      19__author__    = 'Mathieu Daëron'
      20__contact__   = 'daeron@lsce.ipsl.fr'
      21__copyright__ = 'Copyright (c) 2023 Mathieu Daëron'
      22__license__   = 'MIT License - https://opensource.org/licenses/MIT'
      23# __docformat__ = "restructuredtext"
      24__date__      = '2023-09-18'
      25__version__   = '1.0'
      26
      27
      28import typer, sys
      29from typing_extensions import Annotated
      30import ogls as _ogls
      31import numpy as _np
      32from scipy.linalg import block_diag as _block_diag
      33from scipy.interpolate import interp1d as _interp1d
      34from matplotlib import pyplot as _ppl
      35
      36typer.rich_utils.STYLE_HELPTEXT = ''
      37
      38class D47calib(_ogls.InverseTPolynomial):
      39	"""
      40	Δ47 calibration class based on OGLS regression
      41	of Δ47 as a polynomial function of inverse T.
      42	"""
      43
      44	def __init__(self,
      45		samples, T, D47,
      46		sT = None,
      47		sD47 = None,
      48		degrees = [0,2],
      49		xpower = 2,
      50		name = '',
      51		label = '',
      52		description = '',
      53		**kwargs,
      54		):
      55		"""
      56		### Parameters
      57
      58		+ **samples**: a list of N sample names.
      59		+ **T**: a 1-D array (or array-like) of temperatures values (in degrees C), of size N.
      60		+ **D47**: a 1-D array (or array-like) of Δ47 values (in permil), of size N.
      61		+ **sT**: uncertainties on `T`. If specified as:
      62		  + a scalar: `sT` is treated as the standard error applicable to all `T` values;
      63		  + a 1-D array-like of size N: `sT` is treated as the standard errors of `T`;
      64		  + a 2-D array-like of size (N, N): `sT` is treated as the (co)variance matrix of `T`.
      65		+ **sD47**: uncertainties on `D47`. If specified as:
      66		  + a scalar: `sD47` is treated as the standard error applicable to all `D47` values;
      67		  + a 1-D array-like of size N: `sD47` is treated as the standard errors of `D47`;
      68		  + a 2-D array-like of size (N, N): `sD47` is treated as the (co)variance matrix of `D47`.
      69		+ **degrees**: degrees of the polynomial regression, e.g., `[0, 2]` or `[0, 1, 2, 3, 4]`.
      70		+ **name**: a human-readable, short name assigned to the calibration.
      71		+ **label**: a short description of the calibration, e.g., to be used in legends.
      72		+ **description**: a longer description, including relevant references/DOIs.
      73		This is not necessary when `bfp` and `CM_bfp` are specified at instantiation time.
      74		+ **kwargs**: keyword arguments passed to the underlying `ogls.InverseTPolynomial()` call.
      75
      76		### Notable attributes
      77
      78		+ **N**:
      79		The total number of observations (samples) in the calibration data.
      80		+ **samples**:
      81		The list sample names.
      82		+ **T**:
      83		1-D `ndarray` of temperatures in degrees C.
      84		+ **D47**:
      85		1-D `ndarray` of Δ47 values in permil.
      86		+ **sT**:
      87		2-D `ndarray` equal to the full (co)variance matrix for `T`.
      88		+ **D47**:
      89		2-D `ndarray` equal to the full (co)variance matrix for `D47`.
      90		+ **xpower**:
      91		By default, all `D47calib` graphical methods plot Δ47 as a function of 1/T<sup>2</sup>.
      92		It is possible to change this behavior to use a different power of 1/T.
      93		This is done by redefining the `xpower` attribute to a different, non-zero `int` value
      94		(e.g. `foo.xpower = 1` to plot as a function of 1/T instead of 1/T<sup>2</sup>).
      95		+ **bfp**:
      96		The best-fit parameters of the regression.
      97		This is a `dict` with keys equal to the polynomial coefficients (see `bff` definition below)
      98		+ **bff()**:
      99		The best-fit polynomial function of inverse T, defined as:
     100		`bff(x) = sum(bfp[f'a{k}'] * x**k for k in degrees)`
     101		Note that `bff` takes `x = 1/(T+273.15)` (instead of `T`) as input.
     102
     103
     104		### Examples
     105
     106		A very simple example:
     107
     108		````py
     109		.. include:: ../../code_examples/D47calib_init/example.py
     110		````
     111
     112		Should yield:
     113
     114		````
     115		.. include:: ../../code_examples/D47calib_init/output.txt
     116		````
     117
     118		"""
     119
     120		self.samples = samples[:]
     121		self.name = name
     122		self.label = label
     123		self.description = description
     124		self.D47 = _np.asarray(D47, dtype = 'float')
     125		self.N = self.D47.size
     126
     127		if sD47 is None:
     128			self.sD47 = _np.zeros((self.N, self.N))
     129		else:
     130			self.sD47 = _np.asarray(sD47)
     131			if len(self.sD47.shape) == 1:
     132				self.sD47 = _np.diag(self.sD47**2)
     133			elif len(self.sD47.shape) == 0:
     134				self.sD47 = _np.eye(self.D47.size) * self.sD47**2
     135
     136		_ogls.InverseTPolynomial.__init__(self, T=T, Y=D47, sT=sT, sY=sD47, degrees = degrees, xpower = xpower, **kwargs)
     137
     138		if self.bfp is None:
     139			self.regress()
     140
     141		self._bff_deriv = lambda x: _np.array([k * self.bfp[f'a{k}'] * x**(k-1) for k in degrees if k > 0]).sum(axis = 0)
     142
     143		xi = _np.linspace(0,200**-1,1001)
     144		self._inv_bff = _interp1d(self.bff(xi), xi)
     145
     146		self._D47_from_T = lambda T: self.bff((T+273.15)**-1)
     147		self._T_from_D47 = lambda D47: self._inv_bff(D47)**-1 - 273.15
     148		self._D47_from_T_deriv = lambda T: -(T+273.15)**-2 * self._bff_deriv((T+273.15)**-1)
     149		self._T_from_D47_deriv = lambda D47: self._D47_from_T_deriv(self._T_from_D47(D47))**-1
     150
     151	def __repr__(self):
     152		return f'<D47calib: {self.name}>'
     153
     154	def invT_xaxis(self,
     155		xlabel = None,
     156		Ti = [0,20,50,100,250,1000],
     157		):
     158		"""
     159		Create and return an `Axes` object with X values equal to 1/T<sup>2</sup>,
     160		but labeled in degrees Celsius.
     161
     162		### Parameters
     163
     164		+ **xlabel**:
     165		Custom label for X axis (`r'$1\,/\,T^2$'` by default)
     166		+ **Ti**:
     167		Specify tick locations for X axis, in degrees C.
     168
     169		### Returns
     170
     171		+ an `matplotlib.axes.Axes` instance
     172
     173		### Examples
     174
     175		````py
     176		.. include:: ../../code_examples/D47calib_invT_xaxis/example_1.py
     177		````
     178
     179		This should result in something like this:
     180
     181		<img align="center" src="example_invT_xaxis_1.png">
     182
     183		It is also possible to define the X axis using a different power of 1/T
     184		by first redefining the `xpower` attribute:
     185
     186		````py
     187		.. include:: ../../code_examples/D47calib_invT_xaxis/example_2.py
     188		````
     189
     190		This should result in something like this:
     191
     192		<img align="center" src="example_invT_xaxis_2.png">
     193		"""
     194		if xlabel is None:
     195			xlabel = f'$1\\,/\\,T^{self.xpower}$' if self.xpower > 1 else '1/T'
     196		_ppl.xlabel(xlabel)
     197		_ppl.xticks([(273.15 + t) ** -self.xpower for t in sorted(Ti)[::-1]])
     198		ax = _ppl.gca()
     199		ax.set_xticklabels([f"${t}\\,$°C" for t in sorted(Ti)[::-1]])
     200		ax.tick_params(which="major")
     201
     202		return ax
     203
     204
     205	def plot_data(self, label = False, **kwargs):
     206		"""
     207		Plot Δ47 value of each sample as a function of 1/T<sup>2</sup>.
     208
     209		### Parameters
     210
     211		+ **label**:
     212		  + If `label` is a string, use this string as `label` for the underlyig
     213		  `matplotlib.pyplot.plot()` call.
     214		  + If `label = True`, use the caller's `label` attribute instead.
     215		  + If `label = False`, no label is specified (default behavior).
     216		+ **kwargs**:
     217		keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call.
     218
     219		### Returns
     220
     221		+ the return value(s) of the underlying `matplotlib.pyplot.plot()` call.
     222
     223		### Example
     224
     225		````py
     226		from matplotlib import pyplot as ppl
     227		from D47calib import huyghe_2022 as calib
     228
     229		fig = ppl.figure(figsize = (5,3))
     230		ppl.subplots_adjust(bottom = .25, left = .15)
     231		calib.invT_xaxis(Ti = [0,10,25])
     232		calib.plot_data(label = True)
     233		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
     234		ppl.legend()
     235		ppl.savefig('example_plot_data.png', dpi = 100)
     236		`````
     237
     238		This should result in something like this:
     239
     240		<img align="center" src="example_plot_data.png">
     241		"""
     242# 		if 'mec' not in kwargs:
     243# 			kwargs['mec'] = self.color
     244		if label is not False:
     245			kwargs['label'] = self.label if label is True else label
     246		return _ogls.InverseTPolynomial.plot_data(self, **kwargs)
     247
     248
     249	def plot_error_bars(self, **kwargs):
     250		"""
     251		Plot Δ47 error bars (±1.96 SE) of each sample as a function of 1/T<sup>2</sup>.
     252
     253		### Parameters
     254
     255		+ **kwargs**:
     256		keyword arguments passed to the underlying `matplotlib.pyplot.errrobar()` call.
     257
     258		### Returns
     259
     260		+ the return value(s) of the underlying `matplotlib.pyplot.errorbar()` call.
     261
     262		### Example
     263
     264		````py
     265		from matplotlib import pyplot as ppl
     266		from D47calib import huyghe_2022 as calib
     267
     268		fig = ppl.figure(figsize = (5,3))
     269		ppl.subplots_adjust(bottom = .25, left = .15)
     270		calib.invT_xaxis(Ti = [0,10,25])
     271		calib.plot_error_bars(alpha = .4)
     272		calib.plot_data(label = True)
     273		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
     274		ppl.legend()
     275		ppl.savefig('example_plot_error_bars.png', dpi = 100)
     276		`````
     277
     278		This should result in something like this:
     279
     280		<img align="center" src="example_plot_error_bars.png">
     281		"""
     282# 		if 'ecolor' not in kwargs:
     283# 			kwargs['ecolor'] = self.color
     284		return _ogls.InverseTPolynomial.plot_error_bars(self, **kwargs)
     285
     286
     287	def plot_error_ellipses(self, **kwargs):
     288		"""
     289		Plot Δ47 error ellipses (95 % confidence) of each sample as a function of 1/T<sup>2</sup>.
     290
     291		### Parameters
     292
     293		+ **kwargs**:
     294		keyword arguments passed to the underlying `matplotlib.patches.Ellipse()` call.
     295
     296		### Returns
     297
     298		+ the return value(s) of the underlying `matplotlib.patches.Ellipse()` call.
     299
     300		### Example
     301
     302		````py
     303		from matplotlib import pyplot as ppl
     304		from D47calib import huyghe_2022 as calib
     305
     306		fig = ppl.figure(figsize = (5,3))
     307		ppl.subplots_adjust(bottom = .25, left = .15)
     308		calib.invT_xaxis(Ti = [0,10,25])
     309		calib.plot_error_ellipses(alpha = .4)
     310		calib.plot_data(label = True)
     311		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
     312		ppl.legend()
     313		ppl.savefig('example_plot_error_ellipses.png', dpi = 100)
     314		`````
     315
     316		This should result in something like this:
     317
     318		<img align="center" src="example_plot_error_ellipses.png">
     319		"""
     320# 		if 'ec' not in kwargs:
     321# 			kwargs['ec'] = self.color
     322		return _ogls.InverseTPolynomial.plot_error_ellipses(self, **kwargs)
     323
     324
     325	def plot_bff(self, label = False, **kwargs):
     326		"""
     327		Plot best-fit regression of Δ47 as a function of 1/T<sup>2</sup>.
     328
     329		### Parameters
     330
     331		+ **label**:
     332		  + If `label` is a string, use this string as `label` for the underlyig
     333		  `matplotlib.pyplot.plot()` call.
     334		  + If `label = True`, use the caller's `label` attribute instead.
     335		  + If `label = False`, no label is specified (default behavior).
     336		+ **kwargs**:
     337		keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call.
     338
     339		### Returns
     340
     341		+ the return value(s) of the underlying `matplotlib.pyplot.plot()` call.
     342
     343		### Example
     344
     345		````py
     346		from matplotlib import pyplot as ppl
     347		from D47calib import huyghe_2022 as calib
     348
     349		fig = ppl.figure(figsize = (5,3))
     350		ppl.subplots_adjust(bottom = .25, left = .15)
     351		calib.invT_xaxis(Ti = [0,10,25])
     352		calib.plot_bff(label = True, dashes = (8,2,2,2))
     353		calib.plot_data()
     354		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
     355		ppl.legend()
     356		ppl.savefig('example_plot_bff.png', dpi = 100)
     357		`````
     358
     359		This should result in something like this:
     360
     361		<img align="center" src="example_plot_bff.png">
     362		"""
     363# 		if 'color' not in kwargs:
     364# 			kwargs['color'] = self.color
     365		if label is not False:
     366			kwargs['label'] = self.label if label is True else label
     367		return _ogls.InverseTPolynomial.plot_bff(self, **kwargs)
     368
     369
     370	def plot_bff_ci(self, **kwargs):
     371		"""
     372		Plot 95 % confidence region for best-fit regression of Δ47 as a function of 1/T<sup>2</sup>.
     373
     374		### Parameters
     375
     376		+ **label**:
     377		+ **kwargs**:
     378		keyword arguments passed to the underlying `matplotlib.pyplot.fill_between()` call.
     379
     380		### Returns
     381
     382		+ the return value(s) of the underlying `matplotlib.pyplot.fill_between()` call.
     383
     384		### Example
     385
     386		````py
     387		from matplotlib import pyplot as ppl
     388		from D47calib import huyghe_2022 as calib
     389
     390		fig = ppl.figure(figsize = (5,3))
     391		ppl.subplots_adjust(bottom = .25, left = .15)
     392		calib.invT_xaxis(Ti = [0,10,25])
     393		calib.plot_bff_ci(alpha = .15)
     394		calib.plot_bff(label = True, dashes = (8,2,2,2))
     395		calib.plot_data()
     396		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
     397		ppl.legend()
     398		ppl.savefig('example_plot_bff_ci.png', dpi = 100)
     399		`````
     400
     401		This should result in something like this:
     402
     403		<img align="center" src="example_plot_bff_ci.png">
     404		"""
     405# 		if 'color' not in kwargs:
     406# 			kwargs['color'] = self.color
     407		return _ogls.InverseTPolynomial.plot_bff_ci(self, **kwargs)
     408
     409	def T47(self,
     410		D47 = None,
     411		sD47 = None,
     412		T=None,
     413		sT = None,
     414		error_from = 'both',
     415		return_covar = False,
     416		):
     417		'''
     418		When `D47` is input, computes corresponding T value(s).
     419		`D47` input may be specified as a scalar, or as a 1-D array.
     420		`T` output will then have the same type and size as `D47`.
     421
     422		When `T` is input, computes corresponding Δ47 value(s).
     423		`T` input may be specified as a scalar, or as a 1-D array.
     424		`D47` output will then have the same type and size as `T`.
     425
     426		Only one of either `D47` or `T` may be specified as input.
     427
     428		**Arguments:**
     429
     430		* `D47`: Δ47 value(s) to convert into temperature (`float` or 1-D array)
     431		* `sD47`: Δ47 uncertainties, which may be:
     432		  - `None` (default)
     433		  - `float` or `int` (uniform standard error on `D47`)
     434		  - 1-D array (standard errors on `D47`)
     435		  - 2-D array (covariance matrix for `D47`)
     436		* `T`: T value(s) to convert into Δ47 (`float` or 1-D array), in degrees C
     437		* `sT`: T uncertainties, which may be:
     438		  - `None` (default)
     439		  - `float` or `int` (uniform standard error on `T`)
     440		  - 1-D array (standard errors on `T`)
     441		  - 2-D array (variance-covariance matrix for `T`)
     442		* `error_from`: if set to `'both'` (default), returned errors take into account
     443		  input uncertainties (`sT` or `sD47`) as well as calibration uncertainties;
     444		  if set to `'calib'`, only calibration uncertainties are accounted for;
     445		  if set to `'sT'` or `'sD47'`, calibration uncertainties are ignored.
     446		* `return_covar`: (False by default) whether to return the full covariance matrix
     447		  for returned `T` or `D47` values, otherwise return standard errors for the returned
     448		  `T` or `D47` values instead.
     449
     450		**Returns (with `D47` input):**
     451
     452		* `T`: temperature value(s) computed from `D47`
     453		* `sT`: uncertainties on `T` value(s), whether as standard error(s) or covariance matrix
     454
     455		**Returns (with `T` input):**
     456
     457		* `D47`: Δ47 value(s) computed from `D47`
     458		* `sD47`: uncertainties on `D47` value(s), whether as standard error(s) or covariance matrix
     459
     460		### Example
     461
     462		````py
     463		import numpy as np
     464		from matplotlib import pyplot as ppl
     465		from D47calib import ogls_2023 as calib
     466
     467		X = np.linspace(1473**-2, 270**-2)
     468		D47, sD47 = calib.T47(T = X**-0.5 - 273.15)
     469
     470		fig = ppl.figure(figsize = (5,3))
     471		ppl.subplots_adjust(bottom = .25, left = .15)
     472		calib.invT_xaxis()
     473		ppl.plot(X, 1000 * sD47, 'r-')
     474		ppl.ylabel('Calibration SE on $Δ_{47}$ values (ppm)')
     475		ppl.savefig('example_SE47.png', dpi = 100)
     476		`````
     477
     478		This should result in something like this:
     479
     480		<img src="example_SE47.png">
     481		'''
     482
     483		if D47 is None and T is None:
     484			raise ValueError('Either D47 or T must be specified, but both are undefined.')
     485
     486		if D47 is not None and T is not None:
     487			raise ValueError('Either D47 or T must be specified, but not both.')
     488
     489		if T is not None:
     490
     491			D47 = self._D47_from_T(T)
     492			Np = len(self.degrees)
     493			N = D47.size
     494
     495			### Compute covariance matrix of (*bfp, *T):
     496			CM = _np.zeros((Np+N, Np+N))
     497
     498			if error_from in ['calib', 'both']:
     499				CM[:Np, :Np] = self.bfp_CM[:,:]
     500
     501			if (sT is not None) and error_from in ['sT', 'both']:
     502				_sT = _np.asarray(sT)
     503				if _sT.ndim == 0:
     504					for k in range(N):
     505						CM[Np+k, Np+k] = _sT**2
     506				elif _sT.ndim == 1:
     507					for k in range(N):
     508						CM[Np+k, Np+k] = _sT[k]**2
     509				elif _sT.ndim == 2:
     510					CM[-N:, -N:] = _sT[:,:]
     511
     512			### Compute Jacobian of D47(T) relative to (*bfp, *T):
     513			_T = _np.asarray(T)
     514			if _T.ndim == 0:
     515				_T = _np.expand_dims(_T, 0)
     516			J = _np.zeros((N, Np+N))
     517
     518			if (sT is not None) and error_from in ['sT', 'both']:
     519				for k in range(N):
     520					J[k, Np+k] = self._D47_from_T_deriv(_T[k])
     521
     522			if error_from in ['calib', 'both']:
     523
     524				for k in range(Np):
     525
     526					p1 = {_: self.bfp[_] for _ in self.bfp}
     527					p1[f'a{self.degrees[k]}'] += 0.001 * self.bfp_CM[k,k]**.5
     528
     529					p2 = {_: self.bfp[_] for _ in self.bfp}
     530					p2[f'a{self.degrees[k]}'] -= 0.001 * self.bfp_CM[k,k]**.5
     531
     532					J[:, k] = (self.model_fun(p1, (_T+273.15)**-1) - self.model_fun(p2, (_T+273.15)**-1)) / (0.002 * self.bfp_CM[k,k]**.5)
     533
     534			### Error propagation:
     535			CM_D47 = J @ CM @ J.T
     536
     537			if return_covar:
     538				return D47, CM_D47
     539			else:
     540				return D47, float(_np.diag(CM_D47)**.5) if D47.ndim == 0 else _np.diag(CM_D47)**.5
     541
     542		if D47 is not None:
     543
     544			T = self._T_from_D47(D47)
     545			Np = len(self.degrees)
     546			N = T.size
     547
     548			### Compute covariance matrix of (*bfp, *T):
     549			CM = _np.zeros((Np+N, Np+N))
     550
     551			if error_from in ['calib', 'both']:
     552				CM[:Np, :Np] = self.bfp_CM[:,:]
     553
     554			if (sD47 is not None) and error_from in ['sD47', 'both']:
     555				_sD47 = _np.asarray(sD47)
     556				if _sD47.ndim == 0:
     557					for k in range(N):
     558						CM[Np+k, Np+k] = _sD47**2
     559				elif _sD47.ndim == 1:
     560					for k in range(N):
     561						CM[Np+k, Np+k] = _sD47[k]**2
     562				elif _sD47.ndim == 2:
     563					CM[-N:, -N:] = _sD47[:,:]
     564
     565			### Compute Jacobian of T(D47) relative to (*bfp, *D47):
     566			_D47 = _np.asarray(D47)
     567			if _D47.ndim == 0:
     568				_D47 = _np.expand_dims(_D47, 0)
     569			J = _np.zeros((N, Np+N))
     570			if (sD47 is not None) and error_from in ['sD47', 'both']:
     571				for k in range(N):
     572					J[k, Np+k] = self._T_from_D47_deriv(_D47[k])
     573			if error_from in ['calib', 'both']:
     574
     575				xi = _np.linspace(0,200**-1,1001)[1:]
     576				for k in range(Np):
     577
     578					if self.bfp_CM[k,k]:
     579						_epsilon_ = self.bfp_CM[k,k]**.5
     580					else:
     581						_epsilon_ = 1e-6
     582
     583					p1 = {_: self.bfp[_] for _ in self.bfp}
     584					p1[f'a{self.degrees[k]}'] += 0.001 * _epsilon_
     585					T_from_D47_p1 = _interp1d(self.model_fun(p1, xi), xi**-1 - 273.15)
     586
     587					p2 = {_: self.bfp[_] for _ in self.bfp}
     588					p2[f'a{self.degrees[k]}'] -= 0.001 * _epsilon_
     589					T_from_D47_p2 = _interp1d(self.model_fun(p2, xi), xi**-1 - 273.15)
     590
     591					J[:, k] = (T_from_D47_p1(_D47) - T_from_D47_p2(_D47)) / (0.002 * _epsilon_)
     592
     593			### Error propagation:
     594			CM_T = J @ CM @ J.T
     595
     596			if return_covar:
     597				return T, CM_T
     598			else:
     599				return T, float(_np.diag(CM_T)**.5) if T.ndim == 0 else _np.diag(CM_T)**.5
     600
     601
     602	def plot_T47_errors(
     603		self,
     604		calibname = None,
     605		rD47 = 0.010,
     606		Nr = [2,4,8,12,20],
     607		Tmin = 0,
     608		Tmax = 120,
     609		colors = [(1,0,0),(1,.5,0),(.25,.75,0),(0,.5,1),(0.5,0.5,0.5)],
     610		yscale = 'lin',
     611		):
     612		"""
     613		Plot SE of T reconstructed using the calibration as a function of T for various
     614		combinations of analytical precision and number of analytical replicates.
     615
     616		**Arguments**
     617
     618		+ **calibname**:
     619		Which calibration name to display. By default, use `label` attribute.
     620		+ **rD47**:
     621		Analytical precision of a single analysis.
     622		+ **Nr**:
     623		A list of lines to plot, each corresponding to a given number of replicates.
     624		+ **Tmin**:
     625		Minimum T to plot.
     626		+ **Tmax**:
     627		Maximum T to plot.
     628		+ **colors**:
     629		A list of colors to distinguish the plotted lines.
     630		+ **yscale**:
     631		  + If `'lin'`, the Y axis uses a linear scale.
     632		  + If `'log'`, the Y axis uses a logarithmic scale.
     633
     634		**Example**
     635
     636		````py
     637		from matplotlib import pyplot as ppl
     638		from D47calib import devils_laghetto_2023 as calib
     639
     640		fig = ppl.figure(figsize = (3.5,4))
     641		ppl.subplots_adjust(bottom = .2, left = .15)
     642		calib.plot_T47_errors(
     643			calibname = 'Devils Laghetto calibration',
     644			Nr = [1,2,4,16],
     645			Tmin  =0,
     646			Tmax = 40,
     647			)
     648		ppl.savefig('example_SE_T.png', dpi = 100)
     649		````
     650
     651		This should result in something like this:
     652
     653		<img src="example_SE_T.png">
     654		"""
     655
     656		if calibname is None:
     657			calibname = self.label
     658
     659		Nr = _np.array(Nr)
     660		if len(colors) < Nr.size:
     661			print('WARNING: Too few colors to plot different numbers of replicates; generating new colors.')
     662			from colorsys import hsv_to_rgb
     663			hsv = [(x*1.0/Nr.size, 1, .9) for x in range(Nr.size)]
     664			colors = [hsv_to_rgb(*x) for x in hsv]
     665
     666		Ti = _np.linspace(Tmin, Tmax)
     667		D47i, _  = self.T47(T = Ti)
     668		_, sT_calib = self.T47(D47 = D47i, error_from = 'calib')
     669
     670		ymax, ymin = 0, 1e6
     671		for N,c in zip(Nr, colors):
     672			_, sT = self.T47(D47 = D47i, sD47 = rD47 / N**.5, error_from = 'sD47')
     673			_ppl.plot(Ti, sT, '-', color = c, label=f'SE for {N} replicate{"s" if N > 1 else ""}')
     674			ymin = min(ymin, min(sT))
     675			ymax = max(ymax, max(sT))
     676
     677		_ppl.plot(Ti, sT_calib, 'k--', label='SE from calibration')
     678
     679		_ppl.legend(fontsize=9)
     680		_ppl.xlabel("T (°C)")
     681
     682		_ppl.ylabel("Standard error on reconstructed T (°C)")
     683
     684		# yticks([0,.5,1,1.5,2])
     685		_ppl.title(f"{calibname},\nassuming external Δ$_{{47}}$ repeatability of {rD47:.3f} ‰", size = 9)
     686		_ppl.grid( alpha = .25)
     687		if yscale == 'lin':
     688			_ppl.axis([Ti[0], Ti[-1], 0, ymax*1.05])
     689			t1, t2 = self.T.min(), self.T.max()
     690			_ppl.plot([t1, t2], [0, 0], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False)
     691			_ppl.text((t1+t2)/2, 0, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic')
     692			_ppl.axis([None, None, None, _ppl.axis()[-1]*1.25])
     693		elif yscale == 'log':
     694			ymin /= 2
     695			_ppl.axis([Ti[0], Ti[-1], ymin, ymax*1.05])
     696			_ppl.yscale('log')
     697			t1, t2 = self.T.min(), self.T.max()
     698			_ppl.plot([t1, t2], [ymin, ymin], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False)
     699			_ppl.text((t1+t2)/2, ymin, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic')
     700
     701	def export_data(self, csvfile, sep = ',', label = False, T_correl = False, D47_correl = False):
     702		"""
     703		Write calibration data to a csv file.
     704
     705		### Parameters
     706
     707		+ **csvfile**:
     708		The filename to write data to.
     709		+ **sep**:
     710		The separator between CSV fields.
     711		+ **label**:
     712		  + If specified as `True`, include a `Dataset` column with the calibration's `label` attribute.
     713		  + If specified as a `str`, include a `Dataset` column with that string.
     714		  + If specified as `False`, do not include a `Dataset` column.
     715		+ **T_correl**:
     716		  + If `True`, include correlations between all `T` values.
     717		+ **D47_correl**:
     718		  + If `True`, include correlations between all `D47` values.
     719
     720		### Example
     721
     722		````py
     723		D47calib.huyghe_2022.export_data(
     724			csvfile = 'example_export_data.csv',
     725			T_correl = True,
     726			D47_correl = True,
     727			)
     728		````
     729
     730		This should result in something like this ([link](example_export_data.csv)):
     731
     732		.. include:: ../../docs/example_export_data.md
     733
     734		"""
     735		n = len(str(self.N))
     736
     737		with open(csvfile, 'w') as f:
     738			f.write(sep.join(['ID', 'Sample', 'T', 'SE_T', 'D47', 'SE_D47']))
     739
     740			if label:
     741				f.write(f'{sep}Dataset')
     742
     743			if T_correl:
     744				inv_diag_sT = _np.diag(_np.diag(self.sT)**-.5)
     745				Tcorrel = inv_diag_sT @ self.sT @ inv_diag_sT
     746				f.write(sep.join(['']+[f'Tcorrel_{k+1:0{n}d}' for k in range(self.N)]))
     747
     748			if D47_correl:
     749				inv_diag_sD47 = _np.diag(_np.diag(self.sD47)**-.5)
     750				D47correl = inv_diag_sD47 @ self.sD47 @ inv_diag_sD47
     751				f.write(sep.join(['']+[f'D47correl_{k+1:0{n}d}' for k in range(self.N)]))
     752
     753			for k, (s, T, sT, D47, sD47) in enumerate(zip(
     754				self.samples,
     755				self.T,
     756				_np.diag(self.sT)**.5,
     757				self.D47,
     758				_np.diag(self.sD47)**.5,
     759				)):
     760				f.write('\n' + sep.join([f'{k+1:0{n}d}', s, f'{T:.2f}', f'{sT:.2f}', f'{D47:.4f}', f'{sD47:.4f}']))
     761				if label:
     762					if label is True:
     763						f.write(f'{sep}{self.label}')
     764					else:
     765						f.write(f'{sep}{label}')
     766				if T_correl:
     767					f.write(sep.join(['']+[
     768						f'{Tcorrel[k,_]:.0f}'
     769						if f'{Tcorrel[k,_]:.6f}'[-6:] == '000000'
     770						else f'{Tcorrel[k,_]:.6f}'
     771						for _ in range(self.N)]))
     772				if D47_correl:
     773					f.write(sep.join(['']+[
     774						f'{D47correl[k,_]:.0f}'
     775						if f'{D47correl[k,_]:.6f}'[-6:] == '000000'
     776						else f'{D47correl[k,_]:.6f}'
     777						for _ in range(self.N)]))
     778
     779
     780	def export(self, name, filename):
     781		"""
     782		Save `D47calib` object as an importable file.
     783
     784		### Parameters
     785
     786		+ **name**:
     787		The name of the variable to export.
     788		+ **filename**:
     789		The filename to write to.
     790
     791		### Example
     792
     793		````py
     794		D47calib.anderson_2021_lsce.export('foo', 'bar.py')
     795		````
     796
     797		This should result in a `bar.py` file with the following contents:
     798
     799		````py
     800		foo = D47calib(
     801			samples = ['LGB-2', 'DVH-2'],
     802			T = [7.9, 33.7],
     803			D47 = [0.6485720997671647, 0.5695972909966959],
     804			sT = [[0.04000000000000001, 0.0], [0.0, 0.04000000000000001]],
     805			sD47 = [[8.72797097773764e-06, 2.951894073404263e-06], [2.9518940734042614e-06, 7.498611746762038e-06]],
     806			description = 'Devils Hole & Laghetto Basso from Anderson et al. (2021), processed in I-CDES',
     807			label = 'Slow-growing calcites from Anderson et al. (2021)',
     808			color = (0, 0.5, 0),
     809			degrees = [0, 2],
     810			bfp = {'a0': 0.1583220210575451, 'a2': 38724.41371782721},
     811			bfp_CM = [[0.00035908667755871876, -30.707016431538836], [-30.70701643153884, 2668091.396598919]],
     812			chisq = 6.421311854486162e-27,
     813			Nf = 0,
     814			)
     815		````
     816		"""
     817		with open(filename, 'w') as f:
     818			f.write(f'''
     819{name} = D47calib(
     820	samples = {self.samples},
     821	T = {list(self.T)},
     822	D47 = {list(self.D47)},
     823	sT = {[list(l) for l in self.sT]},
     824	sD47 = {[list(l) for l in self.sD47]},
     825	degrees = {self.degrees},
     826	description = {repr(self.description)},
     827	name = {repr(self.name)},
     828	label = {repr(self.label)},
     829	bfp = {self.bfp},
     830	bfp_CM = {[list(l) for l in self.bfp_CM]},
     831	chisq = {self.chisq},
     832	cholesky_residuals = {list(self.cholesky_residuals)},
     833	aic = {self.aic},
     834	bic = {self.bic},
     835	ks_pvalue = {self.ks_pvalue},
     836	)
     837''')
     838
     839def combine_D47calibs(calibs, degrees = [0,2], same_T = []):
     840	'''
     841	Combine data from several `D47calib` instances.
     842
     843	### Parameters
     844
     845	+ **calibs**:
     846	A list of `D47calib` instances
     847	+ **degrees**:
     848	The polynomial degrees of the combined regression.
     849	+ **same_T**:
     850	Use this `list` to specify when samples from different calibrations are known/postulated
     851	to have formed at the same temperature (e.g. `DVH-2` and `DHC2-8` from the `fiebig_2021`
     852	and `anderson_2021_lsce` data sets). Each element of `same_T` is a `list` with the names
     853	of two or more samples formed at the same temperature.
     854
     855	For example, the `ogls_2023` calibration is computed with:
     856
     857	`same_T = [['DVH-2', DHC-2-8'], ['ETH-1-1100-SAM', 'ETH-1-1100']]`
     858
     859	Note that when samples from different calibrations have the same name,
     860	it is not necessary to explicitly list them in `same_T`.
     861
     862	Also note that the regression will fail if samples listed together in `same_T`
     863	actually have different `T` values specified in the original calibrations.
     864
     865	### Example
     866
     867	The `devils_laghetto_2023` calibration is computed using the following code:
     868
     869	````py
     870	K = [fiebig_2021.samples.index(_) for _ in ['LGB-2', 'DVH-2', 'DHC2-8']]
     871
     872	fiebig_temp = D47calib(
     873		samples = [fiebig_2021.samples[_] for _ in K],
     874		T = fiebig_2021.T[K],
     875		D47 = fiebig_2021.D47[K],
     876		sT = fiebig_2021.sT[K,:][:,K],
     877		sD47 = fiebig_2021.sD47[K,:][:,K],
     878		)
     879
     880	devils_laghetto_2023 = combine_D47calibs(
     881		calibs = [
     882			anderson_2021_lsce,
     883			fiebig_temp,
     884			],
     885		degrees = [0,2],
     886		same_T = [
     887			{'DVH-2', 'DHC2-8'},
     888			],
     889		)
     890	````
     891	'''
     892
     893	samples = [s for c in calibs for s in c.samples]
     894	T = [t for c in calibs for t in c.T]
     895	D47 = [x for c in calibs for x in c.D47]
     896	sD47 = _block_diag(*[c.sD47 for c in calibs])
     897	sT = _block_diag(*[c.sT for c in calibs])
     898
     899	for i in range(len(samples)):
     900		for j in range(len(samples)):
     901			if i != j:
     902				if (samples[i] == samples[j] or
     903					any([samples[i] in _ and samples[j] in _ for _ in same_T])):
     904
     905					sT[i,j] = (sT[i,i] * sT[j,j])**.5
     906
     907	calib = D47calib(
     908		samples = samples,
     909		T = T,
     910		D47 = D47,
     911		sT = sT,
     912		sD47 = sD47,
     913		degrees = degrees,
     914		)
     915
     916	return calib
     917
     918from ._calibs import *
     919
     920def _covar2correl(C):
     921	SE = _np.diag(C)**.5
     922	return SE, _np.diag(SE**-1) @ C @ _np.diag(SE**-1)
     923
     924try:
     925	app = typer.Typer(
     926		add_completion = False,
     927		context_settings={'help_option_names': ['-h', '--help']},
     928		rich_markup_mode = 'rich',
     929		)
     930
     931	@app.command()
     932	def _cli(
     933		input: Annotated[str, typer.Argument(help = "Specify either the path of an input file or just '-' to read input from stdin")] = '-',
     934		include_samples: Annotated[str, typer.Option('--include-samples', '-u', help = 'Only include samples listed in this file')] = 'all',
     935		exclude_samples: Annotated[str, typer.Option('--exclude-samples', '-x', help = 'Exclude samples listed in this file')] = 'none',
     936		outfile: Annotated[str, typer.Option('--output-file', '-o', help = 'Write output to this file instead of printing to stdout')] = 'none',
     937		calib: Annotated[str, typer.Option('--calib', '-c', help = 'D47 calibration function to use')] = 'ogls_2023',
     938		delim_in: Annotated[str, typer.Option('--delimiter-in', '-i', help = "Delimiter used in the input.")] = ',',
     939		delim_out: Annotated[str, typer.Option('--delimiter-out', '-j', help = "Delimiter used in the output. Use '>' or '<' for elastic white space with right- or left-justified cells.")] = "',' when writing to output file, '>' when printing to stdout",
     940		T_precision: Annotated[int, typer.Option('--T-precision', '-p', help = 'Precision for T output')] = 2,
     941		D47_precision: Annotated[int, typer.Option('--D47-precision', '-q', help = 'Precision for D47 output')] = 4,
     942		correl_precision: Annotated[int, typer.Option('--correl-precision', '-r', help = 'Precision for correlation output')] = 3,
     943		covar_precision: Annotated[int, typer.Option('--covar-precision', '-s', help = 'Precision for covariance output')] = 3,
     944		return_covar: Annotated[bool, typer.Option('--return-covar', '-v', help = 'Output covariance matrix instead of correlation matrix')] = False,
     945		ignore_correl: Annotated[bool, typer.Option('--ignore-correl', '-g', help = 'Only consider and report standard errors without correlations')] = False,
     946		):
     947		"""
     948[b]Purpose:[/b]
     949
     950Reads data from an input file, converts between T and D47 values, and print out the results.
     951
     952The input file is a CSV, or any similar file with data sorted into lines and columns. The line separator must be a <newline>. The column separator, noted <sep> hereafter, is "," by default, or may be any other single character such as ";" or <tab>.
     953
     954The first line of the input file must be one of the following:
     955
     956- [b]Option 1:[/b] T
     957- [b]Option 2:[/b] T<sep>T_SE
     958- [b]Option 3:[/b] T<sep>T_SE<sep>T_correl
     959- [b]Option 4:[/b] T<sep>T_covar
     960- [b]Option 5:[/b] D47
     961- [b]Option 6:[/b] D47<sep>D47_SE
     962- [b]Option 7:[/b] D47<sep>D47_SE<sep>D47_correl
     963- [b]Option 8:[/b] D47<sep>D47_covar
     964
     965The rest of the input must be any number of lines with float values corresponding to the fields in the first line, separated by <sep>.
     966
     967[bold]Example input file:[/bold]
     968
     969[italic]D47     D47_SE  D47_correl[/italic]
     970[italic]0.6324  0.0101  1.00  0.25  0.25[/italic]
     971[italic]0.6281  0.0087  0.25  1.00  0.25[/italic]
     972[italic]0.6385  0.0095  0.25  0.25  1.00[/italic]
     973
     974The corresponding D47 (options 1-4) or T (options 4-8) values are computed, along with standard errors and error correlations from calibration uncertainties.
     975
     976For options 2-4 and 5-8, which specify standard errors or covariances for the input values, the standard errors and error correlations resulting from these input uncertainties are also computed, as well as the combined standard errors accounting for both calibration and input uncertainties.
     977
     978The example above will thus result in an output with the following fields:
     979
     980[italic]- D47[/italic]
     981[italic]- D47_SE[/italic]
     982[italic]- D47_correl[/italic]
     983[italic]- T[/italic]
     984[italic]- T_SE_from_calib[/italic]
     985[italic]- T_correl_from_calib[/italic]
     986[italic]- T_SE_from_input[/italic]
     987[italic]- T_correl_from_input[/italic]
     988[italic]- T_SE_from_both[/italic]
     989[italic]- T_correl_from_both[/italic]
     990
     991Results may also be saved to a file using [bold]--output-file <filename>[/bold] or [bold]-o <filename>[/bold].
     992
     993To filter the samples (lines) to process using [b]--exclude-samples[/b] and [b]--include-samples[/b], first add a [b]Sample[/b] column to the input data, assign a sample name to each line.
     994Then to exclude some samples, provide the [b]--exclude-samples[/b] option with the name of a file where each line is one sample to exclude.
     995To exclude all samples except those listed in a file, provide the [b]--include-samples[/b] option with the name of that file, where each line is one sample to include.
     996"""
     997
     998		### INCOMPATIBILITY BETWEEN --ignore-correl AND --return-covar
     999		if ignore_correl:
    1000			return_covar = False
    1001
    1002		### USE WHITESPACE AS INPUT DELIMITER
    1003		if delim_in == ' ':
    1004			delim_in = None
    1005
    1006		### SMART SELECTION OF OUTPUT DELIMITER
    1007		if delim_out == "',' when writing to output file, '>' when printing to stdout":
    1008			if outfile == 'none':
    1009				delim_out = '>'
    1010			else:
    1011				delim_out = ','
    1012
    1013		### CALIBRATION
    1014		if calib in globals() and type(globals()[calib]) == D47calib:
    1015			calib = globals()[calib]
    1016		else:
    1017			with open(calib) as f:
    1018				calibdata = _np.array([[c.strip() for c in l.strip().split(delim_in)] for l in f.readlines()[1:]], dtype = float)
    1019
    1020				degrees = [int(d) for d in calibdata[:,0]]
    1021				bfp = {f'a{k}': a for k,a in zip(degrees, calibdata[:,1])}
    1022				bfp_CM = calibdata[:,2:]
    1023				if bfp_CM.shape[0] != bfp_CM.shape[1]:
    1024					bfp_CM = _np.zeros((len(degrees), len(degrees)))
    1025				calib = D47calib(
    1026					samples = [], T = [], sT = [], D47 = [], sD47 = [],
    1027					degrees = degrees, bfp = bfp, bfp_CM = bfp_CM,
    1028					)
    1029
    1030		### READ INPUT STRINGS
    1031		if input == '-':
    1032			data = [[c.strip() for c in l.strip().split(delim_in)] for l in sys.stdin]
    1033		else:
    1034			with open(input) as f:
    1035				data = [[c.strip() for c in l.strip().split(delim_in)] for l in f.readlines()]
    1036
    1037		if include_samples == 'all':
    1038			samples_to_include = []
    1039		else:
    1040			with open(include_samples) as f:
    1041				samples_to_include = [l.strip() for l in f.readlines()]
    1042
    1043		if exclude_samples == 'none':
    1044			samples_to_exclude = []
    1045		else:
    1046			with open(exclude_samples) as f:
    1047				samples_to_exclude = [l.strip() for l in f.readlines()]
    1048
    1049		if len(samples_to_include) > 0 or len(samples_to_exclude) > 0:
    1050			try:
    1051				k = data[0].index('Sample')
    1052			except ValueError:
    1053				raise KeyError("When using options --include-samples or --exclude-samples, the input file must have a column labeled 'Sample'.")
    1054
    1055			if len(samples_to_include) > 0:
    1056				data = [data[0]] + [l for l in data[1:] if l[k] in samples_to_include]
    1057			data = [data[0]] + [l for l in data[1:] if l[k] not in samples_to_exclude]
    1058
    1059		### FIND FIRST DATA COLUMN
    1060		k = 0
    1061		while data[0][k] not in ['T', 'D47']:
    1062			k += 1
    1063			if k == len(data[0]):
    1064				raise KeyError("None of the input column headers are 'T' or 'D47'.")
    1065		data_out = [l[:k] for l in data]
    1066		data = [l[k:] for l in data]
    1067
    1068		### READ INPUT FIELDS
    1069		fields = data[0]
    1070
    1071		### CHECK FOR UNSUPPORTED FIELD COMBINATIONS
    1072		if fields not in [
    1073			['T'],
    1074			['T', 'T_SE'],
    1075			['T', 'T_covar'],
    1076			['T', 'T_SE', 'T_correl'],
    1077			['D47'],
    1078			['D47', 'D47_SE'],
    1079			['D47', 'D47_covar'],
    1080			['D47', 'D47_SE', 'D47_correl'],
    1081			]:
    1082			raise KeyError("There is a problem with the combination of field names provided as input.")
    1083
    1084		### BOOK-KEEPING
    1085		infield = fields[0]
    1086		X_precision = {'T': T_precision, 'D47': D47_precision}[infield]
    1087		outfield = {'T': 'D47', 'D47': 'T'}[infield]
    1088		Y_precision = {'T': T_precision, 'D47': D47_precision}[outfield]
    1089		N = len(data)-1
    1090
    1091		### READ INPUT DATA, ALSO SAVING ITS ORIGINAL STRINGS
    1092		X_str = [l[0] for l in data[1:]]
    1093		X = _np.array(X_str, dtype = float)
    1094
    1095		if len(fields) == 1:
    1096			X_SE = X*0
    1097			X_correl = _np.eye(N)
    1098			X_covar = _np.zeros((N, N))
    1099			X_SE_str = [f'{c:.{X_precision}f}' for c in X_SE]
    1100			X_correl_str = [[f'{c:.{correl_precision}f}' for c in l] for l in X_correl]
    1101			X_covar_str = [[f'{c:.{covar_precision}e}' for c in l] for l in X_covar]
    1102		if len(fields) == 2:
    1103			if fields[1].endswith('_SE'):
    1104				X_SE_str = [l[1] for l in data[1:]]
    1105				X_SE = _np.array(X_SE_str, dtype = float)
    1106				X_covar = _np.diag(X_SE**2)
    1107				X_covar_str = [[f'{c:.{covar_precision}e}' for c in l] for l in X_covar]
    1108			elif fields[1].endswith('_covar'):
    1109				X_covar_str = [l[1:N+1] for l in data[1:]]
    1110				X_covar = _np.array(X_covar_str, dtype = float)
    1111				X_SE = _np.diag(X_covar)**.5
    1112				X_SE_str = [f'{c:.{X_precision}f}' for c in X_SE]
    1113			X_correl = _np.diag(X_SE**-1) @ X_covar @ _np.diag(X_SE**-1)
    1114			X_correl_str = [[f'{c:.{correl_precision}f}' for c in l] for l in X_correl]
    1115		elif len(fields) == 3:
    1116			X_SE_str = [l[1] for l in data[1:]]
    1117			X_SE = _np.array(X_SE_str, dtype = float)
    1118			X_correl_str = [l[2:N+2] for l in data[1:]]
    1119			X_correl = _np.array(X_correl_str, dtype = float)
    1120			X_covar = _np.diag(X_SE) @ X_correl @ _np.diag(X_SE)
    1121			X_covar_str = [[f'{c:.{covar_precision}e}' for c in l] for l in X_covar]
    1122
    1123		### COMPUTE OUTPUT VALUES AND COVAR
    1124		kwargs = {infield: X, f's{infield}': X_covar}
    1125		Y, Y_covar_from_calib = calib.T47(**kwargs, error_from = 'calib', return_covar = True)
    1126		Y, Y_covar_from_input = calib.T47(**kwargs, error_from = f's{infield}', return_covar = True)
    1127		Y, Y_covar_from_both = calib.T47(**kwargs, error_from = 'both', return_covar = True)
    1128
    1129		Y_SE_from_calib = _np.diag(Y_covar_from_calib)**.5
    1130		Y_SE_from_input = _np.diag(Y_covar_from_input)**.5
    1131		Y_SE_from_both = _np.diag(Y_covar_from_both)**.5
    1132
    1133		if (Y_SE_from_calib**2).min():
    1134			Y_correl_from_calib = _np.diag(Y_SE_from_calib**-1) @ Y_covar_from_calib @ _np.diag(Y_SE_from_calib**-1)
    1135		else:
    1136			Y_correl_from_calib = _np.eye(N)
    1137
    1138		if (Y_SE_from_input**2).min():
    1139			Y_correl_from_input = _np.diag(Y_SE_from_input**-1) @ Y_covar_from_input @ _np.diag(Y_SE_from_input**-1)
    1140		else:
    1141			Y_correl_from_input = _np.eye(N)
    1142
    1143		if (Y_SE_from_both**2).min():
    1144			Y_correl_from_both = _np.diag(Y_SE_from_both**-1) @ Y_covar_from_both @ _np.diag(Y_SE_from_both**-1)
    1145		else:
    1146			Y_correl_from_both = _np.eye(N)
    1147
    1148		### BUILD Y STRINGS
    1149		Y_str = [f'{y:.{Y_precision}f}' for y in Y]
    1150
    1151		Y_SE_from_calib_str = [f'{sy:.{Y_precision}f}' for sy in Y_SE_from_calib]
    1152		Y_SE_from_input_str = [f'{sy:.{Y_precision}f}' for sy in Y_SE_from_input]
    1153		Y_SE_from_both_str = [f'{sy:.{Y_precision}f}' for sy in Y_SE_from_both]
    1154
    1155		Y_covar_from_calib_str = [[f'{c:.{covar_precision}e}' for c in l] for l in Y_covar_from_calib]
    1156		Y_covar_from_input_str = [[f'{c:.{covar_precision}e}' for c in l] for l in Y_covar_from_input]
    1157		Y_covar_from_both_str = [[f'{c:.{covar_precision}e}' for c in l] for l in Y_covar_from_both]
    1158
    1159		Y_correl_from_calib_str = [[f'{c:.{correl_precision}f}' for c in l] for l in Y_correl_from_calib]
    1160		Y_correl_from_input_str = [[f'{c:.{correl_precision}f}' for c in l] for l in Y_correl_from_input]
    1161		Y_correl_from_both_str = [[f'{c:.{correl_precision}f}' for c in l] for l in Y_correl_from_both]
    1162
    1163		### ADD SE COLUMN TO INPUT
    1164		if f'{infield}_covar' in fields:
    1165			fields.insert(1, f'{infield}_SE')
    1166
    1167		### ADD X COLUMNS TO OUTPUT DATA
    1168		data_out[0] += [infield]
    1169		for k in range(N):
    1170			data_out[k+1] += [X_str[k]]
    1171		for f in fields[1:]:
    1172			if f.endswith('_SE'):
    1173				data_out[0] += [f]
    1174				for k in range(N):
    1175					data_out[k+1] += [X_SE_str[k]]
    1176			if f.endswith('_covar') or f.endswith('_correl'):
    1177				if not ignore_correl:
    1178					data_out[0] += [f] + ['' for _ in range(N-1)]
    1179					for k in range(N):
    1180						data_out[k+1] += (X_covar_str if f.endswith('_covar') else X_correl_str)[k][:]
    1181
    1182		### ADD Y COLUMNS TO OUTPUT DATA
    1183		data_out[0] += [outfield]
    1184		for k in range(N):
    1185			data_out[k+1] += [Y_str[k]]
    1186
    1187		data_out[0] += [f'{outfield}_SE_from_calib']
    1188		for k in range(N):
    1189			data_out[k+1] += [Y_SE_from_calib_str[k]]
    1190		if not ignore_correl:
    1191			if return_covar:
    1192				data_out[0] += [f'{outfield}_covar_from_calib'] + ['' for _ in range(N-1)]
    1193				for k in range(N):
    1194					data_out[k+1] += Y_covar_from_calib_str[k]
    1195			else:
    1196				data_out[0] += [f'{outfield}_correl_from_calib'] + ['' for _ in range(N-1)]
    1197				for k in range(N):
    1198					data_out[k+1] += Y_correl_from_calib_str[k]
    1199
    1200		data_out[0] += [f'{outfield}_SE_from_input']
    1201		for k in range(N):
    1202			data_out[k+1] += [Y_SE_from_input_str[k]]
    1203		if not ignore_correl:
    1204			if return_covar:
    1205				data_out[0] += [f'{outfield}_covar_from_input'] + ['' for _ in range(N-1)]
    1206				for k in range(N):
    1207					data_out[k+1] += Y_covar_from_input_str[k]
    1208			else:
    1209				data_out[0] += [f'{outfield}_correl_from_input'] + ['' for _ in range(N-1)]
    1210				for k in range(N):
    1211					data_out[k+1] += Y_correl_from_input_str[k]
    1212
    1213		data_out[0] += [f'{outfield}_SE_from_both']
    1214		for k in range(N):
    1215			data_out[k+1] += [Y_SE_from_both_str[k]]
    1216		if not ignore_correl:
    1217			if return_covar:
    1218				data_out[0] += [f'{outfield}_covar_from_both'] + ['' for _ in range(N-1)]
    1219				for k in range(N):
    1220					data_out[k+1] += Y_covar_from_both_str[k]
    1221			else:
    1222				data_out[0] += [f'{outfield}_correl_from_both'] + ['' for _ in range(N-1)]
    1223				for k in range(N):
    1224					data_out[k+1] += Y_correl_from_both_str[k]
    1225
    1226
    1227		### PRINT OUTPUT TO STDOUT OR SAVE IT TO FILE
    1228		if delim_out in '<>':
    1229			lengths = [max([len(data_out[j][k]) for j in range(len(data_out))]) for k in range(len(data_out[0]))]
    1230
    1231			txt = ''
    1232			for l in data_out:
    1233				for k in range(len(l)):
    1234					if k > 0:
    1235						txt += '  '
    1236					txt += f'{l[k]:{delim_out}{lengths[k]}s}'
    1237				txt += '\n'
    1238
    1239			txt = txt[:-1]
    1240
    1241		else:
    1242			txt = '\n'.join([delim_out.join(l) for l in data_out])
    1243
    1244		if outfile == 'none':
    1245			print(txt)
    1246		else:
    1247			with open(outfile, 'w') as f:
    1248				f.write(txt)
    1249
    1250	def __cli():
    1251		app()
    1252
    1253except NameError:
    1254	pass

class D47calib(ogls.InverseTPolynomial): View Source

     39class D47calib(_ogls.InverseTPolynomial):
     40	"""
     41	Δ47 calibration class based on OGLS regression
     42	of Δ47 as a polynomial function of inverse T.
     43	"""
     44
     45	def __init__(self,
     46		samples, T, D47,
     47		sT = None,
     48		sD47 = None,
     49		degrees = [0,2],
     50		xpower = 2,
     51		name = '',
     52		label = '',
     53		description = '',
     54		**kwargs,
     55		):
     56		"""
     57		### Parameters
     58
     59		+ **samples**: a list of N sample names.
     60		+ **T**: a 1-D array (or array-like) of temperatures values (in degrees C), of size N.
     61		+ **D47**: a 1-D array (or array-like) of Δ47 values (in permil), of size N.
     62		+ **sT**: uncertainties on `T`. If specified as:
     63		  + a scalar: `sT` is treated as the standard error applicable to all `T` values;
     64		  + a 1-D array-like of size N: `sT` is treated as the standard errors of `T`;
     65		  + a 2-D array-like of size (N, N): `sT` is treated as the (co)variance matrix of `T`.
     66		+ **sD47**: uncertainties on `D47`. If specified as:
     67		  + a scalar: `sD47` is treated as the standard error applicable to all `D47` values;
     68		  + a 1-D array-like of size N: `sD47` is treated as the standard errors of `D47`;
     69		  + a 2-D array-like of size (N, N): `sD47` is treated as the (co)variance matrix of `D47`.
     70		+ **degrees**: degrees of the polynomial regression, e.g., `[0, 2]` or `[0, 1, 2, 3, 4]`.
     71		+ **name**: a human-readable, short name assigned to the calibration.
     72		+ **label**: a short description of the calibration, e.g., to be used in legends.
     73		+ **description**: a longer description, including relevant references/DOIs.
     74		This is not necessary when `bfp` and `CM_bfp` are specified at instantiation time.
     75		+ **kwargs**: keyword arguments passed to the underlying `ogls.InverseTPolynomial()` call.
     76
     77		### Notable attributes
     78
     79		+ **N**:
     80		The total number of observations (samples) in the calibration data.
     81		+ **samples**:
     82		The list sample names.
     83		+ **T**:
     84		1-D `ndarray` of temperatures in degrees C.
     85		+ **D47**:
     86		1-D `ndarray` of Δ47 values in permil.
     87		+ **sT**:
     88		2-D `ndarray` equal to the full (co)variance matrix for `T`.
     89		+ **D47**:
     90		2-D `ndarray` equal to the full (co)variance matrix for `D47`.
     91		+ **xpower**:
     92		By default, all `D47calib` graphical methods plot Δ47 as a function of 1/T<sup>2</sup>.
     93		It is possible to change this behavior to use a different power of 1/T.
     94		This is done by redefining the `xpower` attribute to a different, non-zero `int` value
     95		(e.g. `foo.xpower = 1` to plot as a function of 1/T instead of 1/T<sup>2</sup>).
     96		+ **bfp**:
     97		The best-fit parameters of the regression.
     98		This is a `dict` with keys equal to the polynomial coefficients (see `bff` definition below)
     99		+ **bff()**:
    100		The best-fit polynomial function of inverse T, defined as:
    101		`bff(x) = sum(bfp[f'a{k}'] * x**k for k in degrees)`
    102		Note that `bff` takes `x = 1/(T+273.15)` (instead of `T`) as input.
    103
    104
    105		### Examples
    106
    107		A very simple example:
    108
    109		````py
    110		.. include:: ../../code_examples/D47calib_init/example.py
    111		````
    112
    113		Should yield:
    114
    115		````
    116		.. include:: ../../code_examples/D47calib_init/output.txt
    117		````
    118
    119		"""
    120
    121		self.samples = samples[:]
    122		self.name = name
    123		self.label = label
    124		self.description = description
    125		self.D47 = _np.asarray(D47, dtype = 'float')
    126		self.N = self.D47.size
    127
    128		if sD47 is None:
    129			self.sD47 = _np.zeros((self.N, self.N))
    130		else:
    131			self.sD47 = _np.asarray(sD47)
    132			if len(self.sD47.shape) == 1:
    133				self.sD47 = _np.diag(self.sD47**2)
    134			elif len(self.sD47.shape) == 0:
    135				self.sD47 = _np.eye(self.D47.size) * self.sD47**2
    136
    137		_ogls.InverseTPolynomial.__init__(self, T=T, Y=D47, sT=sT, sY=sD47, degrees = degrees, xpower = xpower, **kwargs)
    138
    139		if self.bfp is None:
    140			self.regress()
    141
    142		self._bff_deriv = lambda x: _np.array([k * self.bfp[f'a{k}'] * x**(k-1) for k in degrees if k > 0]).sum(axis = 0)
    143
    144		xi = _np.linspace(0,200**-1,1001)
    145		self._inv_bff = _interp1d(self.bff(xi), xi)
    146
    147		self._D47_from_T = lambda T: self.bff((T+273.15)**-1)
    148		self._T_from_D47 = lambda D47: self._inv_bff(D47)**-1 - 273.15
    149		self._D47_from_T_deriv = lambda T: -(T+273.15)**-2 * self._bff_deriv((T+273.15)**-1)
    150		self._T_from_D47_deriv = lambda D47: self._D47_from_T_deriv(self._T_from_D47(D47))**-1
    151
    152	def __repr__(self):
    153		return f'<D47calib: {self.name}>'
    154
    155	def invT_xaxis(self,
    156		xlabel = None,
    157		Ti = [0,20,50,100,250,1000],
    158		):
    159		"""
    160		Create and return an `Axes` object with X values equal to 1/T<sup>2</sup>,
    161		but labeled in degrees Celsius.
    162
    163		### Parameters
    164
    165		+ **xlabel**:
    166		Custom label for X axis (`r'$1\,/\,T^2$'` by default)
    167		+ **Ti**:
    168		Specify tick locations for X axis, in degrees C.
    169
    170		### Returns
    171
    172		+ an `matplotlib.axes.Axes` instance
    173
    174		### Examples
    175
    176		````py
    177		.. include:: ../../code_examples/D47calib_invT_xaxis/example_1.py
    178		````
    179
    180		This should result in something like this:
    181
    182		<img align="center" src="example_invT_xaxis_1.png">
    183
    184		It is also possible to define the X axis using a different power of 1/T
    185		by first redefining the `xpower` attribute:
    186
    187		````py
    188		.. include:: ../../code_examples/D47calib_invT_xaxis/example_2.py
    189		````
    190
    191		This should result in something like this:
    192
    193		<img align="center" src="example_invT_xaxis_2.png">
    194		"""
    195		if xlabel is None:
    196			xlabel = f'$1\\,/\\,T^{self.xpower}$' if self.xpower > 1 else '1/T'
    197		_ppl.xlabel(xlabel)
    198		_ppl.xticks([(273.15 + t) ** -self.xpower for t in sorted(Ti)[::-1]])
    199		ax = _ppl.gca()
    200		ax.set_xticklabels([f"${t}\\,$°C" for t in sorted(Ti)[::-1]])
    201		ax.tick_params(which="major")
    202
    203		return ax
    204
    205
    206	def plot_data(self, label = False, **kwargs):
    207		"""
    208		Plot Δ47 value of each sample as a function of 1/T<sup>2</sup>.
    209
    210		### Parameters
    211
    212		+ **label**:
    213		  + If `label` is a string, use this string as `label` for the underlyig
    214		  `matplotlib.pyplot.plot()` call.
    215		  + If `label = True`, use the caller's `label` attribute instead.
    216		  + If `label = False`, no label is specified (default behavior).
    217		+ **kwargs**:
    218		keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call.
    219
    220		### Returns
    221
    222		+ the return value(s) of the underlying `matplotlib.pyplot.plot()` call.
    223
    224		### Example
    225
    226		````py
    227		from matplotlib import pyplot as ppl
    228		from D47calib import huyghe_2022 as calib
    229
    230		fig = ppl.figure(figsize = (5,3))
    231		ppl.subplots_adjust(bottom = .25, left = .15)
    232		calib.invT_xaxis(Ti = [0,10,25])
    233		calib.plot_data(label = True)
    234		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    235		ppl.legend()
    236		ppl.savefig('example_plot_data.png', dpi = 100)
    237		`````
    238
    239		This should result in something like this:
    240
    241		<img align="center" src="example_plot_data.png">
    242		"""
    243# 		if 'mec' not in kwargs:
    244# 			kwargs['mec'] = self.color
    245		if label is not False:
    246			kwargs['label'] = self.label if label is True else label
    247		return _ogls.InverseTPolynomial.plot_data(self, **kwargs)
    248
    249
    250	def plot_error_bars(self, **kwargs):
    251		"""
    252		Plot Δ47 error bars (±1.96 SE) of each sample as a function of 1/T<sup>2</sup>.
    253
    254		### Parameters
    255
    256		+ **kwargs**:
    257		keyword arguments passed to the underlying `matplotlib.pyplot.errrobar()` call.
    258
    259		### Returns
    260
    261		+ the return value(s) of the underlying `matplotlib.pyplot.errorbar()` call.
    262
    263		### Example
    264
    265		````py
    266		from matplotlib import pyplot as ppl
    267		from D47calib import huyghe_2022 as calib
    268
    269		fig = ppl.figure(figsize = (5,3))
    270		ppl.subplots_adjust(bottom = .25, left = .15)
    271		calib.invT_xaxis(Ti = [0,10,25])
    272		calib.plot_error_bars(alpha = .4)
    273		calib.plot_data(label = True)
    274		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    275		ppl.legend()
    276		ppl.savefig('example_plot_error_bars.png', dpi = 100)
    277		`````
    278
    279		This should result in something like this:
    280
    281		<img align="center" src="example_plot_error_bars.png">
    282		"""
    283# 		if 'ecolor' not in kwargs:
    284# 			kwargs['ecolor'] = self.color
    285		return _ogls.InverseTPolynomial.plot_error_bars(self, **kwargs)
    286
    287
    288	def plot_error_ellipses(self, **kwargs):
    289		"""
    290		Plot Δ47 error ellipses (95 % confidence) of each sample as a function of 1/T<sup>2</sup>.
    291
    292		### Parameters
    293
    294		+ **kwargs**:
    295		keyword arguments passed to the underlying `matplotlib.patches.Ellipse()` call.
    296
    297		### Returns
    298
    299		+ the return value(s) of the underlying `matplotlib.patches.Ellipse()` call.
    300
    301		### Example
    302
    303		````py
    304		from matplotlib import pyplot as ppl
    305		from D47calib import huyghe_2022 as calib
    306
    307		fig = ppl.figure(figsize = (5,3))
    308		ppl.subplots_adjust(bottom = .25, left = .15)
    309		calib.invT_xaxis(Ti = [0,10,25])
    310		calib.plot_error_ellipses(alpha = .4)
    311		calib.plot_data(label = True)
    312		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    313		ppl.legend()
    314		ppl.savefig('example_plot_error_ellipses.png', dpi = 100)
    315		`````
    316
    317		This should result in something like this:
    318
    319		<img align="center" src="example_plot_error_ellipses.png">
    320		"""
    321# 		if 'ec' not in kwargs:
    322# 			kwargs['ec'] = self.color
    323		return _ogls.InverseTPolynomial.plot_error_ellipses(self, **kwargs)
    324
    325
    326	def plot_bff(self, label = False, **kwargs):
    327		"""
    328		Plot best-fit regression of Δ47 as a function of 1/T<sup>2</sup>.
    329
    330		### Parameters
    331
    332		+ **label**:
    333		  + If `label` is a string, use this string as `label` for the underlyig
    334		  `matplotlib.pyplot.plot()` call.
    335		  + If `label = True`, use the caller's `label` attribute instead.
    336		  + If `label = False`, no label is specified (default behavior).
    337		+ **kwargs**:
    338		keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call.
    339
    340		### Returns
    341
    342		+ the return value(s) of the underlying `matplotlib.pyplot.plot()` call.
    343
    344		### Example
    345
    346		````py
    347		from matplotlib import pyplot as ppl
    348		from D47calib import huyghe_2022 as calib
    349
    350		fig = ppl.figure(figsize = (5,3))
    351		ppl.subplots_adjust(bottom = .25, left = .15)
    352		calib.invT_xaxis(Ti = [0,10,25])
    353		calib.plot_bff(label = True, dashes = (8,2,2,2))
    354		calib.plot_data()
    355		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    356		ppl.legend()
    357		ppl.savefig('example_plot_bff.png', dpi = 100)
    358		`````
    359
    360		This should result in something like this:
    361
    362		<img align="center" src="example_plot_bff.png">
    363		"""
    364# 		if 'color' not in kwargs:
    365# 			kwargs['color'] = self.color
    366		if label is not False:
    367			kwargs['label'] = self.label if label is True else label
    368		return _ogls.InverseTPolynomial.plot_bff(self, **kwargs)
    369
    370
    371	def plot_bff_ci(self, **kwargs):
    372		"""
    373		Plot 95 % confidence region for best-fit regression of Δ47 as a function of 1/T<sup>2</sup>.
    374
    375		### Parameters
    376
    377		+ **label**:
    378		+ **kwargs**:
    379		keyword arguments passed to the underlying `matplotlib.pyplot.fill_between()` call.
    380
    381		### Returns
    382
    383		+ the return value(s) of the underlying `matplotlib.pyplot.fill_between()` call.
    384
    385		### Example
    386
    387		````py
    388		from matplotlib import pyplot as ppl
    389		from D47calib import huyghe_2022 as calib
    390
    391		fig = ppl.figure(figsize = (5,3))
    392		ppl.subplots_adjust(bottom = .25, left = .15)
    393		calib.invT_xaxis(Ti = [0,10,25])
    394		calib.plot_bff_ci(alpha = .15)
    395		calib.plot_bff(label = True, dashes = (8,2,2,2))
    396		calib.plot_data()
    397		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    398		ppl.legend()
    399		ppl.savefig('example_plot_bff_ci.png', dpi = 100)
    400		`````
    401
    402		This should result in something like this:
    403
    404		<img align="center" src="example_plot_bff_ci.png">
    405		"""
    406# 		if 'color' not in kwargs:
    407# 			kwargs['color'] = self.color
    408		return _ogls.InverseTPolynomial.plot_bff_ci(self, **kwargs)
    409
    410	def T47(self,
    411		D47 = None,
    412		sD47 = None,
    413		T=None,
    414		sT = None,
    415		error_from = 'both',
    416		return_covar = False,
    417		):
    418		'''
    419		When `D47` is input, computes corresponding T value(s).
    420		`D47` input may be specified as a scalar, or as a 1-D array.
    421		`T` output will then have the same type and size as `D47`.
    422
    423		When `T` is input, computes corresponding Δ47 value(s).
    424		`T` input may be specified as a scalar, or as a 1-D array.
    425		`D47` output will then have the same type and size as `T`.
    426
    427		Only one of either `D47` or `T` may be specified as input.
    428
    429		**Arguments:**
    430
    431		* `D47`: Δ47 value(s) to convert into temperature (`float` or 1-D array)
    432		* `sD47`: Δ47 uncertainties, which may be:
    433		  - `None` (default)
    434		  - `float` or `int` (uniform standard error on `D47`)
    435		  - 1-D array (standard errors on `D47`)
    436		  - 2-D array (covariance matrix for `D47`)
    437		* `T`: T value(s) to convert into Δ47 (`float` or 1-D array), in degrees C
    438		* `sT`: T uncertainties, which may be:
    439		  - `None` (default)
    440		  - `float` or `int` (uniform standard error on `T`)
    441		  - 1-D array (standard errors on `T`)
    442		  - 2-D array (variance-covariance matrix for `T`)
    443		* `error_from`: if set to `'both'` (default), returned errors take into account
    444		  input uncertainties (`sT` or `sD47`) as well as calibration uncertainties;
    445		  if set to `'calib'`, only calibration uncertainties are accounted for;
    446		  if set to `'sT'` or `'sD47'`, calibration uncertainties are ignored.
    447		* `return_covar`: (False by default) whether to return the full covariance matrix
    448		  for returned `T` or `D47` values, otherwise return standard errors for the returned
    449		  `T` or `D47` values instead.
    450
    451		**Returns (with `D47` input):**
    452
    453		* `T`: temperature value(s) computed from `D47`
    454		* `sT`: uncertainties on `T` value(s), whether as standard error(s) or covariance matrix
    455
    456		**Returns (with `T` input):**
    457
    458		* `D47`: Δ47 value(s) computed from `D47`
    459		* `sD47`: uncertainties on `D47` value(s), whether as standard error(s) or covariance matrix
    460
    461		### Example
    462
    463		````py
    464		import numpy as np
    465		from matplotlib import pyplot as ppl
    466		from D47calib import ogls_2023 as calib
    467
    468		X = np.linspace(1473**-2, 270**-2)
    469		D47, sD47 = calib.T47(T = X**-0.5 - 273.15)
    470
    471		fig = ppl.figure(figsize = (5,3))
    472		ppl.subplots_adjust(bottom = .25, left = .15)
    473		calib.invT_xaxis()
    474		ppl.plot(X, 1000 * sD47, 'r-')
    475		ppl.ylabel('Calibration SE on $Δ_{47}$ values (ppm)')
    476		ppl.savefig('example_SE47.png', dpi = 100)
    477		`````
    478
    479		This should result in something like this:
    480
    481		<img src="example_SE47.png">
    482		'''
    483
    484		if D47 is None and T is None:
    485			raise ValueError('Either D47 or T must be specified, but both are undefined.')
    486
    487		if D47 is not None and T is not None:
    488			raise ValueError('Either D47 or T must be specified, but not both.')
    489
    490		if T is not None:
    491
    492			D47 = self._D47_from_T(T)
    493			Np = len(self.degrees)
    494			N = D47.size
    495
    496			### Compute covariance matrix of (*bfp, *T):
    497			CM = _np.zeros((Np+N, Np+N))
    498
    499			if error_from in ['calib', 'both']:
    500				CM[:Np, :Np] = self.bfp_CM[:,:]
    501
    502			if (sT is not None) and error_from in ['sT', 'both']:
    503				_sT = _np.asarray(sT)
    504				if _sT.ndim == 0:
    505					for k in range(N):
    506						CM[Np+k, Np+k] = _sT**2
    507				elif _sT.ndim == 1:
    508					for k in range(N):
    509						CM[Np+k, Np+k] = _sT[k]**2
    510				elif _sT.ndim == 2:
    511					CM[-N:, -N:] = _sT[:,:]
    512
    513			### Compute Jacobian of D47(T) relative to (*bfp, *T):
    514			_T = _np.asarray(T)
    515			if _T.ndim == 0:
    516				_T = _np.expand_dims(_T, 0)
    517			J = _np.zeros((N, Np+N))
    518
    519			if (sT is not None) and error_from in ['sT', 'both']:
    520				for k in range(N):
    521					J[k, Np+k] = self._D47_from_T_deriv(_T[k])
    522
    523			if error_from in ['calib', 'both']:
    524
    525				for k in range(Np):
    526
    527					p1 = {_: self.bfp[_] for _ in self.bfp}
    528					p1[f'a{self.degrees[k]}'] += 0.001 * self.bfp_CM[k,k]**.5
    529
    530					p2 = {_: self.bfp[_] for _ in self.bfp}
    531					p2[f'a{self.degrees[k]}'] -= 0.001 * self.bfp_CM[k,k]**.5
    532
    533					J[:, k] = (self.model_fun(p1, (_T+273.15)**-1) - self.model_fun(p2, (_T+273.15)**-1)) / (0.002 * self.bfp_CM[k,k]**.5)
    534
    535			### Error propagation:
    536			CM_D47 = J @ CM @ J.T
    537
    538			if return_covar:
    539				return D47, CM_D47
    540			else:
    541				return D47, float(_np.diag(CM_D47)**.5) if D47.ndim == 0 else _np.diag(CM_D47)**.5
    542
    543		if D47 is not None:
    544
    545			T = self._T_from_D47(D47)
    546			Np = len(self.degrees)
    547			N = T.size
    548
    549			### Compute covariance matrix of (*bfp, *T):
    550			CM = _np.zeros((Np+N, Np+N))
    551
    552			if error_from in ['calib', 'both']:
    553				CM[:Np, :Np] = self.bfp_CM[:,:]
    554
    555			if (sD47 is not None) and error_from in ['sD47', 'both']:
    556				_sD47 = _np.asarray(sD47)
    557				if _sD47.ndim == 0:
    558					for k in range(N):
    559						CM[Np+k, Np+k] = _sD47**2
    560				elif _sD47.ndim == 1:
    561					for k in range(N):
    562						CM[Np+k, Np+k] = _sD47[k]**2
    563				elif _sD47.ndim == 2:
    564					CM[-N:, -N:] = _sD47[:,:]
    565
    566			### Compute Jacobian of T(D47) relative to (*bfp, *D47):
    567			_D47 = _np.asarray(D47)
    568			if _D47.ndim == 0:
    569				_D47 = _np.expand_dims(_D47, 0)
    570			J = _np.zeros((N, Np+N))
    571			if (sD47 is not None) and error_from in ['sD47', 'both']:
    572				for k in range(N):
    573					J[k, Np+k] = self._T_from_D47_deriv(_D47[k])
    574			if error_from in ['calib', 'both']:
    575
    576				xi = _np.linspace(0,200**-1,1001)[1:]
    577				for k in range(Np):
    578
    579					if self.bfp_CM[k,k]:
    580						_epsilon_ = self.bfp_CM[k,k]**.5
    581					else:
    582						_epsilon_ = 1e-6
    583
    584					p1 = {_: self.bfp[_] for _ in self.bfp}
    585					p1[f'a{self.degrees[k]}'] += 0.001 * _epsilon_
    586					T_from_D47_p1 = _interp1d(self.model_fun(p1, xi), xi**-1 - 273.15)
    587
    588					p2 = {_: self.bfp[_] for _ in self.bfp}
    589					p2[f'a{self.degrees[k]}'] -= 0.001 * _epsilon_
    590					T_from_D47_p2 = _interp1d(self.model_fun(p2, xi), xi**-1 - 273.15)
    591
    592					J[:, k] = (T_from_D47_p1(_D47) - T_from_D47_p2(_D47)) / (0.002 * _epsilon_)
    593
    594			### Error propagation:
    595			CM_T = J @ CM @ J.T
    596
    597			if return_covar:
    598				return T, CM_T
    599			else:
    600				return T, float(_np.diag(CM_T)**.5) if T.ndim == 0 else _np.diag(CM_T)**.5
    601
    602
    603	def plot_T47_errors(
    604		self,
    605		calibname = None,
    606		rD47 = 0.010,
    607		Nr = [2,4,8,12,20],
    608		Tmin = 0,
    609		Tmax = 120,
    610		colors = [(1,0,0),(1,.5,0),(.25,.75,0),(0,.5,1),(0.5,0.5,0.5)],
    611		yscale = 'lin',
    612		):
    613		"""
    614		Plot SE of T reconstructed using the calibration as a function of T for various
    615		combinations of analytical precision and number of analytical replicates.
    616
    617		**Arguments**
    618
    619		+ **calibname**:
    620		Which calibration name to display. By default, use `label` attribute.
    621		+ **rD47**:
    622		Analytical precision of a single analysis.
    623		+ **Nr**:
    624		A list of lines to plot, each corresponding to a given number of replicates.
    625		+ **Tmin**:
    626		Minimum T to plot.
    627		+ **Tmax**:
    628		Maximum T to plot.
    629		+ **colors**:
    630		A list of colors to distinguish the plotted lines.
    631		+ **yscale**:
    632		  + If `'lin'`, the Y axis uses a linear scale.
    633		  + If `'log'`, the Y axis uses a logarithmic scale.
    634
    635		**Example**
    636
    637		````py
    638		from matplotlib import pyplot as ppl
    639		from D47calib import devils_laghetto_2023 as calib
    640
    641		fig = ppl.figure(figsize = (3.5,4))
    642		ppl.subplots_adjust(bottom = .2, left = .15)
    643		calib.plot_T47_errors(
    644			calibname = 'Devils Laghetto calibration',
    645			Nr = [1,2,4,16],
    646			Tmin  =0,
    647			Tmax = 40,
    648			)
    649		ppl.savefig('example_SE_T.png', dpi = 100)
    650		````
    651
    652		This should result in something like this:
    653
    654		<img src="example_SE_T.png">
    655		"""
    656
    657		if calibname is None:
    658			calibname = self.label
    659
    660		Nr = _np.array(Nr)
    661		if len(colors) < Nr.size:
    662			print('WARNING: Too few colors to plot different numbers of replicates; generating new colors.')
    663			from colorsys import hsv_to_rgb
    664			hsv = [(x*1.0/Nr.size, 1, .9) for x in range(Nr.size)]
    665			colors = [hsv_to_rgb(*x) for x in hsv]
    666
    667		Ti = _np.linspace(Tmin, Tmax)
    668		D47i, _  = self.T47(T = Ti)
    669		_, sT_calib = self.T47(D47 = D47i, error_from = 'calib')
    670
    671		ymax, ymin = 0, 1e6
    672		for N,c in zip(Nr, colors):
    673			_, sT = self.T47(D47 = D47i, sD47 = rD47 / N**.5, error_from = 'sD47')
    674			_ppl.plot(Ti, sT, '-', color = c, label=f'SE for {N} replicate{"s" if N > 1 else ""}')
    675			ymin = min(ymin, min(sT))
    676			ymax = max(ymax, max(sT))
    677
    678		_ppl.plot(Ti, sT_calib, 'k--', label='SE from calibration')
    679
    680		_ppl.legend(fontsize=9)
    681		_ppl.xlabel("T (°C)")
    682
    683		_ppl.ylabel("Standard error on reconstructed T (°C)")
    684
    685		# yticks([0,.5,1,1.5,2])
    686		_ppl.title(f"{calibname},\nassuming external Δ$_{{47}}$ repeatability of {rD47:.3f} ‰", size = 9)
    687		_ppl.grid( alpha = .25)
    688		if yscale == 'lin':
    689			_ppl.axis([Ti[0], Ti[-1], 0, ymax*1.05])
    690			t1, t2 = self.T.min(), self.T.max()
    691			_ppl.plot([t1, t2], [0, 0], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False)
    692			_ppl.text((t1+t2)/2, 0, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic')
    693			_ppl.axis([None, None, None, _ppl.axis()[-1]*1.25])
    694		elif yscale == 'log':
    695			ymin /= 2
    696			_ppl.axis([Ti[0], Ti[-1], ymin, ymax*1.05])
    697			_ppl.yscale('log')
    698			t1, t2 = self.T.min(), self.T.max()
    699			_ppl.plot([t1, t2], [ymin, ymin], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False)
    700			_ppl.text((t1+t2)/2, ymin, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic')
    701
    702	def export_data(self, csvfile, sep = ',', label = False, T_correl = False, D47_correl = False):
    703		"""
    704		Write calibration data to a csv file.
    705
    706		### Parameters
    707
    708		+ **csvfile**:
    709		The filename to write data to.
    710		+ **sep**:
    711		The separator between CSV fields.
    712		+ **label**:
    713		  + If specified as `True`, include a `Dataset` column with the calibration's `label` attribute.
    714		  + If specified as a `str`, include a `Dataset` column with that string.
    715		  + If specified as `False`, do not include a `Dataset` column.
    716		+ **T_correl**:
    717		  + If `True`, include correlations between all `T` values.
    718		+ **D47_correl**:
    719		  + If `True`, include correlations between all `D47` values.
    720
    721		### Example
    722
    723		````py
    724		D47calib.huyghe_2022.export_data(
    725			csvfile = 'example_export_data.csv',
    726			T_correl = True,
    727			D47_correl = True,
    728			)
    729		````
    730
    731		This should result in something like this ([link](example_export_data.csv)):
    732
    733		.. include:: ../../docs/example_export_data.md
    734
    735		"""
    736		n = len(str(self.N))
    737
    738		with open(csvfile, 'w') as f:
    739			f.write(sep.join(['ID', 'Sample', 'T', 'SE_T', 'D47', 'SE_D47']))
    740
    741			if label:
    742				f.write(f'{sep}Dataset')
    743
    744			if T_correl:
    745				inv_diag_sT = _np.diag(_np.diag(self.sT)**-.5)
    746				Tcorrel = inv_diag_sT @ self.sT @ inv_diag_sT
    747				f.write(sep.join(['']+[f'Tcorrel_{k+1:0{n}d}' for k in range(self.N)]))
    748
    749			if D47_correl:
    750				inv_diag_sD47 = _np.diag(_np.diag(self.sD47)**-.5)
    751				D47correl = inv_diag_sD47 @ self.sD47 @ inv_diag_sD47
    752				f.write(sep.join(['']+[f'D47correl_{k+1:0{n}d}' for k in range(self.N)]))
    753
    754			for k, (s, T, sT, D47, sD47) in enumerate(zip(
    755				self.samples,
    756				self.T,
    757				_np.diag(self.sT)**.5,
    758				self.D47,
    759				_np.diag(self.sD47)**.5,
    760				)):
    761				f.write('\n' + sep.join([f'{k+1:0{n}d}', s, f'{T:.2f}', f'{sT:.2f}', f'{D47:.4f}', f'{sD47:.4f}']))
    762				if label:
    763					if label is True:
    764						f.write(f'{sep}{self.label}')
    765					else:
    766						f.write(f'{sep}{label}')
    767				if T_correl:
    768					f.write(sep.join(['']+[
    769						f'{Tcorrel[k,_]:.0f}'
    770						if f'{Tcorrel[k,_]:.6f}'[-6:] == '000000'
    771						else f'{Tcorrel[k,_]:.6f}'
    772						for _ in range(self.N)]))
    773				if D47_correl:
    774					f.write(sep.join(['']+[
    775						f'{D47correl[k,_]:.0f}'
    776						if f'{D47correl[k,_]:.6f}'[-6:] == '000000'
    777						else f'{D47correl[k,_]:.6f}'
    778						for _ in range(self.N)]))
    779
    780
    781	def export(self, name, filename):
    782		"""
    783		Save `D47calib` object as an importable file.
    784
    785		### Parameters
    786
    787		+ **name**:
    788		The name of the variable to export.
    789		+ **filename**:
    790		The filename to write to.
    791
    792		### Example
    793
    794		````py
    795		D47calib.anderson_2021_lsce.export('foo', 'bar.py')
    796		````
    797
    798		This should result in a `bar.py` file with the following contents:
    799
    800		````py
    801		foo = D47calib(
    802			samples = ['LGB-2', 'DVH-2'],
    803			T = [7.9, 33.7],
    804			D47 = [0.6485720997671647, 0.5695972909966959],
    805			sT = [[0.04000000000000001, 0.0], [0.0, 0.04000000000000001]],
    806			sD47 = [[8.72797097773764e-06, 2.951894073404263e-06], [2.9518940734042614e-06, 7.498611746762038e-06]],
    807			description = 'Devils Hole & Laghetto Basso from Anderson et al. (2021), processed in I-CDES',
    808			label = 'Slow-growing calcites from Anderson et al. (2021)',
    809			color = (0, 0.5, 0),
    810			degrees = [0, 2],
    811			bfp = {'a0': 0.1583220210575451, 'a2': 38724.41371782721},
    812			bfp_CM = [[0.00035908667755871876, -30.707016431538836], [-30.70701643153884, 2668091.396598919]],
    813			chisq = 6.421311854486162e-27,
    814			Nf = 0,
    815			)
    816		````
    817		"""
    818		with open(filename, 'w') as f:
    819			f.write(f'''
    820{name} = D47calib(
    821	samples = {self.samples},
    822	T = {list(self.T)},
    823	D47 = {list(self.D47)},
    824	sT = {[list(l) for l in self.sT]},
    825	sD47 = {[list(l) for l in self.sD47]},
    826	degrees = {self.degrees},
    827	description = {repr(self.description)},
    828	name = {repr(self.name)},
    829	label = {repr(self.label)},
    830	bfp = {self.bfp},
    831	bfp_CM = {[list(l) for l in self.bfp_CM]},
    832	chisq = {self.chisq},
    833	cholesky_residuals = {list(self.cholesky_residuals)},
    834	aic = {self.aic},
    835	bic = {self.bic},
    836	ks_pvalue = {self.ks_pvalue},
    837	)
    838''')

Δ47 calibration class based on OGLS regression of Δ47 as a polynomial function of inverse T.

D47calib( samples, T, D47, sT=None, sD47=None, degrees=[0, 2], xpower=2, name='', label='', description='', **kwargs) View Source

     45	def __init__(self,
     46		samples, T, D47,
     47		sT = None,
     48		sD47 = None,
     49		degrees = [0,2],
     50		xpower = 2,
     51		name = '',
     52		label = '',
     53		description = '',
     54		**kwargs,
     55		):
     56		"""
     57		### Parameters
     58
     59		+ **samples**: a list of N sample names.
     60		+ **T**: a 1-D array (or array-like) of temperatures values (in degrees C), of size N.
     61		+ **D47**: a 1-D array (or array-like) of Δ47 values (in permil), of size N.
     62		+ **sT**: uncertainties on `T`. If specified as:
     63		  + a scalar: `sT` is treated as the standard error applicable to all `T` values;
     64		  + a 1-D array-like of size N: `sT` is treated as the standard errors of `T`;
     65		  + a 2-D array-like of size (N, N): `sT` is treated as the (co)variance matrix of `T`.
     66		+ **sD47**: uncertainties on `D47`. If specified as:
     67		  + a scalar: `sD47` is treated as the standard error applicable to all `D47` values;
     68		  + a 1-D array-like of size N: `sD47` is treated as the standard errors of `D47`;
     69		  + a 2-D array-like of size (N, N): `sD47` is treated as the (co)variance matrix of `D47`.
     70		+ **degrees**: degrees of the polynomial regression, e.g., `[0, 2]` or `[0, 1, 2, 3, 4]`.
     71		+ **name**: a human-readable, short name assigned to the calibration.
     72		+ **label**: a short description of the calibration, e.g., to be used in legends.
     73		+ **description**: a longer description, including relevant references/DOIs.
     74		This is not necessary when `bfp` and `CM_bfp` are specified at instantiation time.
     75		+ **kwargs**: keyword arguments passed to the underlying `ogls.InverseTPolynomial()` call.
     76
     77		### Notable attributes
     78
     79		+ **N**:
     80		The total number of observations (samples) in the calibration data.
     81		+ **samples**:
     82		The list sample names.
     83		+ **T**:
     84		1-D `ndarray` of temperatures in degrees C.
     85		+ **D47**:
     86		1-D `ndarray` of Δ47 values in permil.
     87		+ **sT**:
     88		2-D `ndarray` equal to the full (co)variance matrix for `T`.
     89		+ **D47**:
     90		2-D `ndarray` equal to the full (co)variance matrix for `D47`.
     91		+ **xpower**:
     92		By default, all `D47calib` graphical methods plot Δ47 as a function of 1/T<sup>2</sup>.
     93		It is possible to change this behavior to use a different power of 1/T.
     94		This is done by redefining the `xpower` attribute to a different, non-zero `int` value
     95		(e.g. `foo.xpower = 1` to plot as a function of 1/T instead of 1/T<sup>2</sup>).
     96		+ **bfp**:
     97		The best-fit parameters of the regression.
     98		This is a `dict` with keys equal to the polynomial coefficients (see `bff` definition below)
     99		+ **bff()**:
    100		The best-fit polynomial function of inverse T, defined as:
    101		`bff(x) = sum(bfp[f'a{k}'] * x**k for k in degrees)`
    102		Note that `bff` takes `x = 1/(T+273.15)` (instead of `T`) as input.
    103
    104
    105		### Examples
    106
    107		A very simple example:
    108
    109		````py
    110		.. include:: ../../code_examples/D47calib_init/example.py
    111		````
    112
    113		Should yield:
    114
    115		````
    116		.. include:: ../../code_examples/D47calib_init/output.txt
    117		````
    118
    119		"""
    120
    121		self.samples = samples[:]
    122		self.name = name
    123		self.label = label
    124		self.description = description
    125		self.D47 = _np.asarray(D47, dtype = 'float')
    126		self.N = self.D47.size
    127
    128		if sD47 is None:
    129			self.sD47 = _np.zeros((self.N, self.N))
    130		else:
    131			self.sD47 = _np.asarray(sD47)
    132			if len(self.sD47.shape) == 1:
    133				self.sD47 = _np.diag(self.sD47**2)
    134			elif len(self.sD47.shape) == 0:
    135				self.sD47 = _np.eye(self.D47.size) * self.sD47**2
    136
    137		_ogls.InverseTPolynomial.__init__(self, T=T, Y=D47, sT=sT, sY=sD47, degrees = degrees, xpower = xpower, **kwargs)
    138
    139		if self.bfp is None:
    140			self.regress()
    141
    142		self._bff_deriv = lambda x: _np.array([k * self.bfp[f'a{k}'] * x**(k-1) for k in degrees if k > 0]).sum(axis = 0)
    143
    144		xi = _np.linspace(0,200**-1,1001)
    145		self._inv_bff = _interp1d(self.bff(xi), xi)
    146
    147		self._D47_from_T = lambda T: self.bff((T+273.15)**-1)
    148		self._T_from_D47 = lambda D47: self._inv_bff(D47)**-1 - 273.15
    149		self._D47_from_T_deriv = lambda T: -(T+273.15)**-2 * self._bff_deriv((T+273.15)**-1)
    150		self._T_from_D47_deriv = lambda D47: self._D47_from_T_deriv(self._T_from_D47(D47))**-1

### Parameters

- samples: a list of N sample names.
- T: a 1-D array (or array-like) of temperatures values (in degrees C), of size N.
- D47: a 1-D array (or array-like) of Δ47 values (in permil), of size N.
- sT: uncertainties on `T`. If specified as:
  - a scalar: `sT` is treated as the standard error applicable to all `T` values;
  - a 1-D array-like of size N: `sT` is treated as the standard errors of `T`;
  - a 2-D array-like of size (N, N): `sT` is treated as the (co)variance matrix of `T`.
- sD47: uncertainties on `D47`. If specified as:
  - a scalar: `sD47` is treated as the standard error applicable to all `D47` values;
  - a 1-D array-like of size N: `sD47` is treated as the standard errors of `D47`;
  - a 2-D array-like of size (N, N): `sD47` is treated as the (co)variance matrix of `D47`.
- degrees: degrees of the polynomial regression, e.g., `[0, 2]` or `[0, 1, 2, 3, 4]`.
- name: a human-readable, short name assigned to the calibration.
- label: a short description of the calibration, e.g., to be used in legends.
- description: a longer description, including relevant references/DOIs. This is not necessary when `bfp` and `CM_bfp` are specified at instantiation time.
- kwargs: keyword arguments passed to the underlying `ogls.InverseTPolynomial()` call.

### Notable attributes

- N: The total number of observations (samples) in the calibration data.
- samples: The list sample names.
- T: 1-D `ndarray` of temperatures in degrees C.
- D47: 1-D `ndarray` of Δ47 values in permil.
- sT: 2-D `ndarray` equal to the full (co)variance matrix for `T`.
- D47: 2-D `ndarray` equal to the full (co)variance matrix for `D47`.
- xpower: By default, all `D47calib` graphical methods plot Δ47 as a function of 1/T2. It is possible to change this behavior to use a different power of 1/T. This is done by redefining the `xpower` attribute to a different, non-zero `int` value (e.g. `foo.xpower = 1` to plot as a function of 1/T instead of 1/T2).
- bfp: The best-fit parameters of the regression. This is a `dict` with keys equal to the polynomial coefficients (see `bff` definition below)
- bff(): The best-fit polynomial function of inverse T, defined as: `bff(x) = sum(bfp[f'a{k}'] * x**k for k in degrees)` Note that `bff` takes `x = 1/(T+273.15)` (instead of `T`) as input.

### Examples

A very simple example:

    from D47calib import D47calib

    mycalib = D47calib(
            samples     = ['FOO', 'BAR'],
            T           = [0.   , 25.  ],
            D47         = [0.7  , 0.6  ],
            sT          = 1.,
            sD47        = 0.01,
            )

    T, sT = mycalib.T47(D47 = 0.650)

    print(f'T = {T:.1f}')
    print(f'sT = {sT:.1f}')

Should yield:

    T = 11.7
    sT = 1.9

def invT_xaxis(self, xlabel=None, Ti=[0, 20, 50, 100, 250, 1000]): View Source

    155	def invT_xaxis(self,
    156		xlabel = None,
    157		Ti = [0,20,50,100,250,1000],
    158		):
    159		"""
    160		Create and return an `Axes` object with X values equal to 1/T<sup>2</sup>,
    161		but labeled in degrees Celsius.
    162
    163		### Parameters
    164
    165		+ **xlabel**:
    166		Custom label for X axis (`r'$1\,/\,T^2$'` by default)
    167		+ **Ti**:
    168		Specify tick locations for X axis, in degrees C.
    169
    170		### Returns
    171
    172		+ an `matplotlib.axes.Axes` instance
    173
    174		### Examples
    175
    176		````py
    177		.. include:: ../../code_examples/D47calib_invT_xaxis/example_1.py
    178		````
    179
    180		This should result in something like this:
    181
    182		<img align="center" src="example_invT_xaxis_1.png">
    183
    184		It is also possible to define the X axis using a different power of 1/T
    185		by first redefining the `xpower` attribute:
    186
    187		````py
    188		.. include:: ../../code_examples/D47calib_invT_xaxis/example_2.py
    189		````
    190
    191		This should result in something like this:
    192
    193		<img align="center" src="example_invT_xaxis_2.png">
    194		"""
    195		if xlabel is None:
    196			xlabel = f'$1\\,/\\,T^{self.xpower}$' if self.xpower > 1 else '1/T'
    197		_ppl.xlabel(xlabel)
    198		_ppl.xticks([(273.15 + t) ** -self.xpower for t in sorted(Ti)[::-1]])
    199		ax = _ppl.gca()
    200		ax.set_xticklabels([f"${t}\\,$°C" for t in sorted(Ti)[::-1]])
    201		ax.tick_params(which="major")
    202
    203		return ax

Create and return an `Axes` object with X values equal to 1/T2, but labeled in degrees Celsius.

### Parameters

- xlabel: Custom label for X axis (`r'$1\,/\,T^2$'` by default)
- Ti: Specify tick locations for X axis, in degrees C.

### Returns

- an `matplotlib.axes.Axes` instance

### Examples

    from matplotlib import pyplot as ppl
    from D47calib import ogls_2023 as calib

    fig = ppl.figure(figsize = (5,3))
    ppl.subplots_adjust(bottom = .25, left = .15)
    ax = calib.invT_xaxis()
    ax.set_xlim((0, 270**-2))
    ppl.savefig('example_invT_xaxis_1.png', dpi = 100)

This should result in something like this:

It is also possible to define the X axis using a different power of 1/T by first redefining the `xpower` attribute:

    from matplotlib import pyplot as ppl
    from D47calib import ogls_2023 as calib

    calib.xpower = 4

    fig = ppl.figure(figsize = (5,3))
    ppl.subplots_adjust(bottom = .25, left = .15)
    ax = calib.invT_xaxis(Ti = [1000, 100, 50, 25, 0])
    ax.set_xlim((0, 270**-4))
    ppl.savefig('example_invT_xaxis_2.png', dpi = 100)

This should result in something like this:

def plot_data(self, label=False, **kwargs): View Source

    206	def plot_data(self, label = False, **kwargs):
    207		"""
    208		Plot Δ47 value of each sample as a function of 1/T<sup>2</sup>.
    209
    210		### Parameters
    211
    212		+ **label**:
    213		  + If `label` is a string, use this string as `label` for the underlyig
    214		  `matplotlib.pyplot.plot()` call.
    215		  + If `label = True`, use the caller's `label` attribute instead.
    216		  + If `label = False`, no label is specified (default behavior).
    217		+ **kwargs**:
    218		keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call.
    219
    220		### Returns
    221
    222		+ the return value(s) of the underlying `matplotlib.pyplot.plot()` call.
    223
    224		### Example
    225
    226		````py
    227		from matplotlib import pyplot as ppl
    228		from D47calib import huyghe_2022 as calib
    229
    230		fig = ppl.figure(figsize = (5,3))
    231		ppl.subplots_adjust(bottom = .25, left = .15)
    232		calib.invT_xaxis(Ti = [0,10,25])
    233		calib.plot_data(label = True)
    234		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    235		ppl.legend()
    236		ppl.savefig('example_plot_data.png', dpi = 100)
    237		`````
    238
    239		This should result in something like this:
    240
    241		<img align="center" src="example_plot_data.png">
    242		"""
    243# 		if 'mec' not in kwargs:
    244# 			kwargs['mec'] = self.color
    245		if label is not False:
    246			kwargs['label'] = self.label if label is True else label
    247		return _ogls.InverseTPolynomial.plot_data(self, **kwargs)

Plot Δ47 value of each sample as a function of 1/T2.

### Parameters

- label:
  - If `label` is a string, use this string as `label` for the underlyig `matplotlib.pyplot.plot()` call.
  - If `label = True`, use the caller's `label` attribute instead.
  - If `label = False`, no label is specified (default behavior).
- kwargs: keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call.

### Returns

- the return value(s) of the underlying `matplotlib.pyplot.plot()` call.

### Example

    from matplotlib import pyplot as ppl
    from D47calib import huyghe_2022 as calib

    fig = ppl.figure(figsize = (5,3))
    ppl.subplots_adjust(bottom = .25, left = .15)
    calib.invT_xaxis(Ti = [0,10,25])
    calib.plot_data(label = True)
    ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    ppl.legend()
    ppl.savefig('example_plot_data.png', dpi = 100)

This should result in something like this:

def plot_error_bars(self, **kwargs): View Source

    250	def plot_error_bars(self, **kwargs):
    251		"""
    252		Plot Δ47 error bars (±1.96 SE) of each sample as a function of 1/T<sup>2</sup>.
    253
    254		### Parameters
    255
    256		+ **kwargs**:
    257		keyword arguments passed to the underlying `matplotlib.pyplot.errrobar()` call.
    258
    259		### Returns
    260
    261		+ the return value(s) of the underlying `matplotlib.pyplot.errorbar()` call.
    262
    263		### Example
    264
    265		````py
    266		from matplotlib import pyplot as ppl
    267		from D47calib import huyghe_2022 as calib
    268
    269		fig = ppl.figure(figsize = (5,3))
    270		ppl.subplots_adjust(bottom = .25, left = .15)
    271		calib.invT_xaxis(Ti = [0,10,25])
    272		calib.plot_error_bars(alpha = .4)
    273		calib.plot_data(label = True)
    274		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    275		ppl.legend()
    276		ppl.savefig('example_plot_error_bars.png', dpi = 100)
    277		`````
    278
    279		This should result in something like this:
    280
    281		<img align="center" src="example_plot_error_bars.png">
    282		"""
    283# 		if 'ecolor' not in kwargs:
    284# 			kwargs['ecolor'] = self.color
    285		return _ogls.InverseTPolynomial.plot_error_bars(self, **kwargs)

Plot Δ47 error bars (±1.96 SE) of each sample as a function of 1/T2.

### Parameters

- kwargs: keyword arguments passed to the underlying `matplotlib.pyplot.errrobar()` call.

### Returns

- the return value(s) of the underlying `matplotlib.pyplot.errorbar()` call.

### Example

    from matplotlib import pyplot as ppl
    from D47calib import huyghe_2022 as calib

    fig = ppl.figure(figsize = (5,3))
    ppl.subplots_adjust(bottom = .25, left = .15)
    calib.invT_xaxis(Ti = [0,10,25])
    calib.plot_error_bars(alpha = .4)
    calib.plot_data(label = True)
    ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    ppl.legend()
    ppl.savefig('example_plot_error_bars.png', dpi = 100)

This should result in something like this:

def plot_error_ellipses(self, **kwargs): View Source

    288	def plot_error_ellipses(self, **kwargs):
    289		"""
    290		Plot Δ47 error ellipses (95 % confidence) of each sample as a function of 1/T<sup>2</sup>.
    291
    292		### Parameters
    293
    294		+ **kwargs**:
    295		keyword arguments passed to the underlying `matplotlib.patches.Ellipse()` call.
    296
    297		### Returns
    298
    299		+ the return value(s) of the underlying `matplotlib.patches.Ellipse()` call.
    300
    301		### Example
    302
    303		````py
    304		from matplotlib import pyplot as ppl
    305		from D47calib import huyghe_2022 as calib
    306
    307		fig = ppl.figure(figsize = (5,3))
    308		ppl.subplots_adjust(bottom = .25, left = .15)
    309		calib.invT_xaxis(Ti = [0,10,25])
    310		calib.plot_error_ellipses(alpha = .4)
    311		calib.plot_data(label = True)
    312		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    313		ppl.legend()
    314		ppl.savefig('example_plot_error_ellipses.png', dpi = 100)
    315		`````
    316
    317		This should result in something like this:
    318
    319		<img align="center" src="example_plot_error_ellipses.png">
    320		"""
    321# 		if 'ec' not in kwargs:
    322# 			kwargs['ec'] = self.color
    323		return _ogls.InverseTPolynomial.plot_error_ellipses(self, **kwargs)

Plot Δ47 error ellipses (95 % confidence) of each sample as a function of 1/T2.

### Parameters

- kwargs: keyword arguments passed to the underlying `matplotlib.patches.Ellipse()` call.

### Returns

- the return value(s) of the underlying `matplotlib.patches.Ellipse()` call.

### Example

    from matplotlib import pyplot as ppl
    from D47calib import huyghe_2022 as calib

    fig = ppl.figure(figsize = (5,3))
    ppl.subplots_adjust(bottom = .25, left = .15)
    calib.invT_xaxis(Ti = [0,10,25])
    calib.plot_error_ellipses(alpha = .4)
    calib.plot_data(label = True)
    ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    ppl.legend()
    ppl.savefig('example_plot_error_ellipses.png', dpi = 100)

This should result in something like this:

def plot_bff(self, label=False, **kwargs): View Source

    326	def plot_bff(self, label = False, **kwargs):
    327		"""
    328		Plot best-fit regression of Δ47 as a function of 1/T<sup>2</sup>.
    329
    330		### Parameters
    331
    332		+ **label**:
    333		  + If `label` is a string, use this string as `label` for the underlyig
    334		  `matplotlib.pyplot.plot()` call.
    335		  + If `label = True`, use the caller's `label` attribute instead.
    336		  + If `label = False`, no label is specified (default behavior).
    337		+ **kwargs**:
    338		keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call.
    339
    340		### Returns
    341
    342		+ the return value(s) of the underlying `matplotlib.pyplot.plot()` call.
    343
    344		### Example
    345
    346		````py
    347		from matplotlib import pyplot as ppl
    348		from D47calib import huyghe_2022 as calib
    349
    350		fig = ppl.figure(figsize = (5,3))
    351		ppl.subplots_adjust(bottom = .25, left = .15)
    352		calib.invT_xaxis(Ti = [0,10,25])
    353		calib.plot_bff(label = True, dashes = (8,2,2,2))
    354		calib.plot_data()
    355		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    356		ppl.legend()
    357		ppl.savefig('example_plot_bff.png', dpi = 100)
    358		`````
    359
    360		This should result in something like this:
    361
    362		<img align="center" src="example_plot_bff.png">
    363		"""
    364# 		if 'color' not in kwargs:
    365# 			kwargs['color'] = self.color
    366		if label is not False:
    367			kwargs['label'] = self.label if label is True else label
    368		return _ogls.InverseTPolynomial.plot_bff(self, **kwargs)

Plot best-fit regression of Δ47 as a function of 1/T2.

### Parameters

- label:
  - If `label` is a string, use this string as `label` for the underlyig `matplotlib.pyplot.plot()` call.
  - If `label = True`, use the caller's `label` attribute instead.
  - If `label = False`, no label is specified (default behavior).
- kwargs: keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call.

### Returns

- the return value(s) of the underlying `matplotlib.pyplot.plot()` call.

### Example

    from matplotlib import pyplot as ppl
    from D47calib import huyghe_2022 as calib

    fig = ppl.figure(figsize = (5,3))
    ppl.subplots_adjust(bottom = .25, left = .15)
    calib.invT_xaxis(Ti = [0,10,25])
    calib.plot_bff(label = True, dashes = (8,2,2,2))
    calib.plot_data()
    ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    ppl.legend()
    ppl.savefig('example_plot_bff.png', dpi = 100)

This should result in something like this:

def plot_bff_ci(self, **kwargs): View Source

    371	def plot_bff_ci(self, **kwargs):
    372		"""
    373		Plot 95 % confidence region for best-fit regression of Δ47 as a function of 1/T<sup>2</sup>.
    374
    375		### Parameters
    376
    377		+ **label**:
    378		+ **kwargs**:
    379		keyword arguments passed to the underlying `matplotlib.pyplot.fill_between()` call.
    380
    381		### Returns
    382
    383		+ the return value(s) of the underlying `matplotlib.pyplot.fill_between()` call.
    384
    385		### Example
    386
    387		````py
    388		from matplotlib import pyplot as ppl
    389		from D47calib import huyghe_2022 as calib
    390
    391		fig = ppl.figure(figsize = (5,3))
    392		ppl.subplots_adjust(bottom = .25, left = .15)
    393		calib.invT_xaxis(Ti = [0,10,25])
    394		calib.plot_bff_ci(alpha = .15)
    395		calib.plot_bff(label = True, dashes = (8,2,2,2))
    396		calib.plot_data()
    397		ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    398		ppl.legend()
    399		ppl.savefig('example_plot_bff_ci.png', dpi = 100)
    400		`````
    401
    402		This should result in something like this:
    403
    404		<img align="center" src="example_plot_bff_ci.png">
    405		"""
    406# 		if 'color' not in kwargs:
    407# 			kwargs['color'] = self.color
    408		return _ogls.InverseTPolynomial.plot_bff_ci(self, **kwargs)

Plot 95 % confidence region for best-fit regression of Δ47 as a function of 1/T2.

### Parameters

- label:
- kwargs: keyword arguments passed to the underlying `matplotlib.pyplot.fill_between()` call.

### Returns

- the return value(s) of the underlying `matplotlib.pyplot.fill_between()` call.

### Example

    from matplotlib import pyplot as ppl
    from D47calib import huyghe_2022 as calib

    fig = ppl.figure(figsize = (5,3))
    ppl.subplots_adjust(bottom = .25, left = .15)
    calib.invT_xaxis(Ti = [0,10,25])
    calib.plot_bff_ci(alpha = .15)
    calib.plot_bff(label = True, dashes = (8,2,2,2))
    calib.plot_data()
    ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
    ppl.legend()
    ppl.savefig('example_plot_bff_ci.png', dpi = 100)

This should result in something like this:

def T47( self, D47=None, sD47=None, T=None, sT=None, error_from='both', return_covar=False): View Source

    410	def T47(self,
    411		D47 = None,
    412		sD47 = None,
    413		T=None,
    414		sT = None,
    415		error_from = 'both',
    416		return_covar = False,
    417		):
    418		'''
    419		When `D47` is input, computes corresponding T value(s).
    420		`D47` input may be specified as a scalar, or as a 1-D array.
    421		`T` output will then have the same type and size as `D47`.
    422
    423		When `T` is input, computes corresponding Δ47 value(s).
    424		`T` input may be specified as a scalar, or as a 1-D array.
    425		`D47` output will then have the same type and size as `T`.
    426
    427		Only one of either `D47` or `T` may be specified as input.
    428
    429		**Arguments:**
    430
    431		* `D47`: Δ47 value(s) to convert into temperature (`float` or 1-D array)
    432		* `sD47`: Δ47 uncertainties, which may be:
    433		  - `None` (default)
    434		  - `float` or `int` (uniform standard error on `D47`)
    435		  - 1-D array (standard errors on `D47`)
    436		  - 2-D array (covariance matrix for `D47`)
    437		* `T`: T value(s) to convert into Δ47 (`float` or 1-D array), in degrees C
    438		* `sT`: T uncertainties, which may be:
    439		  - `None` (default)
    440		  - `float` or `int` (uniform standard error on `T`)
    441		  - 1-D array (standard errors on `T`)
    442		  - 2-D array (variance-covariance matrix for `T`)
    443		* `error_from`: if set to `'both'` (default), returned errors take into account
    444		  input uncertainties (`sT` or `sD47`) as well as calibration uncertainties;
    445		  if set to `'calib'`, only calibration uncertainties are accounted for;
    446		  if set to `'sT'` or `'sD47'`, calibration uncertainties are ignored.
    447		* `return_covar`: (False by default) whether to return the full covariance matrix
    448		  for returned `T` or `D47` values, otherwise return standard errors for the returned
    449		  `T` or `D47` values instead.
    450
    451		**Returns (with `D47` input):**
    452
    453		* `T`: temperature value(s) computed from `D47`
    454		* `sT`: uncertainties on `T` value(s), whether as standard error(s) or covariance matrix
    455
    456		**Returns (with `T` input):**
    457
    458		* `D47`: Δ47 value(s) computed from `D47`
    459		* `sD47`: uncertainties on `D47` value(s), whether as standard error(s) or covariance matrix
    460
    461		### Example
    462
    463		````py
    464		import numpy as np
    465		from matplotlib import pyplot as ppl
    466		from D47calib import ogls_2023 as calib
    467
    468		X = np.linspace(1473**-2, 270**-2)
    469		D47, sD47 = calib.T47(T = X**-0.5 - 273.15)
    470
    471		fig = ppl.figure(figsize = (5,3))
    472		ppl.subplots_adjust(bottom = .25, left = .15)
    473		calib.invT_xaxis()
    474		ppl.plot(X, 1000 * sD47, 'r-')
    475		ppl.ylabel('Calibration SE on $Δ_{47}$ values (ppm)')
    476		ppl.savefig('example_SE47.png', dpi = 100)
    477		`````
    478
    479		This should result in something like this:
    480
    481		<img src="example_SE47.png">
    482		'''
    483
    484		if D47 is None and T is None:
    485			raise ValueError('Either D47 or T must be specified, but both are undefined.')
    486
    487		if D47 is not None and T is not None:
    488			raise ValueError('Either D47 or T must be specified, but not both.')
    489
    490		if T is not None:
    491
    492			D47 = self._D47_from_T(T)
    493			Np = len(self.degrees)
    494			N = D47.size
    495
    496			### Compute covariance matrix of (*bfp, *T):
    497			CM = _np.zeros((Np+N, Np+N))
    498
    499			if error_from in ['calib', 'both']:
    500				CM[:Np, :Np] = self.bfp_CM[:,:]
    501
    502			if (sT is not None) and error_from in ['sT', 'both']:
    503				_sT = _np.asarray(sT)
    504				if _sT.ndim == 0:
    505					for k in range(N):
    506						CM[Np+k, Np+k] = _sT**2
    507				elif _sT.ndim == 1:
    508					for k in range(N):
    509						CM[Np+k, Np+k] = _sT[k]**2
    510				elif _sT.ndim == 2:
    511					CM[-N:, -N:] = _sT[:,:]
    512
    513			### Compute Jacobian of D47(T) relative to (*bfp, *T):
    514			_T = _np.asarray(T)
    515			if _T.ndim == 0:
    516				_T = _np.expand_dims(_T, 0)
    517			J = _np.zeros((N, Np+N))
    518
    519			if (sT is not None) and error_from in ['sT', 'both']:
    520				for k in range(N):
    521					J[k, Np+k] = self._D47_from_T_deriv(_T[k])
    522
    523			if error_from in ['calib', 'both']:
    524
    525				for k in range(Np):
    526
    527					p1 = {_: self.bfp[_] for _ in self.bfp}
    528					p1[f'a{self.degrees[k]}'] += 0.001 * self.bfp_CM[k,k]**.5
    529
    530					p2 = {_: self.bfp[_] for _ in self.bfp}
    531					p2[f'a{self.degrees[k]}'] -= 0.001 * self.bfp_CM[k,k]**.5
    532
    533					J[:, k] = (self.model_fun(p1, (_T+273.15)**-1) - self.model_fun(p2, (_T+273.15)**-1)) / (0.002 * self.bfp_CM[k,k]**.5)
    534
    535			### Error propagation:
    536			CM_D47 = J @ CM @ J.T
    537
    538			if return_covar:
    539				return D47, CM_D47
    540			else:
    541				return D47, float(_np.diag(CM_D47)**.5) if D47.ndim == 0 else _np.diag(CM_D47)**.5
    542
    543		if D47 is not None:
    544
    545			T = self._T_from_D47(D47)
    546			Np = len(self.degrees)
    547			N = T.size
    548
    549			### Compute covariance matrix of (*bfp, *T):
    550			CM = _np.zeros((Np+N, Np+N))
    551
    552			if error_from in ['calib', 'both']:
    553				CM[:Np, :Np] = self.bfp_CM[:,:]
    554
    555			if (sD47 is not None) and error_from in ['sD47', 'both']:
    556				_sD47 = _np.asarray(sD47)
    557				if _sD47.ndim == 0:
    558					for k in range(N):
    559						CM[Np+k, Np+k] = _sD47**2
    560				elif _sD47.ndim == 1:
    561					for k in range(N):
    562						CM[Np+k, Np+k] = _sD47[k]**2
    563				elif _sD47.ndim == 2:
    564					CM[-N:, -N:] = _sD47[:,:]
    565
    566			### Compute Jacobian of T(D47) relative to (*bfp, *D47):
    567			_D47 = _np.asarray(D47)
    568			if _D47.ndim == 0:
    569				_D47 = _np.expand_dims(_D47, 0)
    570			J = _np.zeros((N, Np+N))
    571			if (sD47 is not None) and error_from in ['sD47', 'both']:
    572				for k in range(N):
    573					J[k, Np+k] = self._T_from_D47_deriv(_D47[k])
    574			if error_from in ['calib', 'both']:
    575
    576				xi = _np.linspace(0,200**-1,1001)[1:]
    577				for k in range(Np):
    578
    579					if self.bfp_CM[k,k]:
    580						_epsilon_ = self.bfp_CM[k,k]**.5
    581					else:
    582						_epsilon_ = 1e-6
    583
    584					p1 = {_: self.bfp[_] for _ in self.bfp}
    585					p1[f'a{self.degrees[k]}'] += 0.001 * _epsilon_
    586					T_from_D47_p1 = _interp1d(self.model_fun(p1, xi), xi**-1 - 273.15)
    587
    588					p2 = {_: self.bfp[_] for _ in self.bfp}
    589					p2[f'a{self.degrees[k]}'] -= 0.001 * _epsilon_
    590					T_from_D47_p2 = _interp1d(self.model_fun(p2, xi), xi**-1 - 273.15)
    591
    592					J[:, k] = (T_from_D47_p1(_D47) - T_from_D47_p2(_D47)) / (0.002 * _epsilon_)
    593
    594			### Error propagation:
    595			CM_T = J @ CM @ J.T
    596
    597			if return_covar:
    598				return T, CM_T
    599			else:
    600				return T, float(_np.diag(CM_T)**.5) if T.ndim == 0 else _np.diag(CM_T)**.5

When `D47` is input, computes corresponding T value(s). `D47` input may be specified as a scalar, or as a 1-D array. `T` output will then have the same type and size as `D47`.

When `T` is input, computes corresponding Δ47 value(s). `T` input may be specified as a scalar, or as a 1-D array. `D47` output will then have the same type and size as `T`.

Only one of either `D47` or `T` may be specified as input.

Arguments:

- `D47`: Δ47 value(s) to convert into temperature (`float` or 1-D array)
- `sD47`: Δ47 uncertainties, which may be:
  - `None` (default)
  - `float` or `int` (uniform standard error on `D47`)
  - 1-D array (standard errors on `D47`)
  - 2-D array (covariance matrix for `D47`)
- `T`: T value(s) to convert into Δ47 (`float` or 1-D array), in degrees C
- `sT`: T uncertainties, which may be:
  - `None` (default)
  - `float` or `int` (uniform standard error on `T`)
  - 1-D array (standard errors on `T`)
  - 2-D array (variance-covariance matrix for `T`)
- `error_from`: if set to `'both'` (default), returned errors take into account input uncertainties (`sT` or `sD47`) as well as calibration uncertainties; if set to `'calib'`, only calibration uncertainties are accounted for; if set to `'sT'` or `'sD47'`, calibration uncertainties are ignored.
- `return_covar`: (False by default) whether to return the full covariance matrix for returned `T` or `D47` values, otherwise return standard errors for the returned `T` or `D47` values instead.

Returns (with `D47` input):

- `T`: temperature value(s) computed from `D47`
- `sT`: uncertainties on `T` value(s), whether as standard error(s) or covariance matrix

Returns (with `T` input):

- `D47`: Δ47 value(s) computed from `D47`
- `sD47`: uncertainties on `D47` value(s), whether as standard error(s) or covariance matrix

### Example

    import numpy as np
    from matplotlib import pyplot as ppl
    from D47calib import ogls_2023 as calib

    X = np.linspace(1473**-2, 270**-2)
    D47, sD47 = calib.T47(T = X**-0.5 - 273.15)

    fig = ppl.figure(figsize = (5,3))
    ppl.subplots_adjust(bottom = .25, left = .15)
    calib.invT_xaxis()
    ppl.plot(X, 1000 * sD47, 'r-')
    ppl.ylabel('Calibration SE on $Δ_{47}$ values (ppm)')
    ppl.savefig('example_SE47.png', dpi = 100)

This should result in something like this:

def plot_T47_errors( self, calibname=None, rD47=0.01, Nr=[2, 4, 8, 12, 20], Tmin=0, Tmax=120, colors=[(1, 0, 0), (1, 0.5, 0), (0.25, 0.75, 0), (0, 0.5, 1), (0.5, 0.5, 0.5)], yscale='lin'): View Source

    603	def plot_T47_errors(
    604		self,
    605		calibname = None,
    606		rD47 = 0.010,
    607		Nr = [2,4,8,12,20],
    608		Tmin = 0,
    609		Tmax = 120,
    610		colors = [(1,0,0),(1,.5,0),(.25,.75,0),(0,.5,1),(0.5,0.5,0.5)],
    611		yscale = 'lin',
    612		):
    613		"""
    614		Plot SE of T reconstructed using the calibration as a function of T for various
    615		combinations of analytical precision and number of analytical replicates.
    616
    617		**Arguments**
    618
    619		+ **calibname**:
    620		Which calibration name to display. By default, use `label` attribute.
    621		+ **rD47**:
    622		Analytical precision of a single analysis.
    623		+ **Nr**:
    624		A list of lines to plot, each corresponding to a given number of replicates.
    625		+ **Tmin**:
    626		Minimum T to plot.
    627		+ **Tmax**:
    628		Maximum T to plot.
    629		+ **colors**:
    630		A list of colors to distinguish the plotted lines.
    631		+ **yscale**:
    632		  + If `'lin'`, the Y axis uses a linear scale.
    633		  + If `'log'`, the Y axis uses a logarithmic scale.
    634
    635		**Example**
    636
    637		````py
    638		from matplotlib import pyplot as ppl
    639		from D47calib import devils_laghetto_2023 as calib
    640
    641		fig = ppl.figure(figsize = (3.5,4))
    642		ppl.subplots_adjust(bottom = .2, left = .15)
    643		calib.plot_T47_errors(
    644			calibname = 'Devils Laghetto calibration',
    645			Nr = [1,2,4,16],
    646			Tmin  =0,
    647			Tmax = 40,
    648			)
    649		ppl.savefig('example_SE_T.png', dpi = 100)
    650		````
    651
    652		This should result in something like this:
    653
    654		<img src="example_SE_T.png">
    655		"""
    656
    657		if calibname is None:
    658			calibname = self.label
    659
    660		Nr = _np.array(Nr)
    661		if len(colors) < Nr.size:
    662			print('WARNING: Too few colors to plot different numbers of replicates; generating new colors.')
    663			from colorsys import hsv_to_rgb
    664			hsv = [(x*1.0/Nr.size, 1, .9) for x in range(Nr.size)]
    665			colors = [hsv_to_rgb(*x) for x in hsv]
    666
    667		Ti = _np.linspace(Tmin, Tmax)
    668		D47i, _  = self.T47(T = Ti)
    669		_, sT_calib = self.T47(D47 = D47i, error_from = 'calib')
    670
    671		ymax, ymin = 0, 1e6
    672		for N,c in zip(Nr, colors):
    673			_, sT = self.T47(D47 = D47i, sD47 = rD47 / N**.5, error_from = 'sD47')
    674			_ppl.plot(Ti, sT, '-', color = c, label=f'SE for {N} replicate{"s" if N > 1 else ""}')
    675			ymin = min(ymin, min(sT))
    676			ymax = max(ymax, max(sT))
    677
    678		_ppl.plot(Ti, sT_calib, 'k--', label='SE from calibration')
    679
    680		_ppl.legend(fontsize=9)
    681		_ppl.xlabel("T (°C)")
    682
    683		_ppl.ylabel("Standard error on reconstructed T (°C)")
    684
    685		# yticks([0,.5,1,1.5,2])
    686		_ppl.title(f"{calibname},\nassuming external Δ$_{{47}}$ repeatability of {rD47:.3f} ‰", size = 9)
    687		_ppl.grid( alpha = .25)
    688		if yscale == 'lin':
    689			_ppl.axis([Ti[0], Ti[-1], 0, ymax*1.05])
    690			t1, t2 = self.T.min(), self.T.max()
    691			_ppl.plot([t1, t2], [0, 0], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False)
    692			_ppl.text((t1+t2)/2, 0, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic')
    693			_ppl.axis([None, None, None, _ppl.axis()[-1]*1.25])
    694		elif yscale == 'log':
    695			ymin /= 2
    696			_ppl.axis([Ti[0], Ti[-1], ymin, ymax*1.05])
    697			_ppl.yscale('log')
    698			t1, t2 = self.T.min(), self.T.max()
    699			_ppl.plot([t1, t2], [ymin, ymin], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False)
    700			_ppl.text((t1+t2)/2, ymin, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic')

Plot SE of T reconstructed using the calibration as a function of T for various combinations of analytical precision and number of analytical replicates.

Arguments

- calibname: Which calibration name to display. By default, use `label` attribute.
- rD47: Analytical precision of a single analysis.
- Nr: A list of lines to plot, each corresponding to a given number of replicates.
- Tmin: Minimum T to plot.
- Tmax: Maximum T to plot.
- colors: A list of colors to distinguish the plotted lines.
- yscale:
  - If `'lin'`, the Y axis uses a linear scale.
  - If `'log'`, the Y axis uses a logarithmic scale.

Example

    from matplotlib import pyplot as ppl
    from D47calib import devils_laghetto_2023 as calib

    fig = ppl.figure(figsize = (3.5,4))
    ppl.subplots_adjust(bottom = .2, left = .15)
    calib.plot_T47_errors(
            calibname = 'Devils Laghetto calibration',
            Nr = [1,2,4,16],
            Tmin  =0,
            Tmax = 40,
            )
    ppl.savefig('example_SE_T.png', dpi = 100)

This should result in something like this:

def export_data( self, csvfile, sep=',', label=False, T_correl=False, D47_correl=False): View Source

    702	def export_data(self, csvfile, sep = ',', label = False, T_correl = False, D47_correl = False):
    703		"""
    704		Write calibration data to a csv file.
    705
    706		### Parameters
    707
    708		+ **csvfile**:
    709		The filename to write data to.
    710		+ **sep**:
    711		The separator between CSV fields.
    712		+ **label**:
    713		  + If specified as `True`, include a `Dataset` column with the calibration's `label` attribute.
    714		  + If specified as a `str`, include a `Dataset` column with that string.
    715		  + If specified as `False`, do not include a `Dataset` column.
    716		+ **T_correl**:
    717		  + If `True`, include correlations between all `T` values.
    718		+ **D47_correl**:
    719		  + If `True`, include correlations between all `D47` values.
    720
    721		### Example
    722
    723		````py
    724		D47calib.huyghe_2022.export_data(
    725			csvfile = 'example_export_data.csv',
    726			T_correl = True,
    727			D47_correl = True,
    728			)
    729		````
    730
    731		This should result in something like this ([link](example_export_data.csv)):
    732
    733		.. include:: ../../docs/example_export_data.md
    734
    735		"""
    736		n = len(str(self.N))
    737
    738		with open(csvfile, 'w') as f:
    739			f.write(sep.join(['ID', 'Sample', 'T', 'SE_T', 'D47', 'SE_D47']))
    740
    741			if label:
    742				f.write(f'{sep}Dataset')
    743
    744			if T_correl:
    745				inv_diag_sT = _np.diag(_np.diag(self.sT)**-.5)
    746				Tcorrel = inv_diag_sT @ self.sT @ inv_diag_sT
    747				f.write(sep.join(['']+[f'Tcorrel_{k+1:0{n}d}' for k in range(self.N)]))
    748
    749			if D47_correl:
    750				inv_diag_sD47 = _np.diag(_np.diag(self.sD47)**-.5)
    751				D47correl = inv_diag_sD47 @ self.sD47 @ inv_diag_sD47
    752				f.write(sep.join(['']+[f'D47correl_{k+1:0{n}d}' for k in range(self.N)]))
    753
    754			for k, (s, T, sT, D47, sD47) in enumerate(zip(
    755				self.samples,
    756				self.T,
    757				_np.diag(self.sT)**.5,
    758				self.D47,
    759				_np.diag(self.sD47)**.5,
    760				)):
    761				f.write('\n' + sep.join([f'{k+1:0{n}d}', s, f'{T:.2f}', f'{sT:.2f}', f'{D47:.4f}', f'{sD47:.4f}']))
    762				if label:
    763					if label is True:
    764						f.write(f'{sep}{self.label}')
    765					else:
    766						f.write(f'{sep}{label}')
    767				if T_correl:
    768					f.write(sep.join(['']+[
    769						f'{Tcorrel[k,_]:.0f}'
    770						if f'{Tcorrel[k,_]:.6f}'[-6:] == '000000'
    771						else f'{Tcorrel[k,_]:.6f}'
    772						for _ in range(self.N)]))
    773				if D47_correl:
    774					f.write(sep.join(['']+[
    775						f'{D47correl[k,_]:.0f}'
    776						if f'{D47correl[k,_]:.6f}'[-6:] == '000000'
    777						else f'{D47correl[k,_]:.6f}'
    778						for _ in range(self.N)]))

Write calibration data to a csv file.

### Parameters

- csvfile: The filename to write data to.
- sep: The separator between CSV fields.
- label:
  - If specified as `True`, include a `Dataset` column with the calibration's `label` attribute.
  - If specified as a `str`, include a `Dataset` column with that string.
  - If specified as `False`, do not include a `Dataset` column.
- T_correl:
  - If `True`, include correlations between all `T` values.
- D47_correl:
  - If `True`, include correlations between all `D47` values.

### Example

    D47calib.huyghe_2022.export_data(
            csvfile = 'example_export_data.csv',
            T_correl = True,
            D47_correl = True,
            )

This should result in something like this (link):

ID | Sample | T | SE_T | D47 | SE_D47 | D47correl_1 | D47correl_2 | D47correl_3 | D47correl_4 | D47correl_5 | D47correl_6 | D47correl_7
--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
1 | Ad | -1.80 | 0.50 | 0.6893 | 0.0060 | 1 | 0.048049 | 0.028770 | 0.544016 | 0.093188 | 0.020880 | 0.471516
2 | BDV-S | 18.70 | 0.75 | 0.6121 | 0.0049 | 0.048049 | 1 | 0.650474 | 0.053281 | 0.011132 | 0.002494 | 0.050651
3 | BDV-W | 11.01 | 1.00 | 0.6349 | 0.0052 | 0.028770 | 0.650474 | 1 | 0.031903 | 0.006666 | 0.001494 | 0.030328
4 | PY | 13.44 | 0.06 | 0.6397 | 0.0049 | 0.544016 | 0.053281 | 0.031903 | 1 | 0.104392 | 0.023391 | 0.513257
5 | TES-S | 22.50 | 2.10 | 0.5972 | 0.0053 | 0.093188 | 0.011132 | 0.006666 | 0.104392 | 1 | 0.275629 | 0.100150
6 | TES-W | 12.23 | 1.00 | 0.6329 | 0.0102 | 0.020880 | 0.002494 | 0.001494 | 0.023391 | 0.275629 | 1 | 0.022440
7 | TW | 26.80 | 0.85 | 0.6001 | 0.0048 | 0.471516 | 0.050651 | 0.030328 | 0.513257 | 0.100150 | 0.022440 | 1

def export(self, name, filename): View Source

    781	def export(self, name, filename):
    782		"""
    783		Save `D47calib` object as an importable file.
    784
    785		### Parameters
    786
    787		+ **name**:
    788		The name of the variable to export.
    789		+ **filename**:
    790		The filename to write to.
    791
    792		### Example
    793
    794		````py
    795		D47calib.anderson_2021_lsce.export('foo', 'bar.py')
    796		````
    797
    798		This should result in a `bar.py` file with the following contents:
    799
    800		````py
    801		foo = D47calib(
    802			samples = ['LGB-2', 'DVH-2'],
    803			T = [7.9, 33.7],
    804			D47 = [0.6485720997671647, 0.5695972909966959],
    805			sT = [[0.04000000000000001, 0.0], [0.0, 0.04000000000000001]],
    806			sD47 = [[8.72797097773764e-06, 2.951894073404263e-06], [2.9518940734042614e-06, 7.498611746762038e-06]],
    807			description = 'Devils Hole & Laghetto Basso from Anderson et al. (2021), processed in I-CDES',
    808			label = 'Slow-growing calcites from Anderson et al. (2021)',
    809			color = (0, 0.5, 0),
    810			degrees = [0, 2],
    811			bfp = {'a0': 0.1583220210575451, 'a2': 38724.41371782721},
    812			bfp_CM = [[0.00035908667755871876, -30.707016431538836], [-30.70701643153884, 2668091.396598919]],
    813			chisq = 6.421311854486162e-27,
    814			Nf = 0,
    815			)
    816		````
    817		"""
    818		with open(filename, 'w') as f:
    819			f.write(f'''
    820{name} = D47calib(
    821	samples = {self.samples},
    822	T = {list(self.T)},
    823	D47 = {list(self.D47)},
    824	sT = {[list(l) for l in self.sT]},
    825	sD47 = {[list(l) for l in self.sD47]},
    826	degrees = {self.degrees},
    827	description = {repr(self.description)},
    828	name = {repr(self.name)},
    829	label = {repr(self.label)},
    830	bfp = {self.bfp},
    831	bfp_CM = {[list(l) for l in self.bfp_CM]},
    832	chisq = {self.chisq},
    833	cholesky_residuals = {list(self.cholesky_residuals)},
    834	aic = {self.aic},
    835	bic = {self.bic},
    836	ks_pvalue = {self.ks_pvalue},
    837	)
    838''')

Save `D47calib` object as an importable file.

### Parameters

- name: The name of the variable to export.
- filename: The filename to write to.

### Example

    D47calib.anderson_2021_lsce.export('foo', 'bar.py')

This should result in a `bar.py` file with the following contents:

    foo = D47calib(
            samples = ['LGB-2', 'DVH-2'],
            T = [7.9, 33.7],
            D47 = [0.6485720997671647, 0.5695972909966959],
            sT = [[0.04000000000000001, 0.0], [0.0, 0.04000000000000001]],
            sD47 = [[8.72797097773764e-06, 2.951894073404263e-06], [2.9518940734042614e-06, 7.498611746762038e-06]],
            description = 'Devils Hole & Laghetto Basso from Anderson et al. (2021), processed in I-CDES',
            label = 'Slow-growing calcites from Anderson et al. (2021)',
            color = (0, 0.5, 0),
            degrees = [0, 2],
            bfp = {'a0': 0.1583220210575451, 'a2': 38724.41371782721},
            bfp_CM = [[0.00035908667755871876, -30.707016431538836], [-30.70701643153884, 2668091.396598919]],
            chisq = 6.421311854486162e-27,
            Nf = 0,
            )

def combine_D47calibs(calibs, degrees=[0, 2], same_T=[]): View Source

    840def combine_D47calibs(calibs, degrees = [0,2], same_T = []):
    841	'''
    842	Combine data from several `D47calib` instances.
    843
    844	### Parameters
    845
    846	+ **calibs**:
    847	A list of `D47calib` instances
    848	+ **degrees**:
    849	The polynomial degrees of the combined regression.
    850	+ **same_T**:
    851	Use this `list` to specify when samples from different calibrations are known/postulated
    852	to have formed at the same temperature (e.g. `DVH-2` and `DHC2-8` from the `fiebig_2021`
    853	and `anderson_2021_lsce` data sets). Each element of `same_T` is a `list` with the names
    854	of two or more samples formed at the same temperature.
    855
    856	For example, the `ogls_2023` calibration is computed with:
    857
    858	`same_T = [['DVH-2', DHC-2-8'], ['ETH-1-1100-SAM', 'ETH-1-1100']]`
    859
    860	Note that when samples from different calibrations have the same name,
    861	it is not necessary to explicitly list them in `same_T`.
    862
    863	Also note that the regression will fail if samples listed together in `same_T`
    864	actually have different `T` values specified in the original calibrations.
    865
    866	### Example
    867
    868	The `devils_laghetto_2023` calibration is computed using the following code:
    869
    870	````py
    871	K = [fiebig_2021.samples.index(_) for _ in ['LGB-2', 'DVH-2', 'DHC2-8']]
    872
    873	fiebig_temp = D47calib(
    874		samples = [fiebig_2021.samples[_] for _ in K],
    875		T = fiebig_2021.T[K],
    876		D47 = fiebig_2021.D47[K],
    877		sT = fiebig_2021.sT[K,:][:,K],
    878		sD47 = fiebig_2021.sD47[K,:][:,K],
    879		)
    880
    881	devils_laghetto_2023 = combine_D47calibs(
    882		calibs = [
    883			anderson_2021_lsce,
    884			fiebig_temp,
    885			],
    886		degrees = [0,2],
    887		same_T = [
    888			{'DVH-2', 'DHC2-8'},
    889			],
    890		)
    891	````
    892	'''
    893
    894	samples = [s for c in calibs for s in c.samples]
    895	T = [t for c in calibs for t in c.T]
    896	D47 = [x for c in calibs for x in c.D47]
    897	sD47 = _block_diag(*[c.sD47 for c in calibs])
    898	sT = _block_diag(*[c.sT for c in calibs])
    899
    900	for i in range(len(samples)):
    901		for j in range(len(samples)):
    902			if i != j:
    903				if (samples[i] == samples[j] or
    904					any([samples[i] in _ and samples[j] in _ for _ in same_T])):
    905
    906					sT[i,j] = (sT[i,i] * sT[j,j])**.5
    907
    908	calib = D47calib(
    909		samples = samples,
    910		T = T,
    911		D47 = D47,
    912		sT = sT,
    913		sD47 = sD47,
    914		degrees = degrees,
    915		)
    916
    917	return calib

Combine data from several `D47calib` instances.

### Parameters

- calibs: A list of `D47calib` instances
- degrees: The polynomial degrees of the combined regression.
- same_T: Use this `list` to specify when samples from different calibrations are known/postulated to have formed at the same temperature (e.g. `DVH-2` and `DHC2-8` from the `fiebig_2021` and `anderson_2021_lsce` data sets). Each element of `same_T` is a `list` with the names of two or more samples formed at the same temperature.

For example, the `ogls_2023` calibration is computed with:

`same_T = [['DVH-2', DHC-2-8'], ['ETH-1-1100-SAM', 'ETH-1-1100']]`

Note that when samples from different calibrations have the same name, it is not necessary to explicitly list them in `same_T`.

Also note that the regression will fail if samples listed together in `same_T` actually have different `T` values specified in the original calibrations.

### Example

The `devils_laghetto_2023` calibration is computed using the following code:

    K = [fiebig_2021.samples.index(_) for _ in ['LGB-2', 'DVH-2', 'DHC2-8']]

    fiebig_temp = D47calib(
            samples = [fiebig_2021.samples[_] for _ in K],
            T = fiebig_2021.T[K],
            D47 = fiebig_2021.D47[K],
            sT = fiebig_2021.sT[K,:][:,K],
            sD47 = fiebig_2021.sD47[K,:][:,K],
            )

    devils_laghetto_2023 = combine_D47calibs(
            calibs = [
                    anderson_2021_lsce,
                    fiebig_temp,
                    ],
            degrees = [0,2],
            same_T = [
                    {'DVH-2', 'DHC2-8'},
                    ],
            )