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. Installation

## 1.1 Library

This should do the trick:

    pip install D47calib

Alternatively:

1. download or clone the source from https://github.com/mdaeron/D47calib
2. chose one of one of the following options:
   - copy/move the `/src/D47calib` directory to somewhere in your Python path
   - copy/move the `/src/D47calib` directory to your current working directory
   - copy/move the `/src/D47calib` directory to any other location (e.g., `/foo/bar`) and include the following code snippet in your scripts:

    import sys
    sys.path.append('/foo/bar')

I you don't install from pip, you will probably need to install the requirements listed in `pyproject.toml`.

## 1.2 Only install command-line interface using `pipx`

If you only want to install the CLI, one easy option is to do so using `pipx`:

    pipx install D47calib

Then reopen a shell window and try `D47calib --help`.

# 2. Calibrations included

`D47calib` provides the following pre-built calibrations. All code and raw data used to compute these calibrations can be found in the `build_calibs` directory.

##### `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).

##### `OGLS23`:

Combined data set including all of the above. For a detailed discussion of goodness-of-fit and regression uncertainties, see Daëron & Vermeesch (2023). Also aliased as `ogls_2023` for backward-compatibility.

# 3. 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.

## 3.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 (`OGLS23`).
- `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?

### 3.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`.

### 3.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`.

### 3.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.

### 3.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

## 3.2 Integration with `D47crunch`

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

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

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

## 3.3 Further customizing the CLI

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

### 3.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`.

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

Use the `--return-covar` option.

### 3.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.

### 3.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.

### 3.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

### 3.3.6 Using a different Δ47 calibration

