D47calib

Generate, combine, display and apply Δ47 calibrations

This library provides support for:

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

1. Calibrations included

D47calib provides the following pre-built calibrations:

breitenbach_2018:

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

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

peral_2018:

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

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

jautzy_2020:

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

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

anderson_2021_mit:

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

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

anderson_2021_lsce:

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

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

fiebig_2021:

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

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

huyghe_2022:

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

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

devils_laghetto_2023:

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

ogls_2023:

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

2. Command-line interface

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

2.1 Simple examples

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

D47
0.567

Then process it:

D47calib input.csv

This prints out:

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

2.1.1 Adding D47 measurement uncertainties

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

D47,D47_SE
0.567,0.008

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

Sample   D47    D47_SE
FOO-1  0.567   0.008

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

D47calib -i ' ' input.csv

This yields:

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

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

2.1.2 Converting more than one measurement

Let's add lines to our input file:

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

Which yields:

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

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

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

2.1.3 Accounting for correlations in D47 errors

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

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

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

This yields:

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

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

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

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

This will create the following output.csv file:

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

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

2.1.4 Converting from T to D47

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

T   T_SE
0    0.5
10   1.0
20   2.0

Yields:

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

2.2 Integration with D47crunch

Starting with the following input file rawdata.csv:

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

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

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

With the contents of output.csv being:

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

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

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

2.3 Further customizing the CLI

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

2.3.1 Using covariance instead of correlation matrix as input

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

2.3.2 Reporting covariance instead of correlation matrices in the output

Use the --return-covar option.

2.3.3 Reporting neither covariances nor correlations in the output

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

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

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

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

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

2.3.5 Changing the numerical precision of the output

This is controlled by the following options:

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

2.3.6 Using a different Δ47 calibration

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

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

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

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

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

degree    coef
0       0.1741
1      -17.889
2        42614

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


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

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

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

Parameters

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

Notable attributes

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

Examples

A very simple example:

from D47calib import D47calib

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

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

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

Should yield:

T = 11.7
sT = 1.9

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

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

Parameters

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

Returns

  • an matplotlib.axes.Axes instance

Examples

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

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

This should result in something like this:

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

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

calib.xpower = 4

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

This should result in something like this:

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

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

Parameters

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

Returns

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

Example

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

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

This should result in something like this:

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

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

Parameters

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

Returns

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

Example

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

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

This should result in something like this:

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

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

Parameters

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

Returns

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

Example

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

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

This should result in something like this:

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

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

Parameters

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

Returns

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

Example

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

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

This should result in something like this:

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

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

Parameters

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

Returns

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

Example

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

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

This should result in something like this:

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

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

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

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

Arguments:

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

Returns (with D47 input):

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

Returns (with T input):

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

Example

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

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

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

This should result in something like this:

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

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

Arguments

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

Example

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

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

This should result in something like this:

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

Write calibration data to a csv file.

Parameters

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

Example

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

This should result in something like this (link):

ID Sample T SE_T D47 SE_D47 D47correl_1 D47correl_2 D47correl_3 D47correl_4 D47correl_5 D47correl_6 D47correl_7
1 Ad -1.80 0.50 0.6893 0.0060 1 0.048049 0.028770 0.544016 0.093188 0.020880 0.471516
2 BDV-S 18.70 0.75 0.6121 0.0049 0.048049 1 0.650474 0.053281 0.011132 0.002494 0.050651
3 BDV-W 11.01 1.00 0.6349 0.0052 0.028770 0.650474 1 0.031903 0.006666 0.001494 0.030328
4 PY 13.44 0.06 0.6397 0.0049 0.544016 0.053281 0.031903 1 0.104392 0.023391 0.513257
5 TES-S 22.50 2.10 0.5972 0.0053 0.093188 0.011132 0.006666 0.104392 1 0.275629 0.100150
6 TES-W 12.23 1.00 0.6329 0.0102 0.020880 0.002494 0.001494 0.023391 0.275629 1 0.022440
7 TW 26.80 0.85 0.6001 0.0048 0.471516 0.050651 0.030328 0.513257 0.100150 0.022440 1
def export(self, name, filename):
781	def export(self, name, filename):
782		"""
783		Save `D47calib` object as an importable file.
784		
785		### Parameters
786		
787		+ **name**:
788		The name of the variable to export.
789		+ **filename**:
790		The filename to write to.
791		
792		### Example
793
794		````py
795		D47calib.anderson_2021_lsce.export('foo', 'bar.py')
796		````
797
798		This should result in a `bar.py` file with the following contents:
799		
800		````py
801		foo = D47calib(
802			samples = ['LGB-2', 'DVH-2'],
803			T = [7.9, 33.7],
804			D47 = [0.6485720997671647, 0.5695972909966959],
805			sT = [[0.04000000000000001, 0.0], [0.0, 0.04000000000000001]],
806			sD47 = [[8.72797097773764e-06, 2.951894073404263e-06], [2.9518940734042614e-06, 7.498611746762038e-06]],
807			description = 'Devils Hole & Laghetto Basso from Anderson et al. (2021), processed in I-CDES',
808			label = 'Slow-growing calcites from Anderson et al. (2021)',
809			color = (0, 0.5, 0),
810			degrees = [0, 2],
811			bfp = {'a0': 0.1583220210575451, 'a2': 38724.41371782721},
812			bfp_CM = [[0.00035908667755871876, -30.707016431538836], [-30.70701643153884, 2668091.396598919]],
813			chisq = 6.421311854486162e-27,
814			Nf = 0,
815			)
816		````
817		"""
818		with open(filename, 'w') as f:
819			f.write(f'''
820{name} = D47calib(
821	samples = {self.samples},
822	T = {list(self.T)},
823	D47 = {list(self.D47)},
824	sT = {[list(l) for l in self.sT]},
825	sD47 = {[list(l) for l in self.sD47]},
826	degrees = {self.degrees},
827	description = {repr(self.description)},
828	name = {repr(self.name)},
829	label = {repr(self.label)},
830	bfp = {self.bfp},
831	bfp_CM = {[list(l) for l in self.bfp_CM]},
832	chisq = {self.chisq},
833	cholesky_residuals = {list(self.cholesky_residuals)},
834	aic = {self.aic},
835	bic = {self.bic},
836	ks_pvalue = {self.ks_pvalue},
837	)
838''')

Save D47calib object as an importable file.

Parameters

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

Example

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

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

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

Combine data from several D47calib instances.

Parameters

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

For example, the ogls_2023 calibration is computed with:

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

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

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

Example

The devils_laghetto_2023 calibration is computed using the following code:

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

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

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