You may use a different calibration than the default `OGLS23` 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/install.md
      14.. include:: ../../docpages/calibs.md
      15.. include:: ../../docpages/cli.md
      16
      17* * *
      18"""
      19
      20__author__    = 'Mathieu Daëron'
      21__contact__   = 'daeron@lsce.ipsl.fr'
      22__copyright__ = 'Copyright (c) 2023 Mathieu Daëron'
      23__license__   = 'MIT License - https://opensource.org/licenses/MIT'
      24# __docformat__ = "restructuredtext"
      25__date__      = '2023-09-29'
      26__version__   = '1.3.1'
      27
      28
      29import typer, sys
      30from typing_extensions import Annotated
      31import ogls as _ogls
      32import numpy as _np
      33from scipy.linalg import block_diag as _block_diag
      34from scipy.interpolate import interp1d as _interp1d
      35from matplotlib import pyplot as _ppl
      36
      37typer.rich_utils.STYLE_HELPTEXT = ''
      38
      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 OGLS23 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''')
     839
     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 `OGLS23` 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 = [anderson_2021_lsce, fiebig_temp],
     883		degrees = [0,2],
     884		same_T = [{'DVH-2', 'DHC2-8'}],
     885		)
     886	````
     887	'''
     888
     889	samples = [s for c in calibs for s in c.samples]
     890	T = [t for c in calibs for t in c.T]
     891	D47 = [x for c in calibs for x in c.D47]
     892	sD47 = _block_diag(*[c.sD47 for c in calibs])
     893	sT = _block_diag(*[c.sT for c in calibs])
     894
     895	for i in range(len(samples)):
     896		for j in range(len(samples)):
     897			if i != j:
     898				if (samples[i] == samples[j] or
     899					any([samples[i] in _ and samples[j] in _ for _ in same_T])):
     900
     901					sT[i,j] = (sT[i,i] * sT[j,j])**.5
     902
     903	calib = D47calib(
     904		samples = samples,
     905		T = T,
     906		D47 = D47,
     907		sT = sT,
     908		sD47 = sD47,
     909		degrees = degrees,
     910		)
     911
     912	return calib
     913
     914from ._calibs import *
     915
     916def _covar2correl(C):
     917	SE = _np.diag(C)**.5
     918	return SE, _np.diag(SE**-1) @ C @ _np.diag(SE**-1)
     919
     920try:
     921	app = typer.Typer(
     922		add_completion = False,
     923		context_settings={'help_option_names': ['-h', '--help']},
     924		rich_markup_mode = 'rich',
     925		)
     926
     927	@app.command()
     928	def _cli(
     929		input: Annotated[str, typer.Argument(help = "Specify either the path of an input file or just '-' to read input from stdin")] = '-',
     930		include_samples: Annotated[str, typer.Option('--include-samples', '-u', help = 'Only include samples listed in this file')] = 'all',
     931		exclude_samples: Annotated[str, typer.Option('--exclude-samples', '-x', help = 'Exclude samples listed in this file')] = 'none',
     932		outfile: Annotated[str, typer.Option('--output-file', '-o', help = 'Write output to this file instead of printing to stdout')] = 'none',
     933		calib: Annotated[str, typer.Option('--calib', '-c', help = 'D47 calibration function to use')] = 'OGLS23',
     934		delim_in: Annotated[str, typer.Option('--delimiter-in', '-i', help = "Delimiter used in the input.")] = ',',
     935		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",
     936		T_precision: Annotated[int, typer.Option('--T-precision', '-p', help = 'Precision for T output')] = 2,
     937		D47_precision: Annotated[int, typer.Option('--D47-precision', '-q', help = 'Precision for D47 output')] = 4,
     938		correl_precision: Annotated[int, typer.Option('--correl-precision', '-r', help = 'Precision for correlation output')] = 3,
     939		covar_precision: Annotated[int, typer.Option('--covar-precision', '-s', help = 'Precision for covariance output')] = 3,
     940		return_covar: Annotated[bool, typer.Option('--return-covar', '-v', help = 'Output covariance matrix instead of correlation matrix')] = False,
     941		ignore_correl: Annotated[bool, typer.Option('--ignore-correl', '-g', help = 'Only consider and report standard errors without correlations')] = False,
     942		version: Annotated[bool, typer.Option('--version', '-V', help = 'Print D47calib version')] = False,
     943		):
     944		"""
     945[b]Purpose:[/b]
     946
     947Reads data from an input file, converts between T and D47 values, and print out the results.
     948
     949The 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>.
     950
     951The first line of the input file must be one of the following:
     952
     953- [b]Option 1:[/b] T
     954- [b]Option 2:[/b] T<sep>T_SE
     955- [b]Option 3:[/b] T<sep>T_SE<sep>T_correl
     956- [b]Option 4:[/b] T<sep>T_covar
     957- [b]Option 5:[/b] D47
     958- [b]Option 6:[/b] D47<sep>D47_SE
     959- [b]Option 7:[/b] D47<sep>D47_SE<sep>D47_correl
     960- [b]Option 8:[/b] D47<sep>D47_covar
     961
     962The rest of the input must be any number of lines with float values corresponding to the fields in the first line, separated by <sep>.
     963
     964[bold]Example input file:[/bold]
     965
     966[italic]D47     D47_SE  D47_correl[/italic]
     967[italic]0.6324  0.0101  1.00  0.25  0.25[/italic]
     968[italic]0.6281  0.0087  0.25  1.00  0.25[/italic]
     969[italic]0.6385  0.0095  0.25  0.25  1.00[/italic]
     970
     971The corresponding D47 (options 1-4) or T (options 4-8) values are computed, along with standard errors and error correlations from calibration uncertainties.
     972
     973For 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.
     974
     975The example above will thus result in an output with the following fields:
     976
     977[italic]- D47[/italic]
     978[italic]- D47_SE[/italic]
     979[italic]- D47_correl[/italic]
     980[italic]- T[/italic]
     981[italic]- T_SE_from_calib[/italic]
     982[italic]- T_correl_from_calib[/italic]
     983[italic]- T_SE_from_input[/italic]
     984[italic]- T_correl_from_input[/italic]
     985[italic]- T_SE_from_both[/italic]
     986[italic]- T_correl_from_both[/italic]
     987
     988Results may also be saved to a file using [bold]--output-file <filename>[/bold] or [bold]-o <filename>[/bold].
     989
     990To 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.
     991Then 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.
     992To 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.
     993"""
     994
     995		if version:
     996			print(__version__)
     997			return None
     998
     999		### INCOMPATIBILITY BETWEEN --ignore-correl AND --return-covar
    1000		if ignore_correl:
    1001			return_covar = False
    1002
    1003		### USE WHITESPACE AS INPUT DELIMITER
    1004		if delim_in == ' ':
    1005			delim_in = None
    1006
    1007		### SMART SELECTION OF OUTPUT DELIMITER
    1008		if delim_out == "',' when writing to output file, '>' when printing to stdout":
    1009			if outfile == 'none':
    1010				delim_out = '>'
    1011			else:
    1012				delim_out = ','
    1013
    1014		### CALIBRATION
    1015		if calib in globals() and type(globals()[calib]) == D47calib:
    1016			calib = globals()[calib]
    1017		else:
    1018			with open(calib) as f:
    1019				calibdata = _np.array([[c.strip() for c in l.strip().split(delim_in)] for l in f.readlines()[1:]], dtype = float)
    1020
    1021				degrees = [int(d) for d in calibdata[:,0]]
    1022				bfp = {f'a{k}': a for k,a in zip(degrees, calibdata[:,1])}
    1023				bfp_CM = calibdata[:,2:]
    1024				if bfp_CM.shape[0] != bfp_CM.shape[1]:
    1025					bfp_CM = _np.zeros((len(degrees), len(degrees)))
    1026				calib = D47calib(
    1027					samples = [], T = [], sT = [], D47 = [], sD47 = [],
    1028					degrees = degrees, bfp = bfp, bfp_CM = bfp_CM,
    1029					)
    1030
    1031		### READ INPUT STRINGS
    1032		if input == '-':
    1033			data = [[c.strip() for c in l.strip().split(delim_in)] for l in sys.stdin]
    1034		else:
    1035			with open(input) as f:
    1036				data = [[c.strip() for c in l.strip().split(delim_in)] for l in f.readlines()]
    1037
    1038		if include_samples == 'all':
    1039			samples_to_include = []
    1040		else:
    1041			with open(include_samples) as f:
    1042				samples_to_include = [l.strip() for l in f.readlines()]
    1043
    1044		if exclude_samples == 'none':
    1045			samples_to_exclude = []
    1046		else:
    1047			with open(exclude_samples) as f:
    1048				samples_to_exclude = [l.strip() for l in f.readlines()]
    1049
    1050		if len(samples_to_include) > 0 or len(samples_to_exclude) > 0:
    1051			try:
    1052				k = data[0].index('Sample')
    1053			except ValueError:
    1054				raise KeyError("When using options --include-samples or --exclude-samples, the input file must have a column labeled 'Sample'.")
    1055
    1056			if len(samples_to_include) > 0:
    1057				data = [data[0]] + [l for l in data[1:] if l[k] in samples_to_include]
    1058			data = [data[0]] + [l for l in data[1:] if l[k] not in samples_to_exclude]
    1059
    1060		### FIND FIRST DATA COLUMN
    1061		k = 0
    1062		while data[0][k] not in ['T', 'D47']:
    1063			k += 1
    1064			if k == len(data[0]):
    1065				raise KeyError("None of the input column headers are 'T' or 'D47'.")
    1066		data_out = [l[:k] for l in data]
    1067		data = [l[k:] for l in data]
    1068
    1069		### READ INPUT FIELDS
    1070		fields = data[0]
    1071
    1072		### CHECK FOR UNSUPPORTED FIELD COMBINATIONS
    1073		if fields not in [
    1074			['T'],
    1075			['T', 'T_SE'],
    1076			['T', 'T_covar'],
    1077			['T', 'T_SE', 'T_correl'],
    1078			['D47'],
    1079			['D47', 'D47_SE'],
    1080			['D47', 'D47_covar'],
    1081			['D47', 'D47_SE', 'D47_correl'],
    1082			]:
    1083			raise KeyError("There is a problem with the combination of field names provided as input.")
    1084
    1085		### BOOK-KEEPING
    1086		infield = fields[0]
    1087		X_precision = {'T': T_precision, 'D47': D47_precision}[infield]
    1088		outfield = {'T': 'D47', 'D47': 'T'}[infield]
    1089		Y_precision = {'T': T_precision, 'D47': D47_precision}[outfield]
    1090		N = len(data)-1
    1091
    1092		### READ INPUT DATA, ALSO SAVING ITS ORIGINAL STRINGS
    1093		X_str = [l[0] for l in data[1:]]
    1094		X = _np.array(X_str, dtype = float)
    1095
    1096		if len(fields) == 1:
    1097			X_SE = X*0
    1098			X_correl = _np.eye(N)
    1099			X_covar = _np.zeros((N, N))
    1100			X_SE_str = [f'{c:.{X_precision}f}' for c in X_SE]
    1101			X_correl_str = [[f'{c:.{correl_precision}f}' for c in l] for l in X_correl]
    1102			X_covar_str = [[f'{c:.{covar_precision}e}' for c in l] for l in X_covar]
    1103		if len(fields) == 2:
    1104			if fields[1].endswith('_SE'):
    1105				X_SE_str = [l[1] for l in data[1:]]
    1106				X_SE = _np.array(X_SE_str, dtype = float)
    1107				X_covar = _np.diag(X_SE**2)
    1108				X_covar_str = [[f'{c:.{covar_precision}e}' for c in l] for l in X_covar]
    1109			elif fields[1].endswith('_covar'):
    1110				X_covar_str = [l[1:N+1] for l in data[1:]]
    1111				X_covar = _np.array(X_covar_str, dtype = float)
    1112				X_SE = _np.diag(X_covar)**.5
    1113				X_SE_str = [f'{c:.{X_precision}f}' for c in X_SE]
    1114			X_correl = _np.diag(X_SE**-1) @ X_covar @ _np.diag(X_SE**-1)
    1115			X_correl_str = [[f'{c:.{correl_precision}f}' for c in l] for l in X_correl]
    1116		elif len(fields) == 3:
    1117			X_SE_str = [l[1] for l in data[1:]]
    1118			X_SE = _np.array(X_SE_str, dtype = float)
    1119			X_correl_str = [l[2:N+2] for l in data[1:]]
    1120			X_correl = _np.array(X_correl_str, dtype = float)
    1121			X_covar = _np.diag(X_SE) @ X_correl @ _np.diag(X_SE)
    1122			X_covar_str = [[f'{c:.{covar_precision}e}' for c in l] for l in X_covar]
    1123
    1124		### COMPUTE OUTPUT VALUES AND COVAR
    1125		kwargs = {infield: X, f's{infield}': X_covar}
    1126		Y, Y_covar_from_calib = calib.T47(**kwargs, error_from = 'calib', return_covar = True)
    1127		Y, Y_covar_from_input = calib.T47(**kwargs, error_from = f's{infield}', return_covar = True)
    1128		Y, Y_covar_from_both = calib.T47(**kwargs, error_from = 'both', return_covar = True)
    1129
    1130		Y_SE_from_calib = _np.diag(Y_covar_from_calib)**.5
    1131		Y_SE_from_input = _np.diag(Y_covar_from_input)**.5
    1132		Y_SE_from_both = _np.diag(Y_covar_from_both)**.5
    1133
    1134		if (Y_SE_from_calib**2).min():
    1135			Y_correl_from_calib = _np.diag(Y_SE_from_calib**-1) @ Y_covar_from_calib @ _np.diag(Y_SE_from_calib**-1)
    1136		else:
    1137			Y_correl_from_calib = _np.eye(N)
    1138
    1139		if (Y_SE_from_input**2).min():
    1140			Y_correl_from_input = _np.diag(Y_SE_from_input**-1) @ Y_covar_from_input @ _np.diag(Y_SE_from_input**-1)
    1141		else:
    1142			Y_correl_from_input = _np.eye(N)
    1143
    1144		if (Y_SE_from_both**2).min():
    1145			Y_correl_from_both = _np.diag(Y_SE_from_both**-1) @ Y_covar_from_both @ _np.diag(Y_SE_from_both**-1)
    1146		else:
    1147			Y_correl_from_both = _np.eye(N)
    1148
    1149		### BUILD Y STRINGS
    1150		Y_str = [f'{y:.{Y_precision}f}' for y in Y]
    1151
    1152		Y_SE_from_calib_str = [f'{sy:.{Y_precision}f}' for sy in Y_SE_from_calib]
    1153		Y_SE_from_input_str = [f'{sy:.{Y_precision}f}' for sy in Y_SE_from_input]
    1154		Y_SE_from_both_str = [f'{sy:.{Y_precision}f}' for sy in Y_SE_from_both]
    1155
    1156		Y_covar_from_calib_str = [[f'{c:.{covar_precision}e}' for c in l] for l in Y_covar_from_calib]
    1157		Y_covar_from_input_str = [[f'{c:.{covar_precision}e}' for c in l] for l in Y_covar_from_input]
    1158		Y_covar_from_both_str = [[f'{c:.{covar_precision}e}' for c in l] for l in Y_covar_from_both]
    1159
    1160		Y_correl_from_calib_str = [[f'{c:.{correl_precision}f}' for c in l] for l in Y_correl_from_calib]
    1161		Y_correl_from_input_str = [[f'{c:.{correl_precision}f}' for c in l] for l in Y_correl_from_input]
    1162		Y_correl_from_both_str = [[f'{c:.{correl_precision}f}' for c in l] for l in Y_correl_from_both]
    1163
    1164		### ADD SE COLUMN TO INPUT
    1165		if f'{infield}_covar' in fields:
    1166			fields.insert(1, f'{infield}_SE')
    1167
    1168		### ADD X COLUMNS TO OUTPUT DATA
    1169		data_out[0] += [infield]
    1170		for k in range(N):
    1171			data_out[k+1] += [X_str[k]]
    1172		for f in fields[1:]:
    1173			if f.endswith('_SE'):
    1174				data_out[0] += [f]
    1175				for k in range(N):
    1176					data_out[k+1] += [X_SE_str[k]]
    1177			if f.endswith('_covar') or f.endswith('_correl'):
    1178				if not ignore_correl:
    1179					data_out[0] += [f] + ['' for _ in range(N-1)]
    1180					for k in range(N):
    1181						data_out[k+1] += (X_covar_str if f.endswith('_covar') else X_correl_str)[k][:]
    1182
    1183		### ADD Y COLUMNS TO OUTPUT DATA
    1184		data_out[0] += [outfield]
    1185		for k in range(N):
    1186			data_out[k+1] += [Y_str[k]]
    1187
    1188		data_out[0] += [f'{outfield}_SE_from_calib']
    1189		for k in range(N):
    1190			data_out[k+1] += [Y_SE_from_calib_str[k]]
    1191		if not ignore_correl:
    1192			if return_covar:
    1193				data_out[0] += [f'{outfield}_covar_from_calib'] + ['' for _ in range(N-1)]
    1194				for k in range(N):
    1195					data_out[k+1] += Y_covar_from_calib_str[k]
    1196			else:
    1197				data_out[0] += [f'{outfield}_correl_from_calib'] + ['' for _ in range(N-1)]
    1198				for k in range(N):
    1199					data_out[k+1] += Y_correl_from_calib_str[k]
    1200
    1201		data_out[0] += [f'{outfield}_SE_from_input']
    1202		for k in range(N):
    1203			data_out[k+1] += [Y_SE_from_input_str[k]]
    1204		if not ignore_correl:
    1205			if return_covar:
    1206				data_out[0] += [f'{outfield}_covar_from_input'] + ['' for _ in range(N-1)]
    1207				for k in range(N):
    1208					data_out[k+1] += Y_covar_from_input_str[k]
    1209			else:
    1210				data_out[0] += [f'{outfield}_correl_from_input'] + ['' for _ in range(N-1)]
    1211				for k in range(N):
    1212					data_out[k+1] += Y_correl_from_input_str[k]
    1213
    1214		data_out[0] += [f'{outfield}_SE_from_both']
    1215		for k in range(N):
    1216			data_out[k+1] += [Y_SE_from_both_str[k]]
    1217		if not ignore_correl:
    1218			if return_covar:
    1219				data_out[0] += [f'{outfield}_covar_from_both'] + ['' for _ in range(N-1)]
    1220				for k in range(N):
    1221					data_out[k+1] += Y_covar_from_both_str[k]
    1222			else:
    1223				data_out[0] += [f'{outfield}_correl_from_both'] + ['' for _ in range(N-1)]
    1224				for k in range(N):
    1225					data_out[k+1] += Y_correl_from_both_str[k]
    1226
    1227
    1228		### PRINT OUTPUT TO STDOUT OR SAVE IT TO FILE
    1229		if delim_out in '<>':
    1230			lengths = [max([len(data_out[j][k]) for j in range(len(data_out))]) for k in range(len(data_out[0]))]
    1231
    1232			txt = ''
    1233			for l in data_out:
    1234				for k in range(len(l)):
    1235					if k > 0:
    1236						txt += '  '
    1237					txt += f'{l[k]:{delim_out}{lengths[k]}s}'
    1238				txt += '\n'
    1239
    1240			txt = txt[:-1]
    1241
    1242		else:
    1243			txt = '\n'.join([delim_out.join(l) for l in data_out])
    1244
    1245		if outfile == 'none':
    1246			print(txt)
    1247		else:
    1248			with open(outfile, 'w') as f:
    1249				f.write(txt)
    1250
    1251	def __cli():
    1252		app()
    1253
    1254except NameError:
    1255	pass

class D47calib(ogls.InverseTPolynomial): View Source

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

Δ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

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

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

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

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

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

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

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

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

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

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

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

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

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