D47calib
Generate, combine, display and apply Δ47 calibrations
This library provides support for:
- computing Δ47 calibrations by applying OGLS regression to sets of (T, Δ47) observations
- combining Δ47 datasets to produce a combined calibration
- various methods useful for creating Δ47 calibration plots
- Using Δ47 calibrations to convert between T and Δ47, keeping track of covariance between inputs
and/or uncertainties/covariance originating from calibration uncertainties. This may be done within
Python code or by using a simple command-line interface (e.g.,
D47calib input.csv > output.csv
).
1. Installation
This should do the trick:
pip install D47calib
Alternatively:
- download or clone the source from https://github.com/mdaeron/D47calib
- chose one of one of the following options:
- copy/move the
/src/D47calib
directory to somewhere in your Python path - copy/move the
/src/D47calib
directory to your current working directory - copy/move the
/src/D47calib
directory to any other location (e.g.,/foo/bar
) and include the following code snippet in your scripts:
- copy/move the
import sys
sys.path.append('/foo/bar')
I you don't install from pip, you will probably need to install the requirements listed in pyproject.toml
.
2. Calibrations included
D47calib
provides the following pre-built calibrations. All code and raw data used to compute these calibrations can be found in the build_calibs
directory.
breitenbach_2018
:
Cave pearls analyzed by Breitenbach et al. (2018).
Raw data were obtained from the original study’s supplementary information. The original publication processed data according to two sessions, each 4-5 months long, separated by 2 months. After reprocessing the original raw data using D47crunch, visual inspection of the standardization residuals defined revealed the presence of substantial drifts in both sessions. We thus assigned modified session boundaries defining four continuous measurement periods separated by 21 to 52 days, with new session lengths ranging from 24 to 80 days. The original data was not modified in any other way. Formation temperatures are from table 1 of the original study. We assigned arbitrary 95 % uncertainties of ±1 °C, which seem reasonable for cave environments.
peral_2018
:
Planktic foraminifera analyzed by Peral et al. (2018), reprocessed by Daëron & Gray (2023).
Peral et al. [2018] reported Δ47 values of foraminifera from core-tops, both planktic and benthic, whose calcification temperature estimates were recently reassessed by Daëron & Gray [2023]. Here we only consider Peral et al.’s planktic data, excluding two benthic samples (cf Daëron & Gray for reasons why we only consider planktic samples for now). In our reprocessing, as in the original study, “samples” are defined by default as a unique combination of core site, species, and size fraction. Δ47 values are then standardized in the usual way, before using D47crunch.combine_samples()
to combine all size fractions with the same core and species, except for G. inflata samples (cf Daëron & Gray and accompanying GitHub repository). By properly accounting for analytical error covariance between the Δ47 values to combine, this two-step approach avoids underestimating the final standardization errors. This
jautzy_2020
:
Synthetic calcites analyzed by Jautzy et al. (2020).
Jautzy et al. reported data from a continuous period spanning 10 months, and used a moving-window approach to standardize their measurements. We assigned sessions defined, whenever possible, as periods of one or more complete weeks enclosing one of more unknown sample analyses. The resulting Δ47 residuals, on the order of 40 ppm (1SD), do not display evidence of instrumental drift. Formation temperatures are from table S2 of the original study. We assigned arbitrary 95 % uncertainties of ±1 °C, which seem reasonable for laboratory experiments.
anderson_2021_mit
:
Various natural and synthetic carbonates analyzed at MIT by Anderson et al. (2021).
Raw IRMS data and temperature constraints were obtained from the original study’s supple- mentary information (tables S01 and S02). When reprocesseded the IRMS data we made no changes to the session defintions, but we excluded sessions 5 and 25 because they did not include any unknown sample analyses.
anderson_2021_lsce
:
Slow-growing mammillary calcite from Devils Hole and Laghetto Basso analyzed at LSCE by Anderson et al. (2021).
Raw IRMS data is from the original study’s supplementary information (SI-S02). Temperature contraints are from table 1 in Daëron et al. (2019).
fiebig_2021
:
Inorganic calcites analyzed by Fiebig et al. (2021).
Temperature contraints are duplicated from the earlier publications where the corresponding samples were first described [Daëron et al., 2019; Jautzy et al., 2020; Anderson et al., 2021]. Raw IRMS data and were obtained from the original study’s supplementary information, and processed as described by Fiebig et al. [2021], jointly using (a) heated and 25 °C-equilibrated CO2 to constrain the scrambling effect and compositional nonlinearity associated with each session, and (b) ETH-1 and ETH-2 reference materials to anchor unknown samples to the I-CDES scale.
huyghe_2022
:
Marine calcitic bivalves analyzed by Huyghe et al. (2022).
Huyghe et al. reported Δ47 values of modern calcitic bivalves collected from localities with good environmental constraints. As was done in the original publication, different bivalve individuals were initially treated as distinct analytical samples. In some sites with strong seasonality, individuals were sub-sampled into winter-calcified a summer-calcified fractions. Δ47 values were then standardized in the usual way, before using D47crunch.combine_samples()
method to combine all samples from the same locality. Calcification temperature estimates are from the original study.
devils_laghetto_2023
:
Combined data set of slow-growing mammillary calcite from Devils Hole and Laghetto Basso, analyzed both at LSCE by Anderson et al. (2021) and at GU by Fiebig et al. (2021).
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).
3. Command-line interface
D47calib
also provides a command-line interface (CLI) for converting between Δ47 and temperature values, computing uncertainties for each computed value (and how these uncertainties are correlated with each other) from different sources (from calibration errors alone, from measurement errors alone, and from both). The computed uncertainties are provided as standard errors, correlation matrix and/or covariance matrix. Input and output files may be comma-separated, tab-separated, or printed out as visually aligned data columns.
3.1 Simple examples
Start with the simplest input file possible (here named input.csv
):
D47
0.567
Then process it:
D47calib input.csv
This prints out:
D47 T T_SE_from_calib T_correl_from_calib T_SE_from_input T_correl_from_input T_SE_from_both T_correl_from_both
0.567 34.20 0.38 1.000 0.00 1.000 0.38 1.000
T
is the temperature corresponding to aD47
value of 0.567 ‰ according to the default calibration (ogls_2023
).T_SE_from_calib
is the standard error onT
from the calibration uncertaintyT_correl_from_calib
is the correlation matrix for theT_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 onT
from the measurement uncertainties onD47
. Because these are not specified here,T_SE_from_input
is equal to zero.T_correl_from_input
is, predictably, the correlation matrix for theT_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 onT
obtained by combining the two previously considered sources of uncertainties.T_correl_from_both
is what you expect it to be if you've been reading so far. Can you guess why it is a 1-by-1 matrix with a single value of one?
3.1.1 Adding D47
measurement uncertainties
This can be done by adding a column to input.csv
:
D47,D47_SE
0.567,0.008
Because this is not very human-friendly, we'll replace the comma separators by whitespace. We'll also add a column listing sample names:
Sample D47 D47_SE
FOO-1 0.567 0.008
Then process it. We're adding an option (-i ' '
, or --delimiter-in ' '
) specifying that we're no longer using commas but whitespaces as delimiters:
D47calib -i ' ' input.csv
This yields:
Sample D47 D47_SE T T_SE_from_calib T_correl_from_calib T_SE_from_input T_correl_from_input T_SE_from_both T_correl_from_both
FOO-1 0.567 0.008 34.20 0.38 1.000 2.91 1.000 2.94 1.000
You can see that T_SE_from_input
is now much larger than T_SE_from_calib
, and that the combined T_SE_from_both
is equal to the quadratic sum of T_SE_from_calib
and T_SE_from_input
.
3.1.2 Converting more than one measurement
Let's add lines to our input file:
Sample D47 D47_SE
FOO-1 0.567 0.008
BAR-2 0.575 0.009
BAZ-3 0.582 0.007
Which yields:
Sample D47 D47_SE T T_SE_from_calib T_correl_from_calib T_SE_from_input T_correl_from_input T_SE_from_both T_correl_from_both
FOO-1 0.567 0.008 34.20 0.38 1.000 0.996 0.987 2.91 1.000 0.000 0.000 2.94 1.000 0.015 0.019
BAR-2 0.575 0.009 31.33 0.37 0.996 1.000 0.997 3.18 0.000 1.000 0.000 3.21 0.015 1.000 0.017
BAZ-3 0.582 0.007 28.89 0.36 0.987 0.997 1.000 2.42 0.000 0.000 1.000 2.44 0.019 0.017 1.000
A notable change are the 3-by-3 correlation matrices, which tell us how the T
errors or these three measurements covary. T_correl_from_calib
shows that the T_SE_from_calib
errors are strongly correlated, because the three D47
values are close to each other. T_correl_from_input
indicates statistically independent T_SE_from_input
errors. This may be true or not, but it is the expected result because our input file does not include any information on how the D47_SE
errors may covary (see below how this additional info may be specified). Thus in this case D47calib
assumes that the D47
values are statistically independent (gentle reminder: this is often not the case, see below).
Note that because T_SE_from_input
errors are much larger than T_SE_from_calib
errors, the combined T_SE_from_both
errors are only weakly correlated, as seen in T_correl_from_both
.
3.1.3 Accounting for correlations in D47
errors
Because Δ47 measurements performed in the same analytical session(s) are not statistically independent, we may add to input.csv
a correlation matrix describing how D47_SE
errors covary.
One simple way to compute this correlation matrix is to use the save_D47_correl()
method from the D47crunch
library (PyPI, GitHub, Zenodo) described by Daëron (2021).
Sample D47 D47_SE D47_correl
FOO-1 0.567 0.008 1.00 0.25 0.25
BAR-2 0.575 0.009 0.25 1.00 0.25
BAZ-3 0.582 0.007 0.25 0.25 1.00
This yields:
Sample D47 D47_SE D47_correl T T_SE_from_calib T_correl_from_calib T_SE_from_input T_correl_from_input T_SE_from_both T_correl_from_both
FOO-1 0.567 0.008 1.00 0.25 0.25 34.20 0.38 1.000 0.996 0.987 2.91 1.000 0.250 0.250 2.94 1.000 0.261 0.264
BAR-2 0.575 0.009 0.25 1.00 0.25 31.33 0.37 0.996 1.000 0.997 3.18 0.250 1.000 0.250 3.21 0.261 1.000 0.263
BAZ-3 0.582 0.007 0.25 0.25 1.00 28.89 0.36 0.987 0.997 1.000 2.42 0.250 0.250 1.000 2.44 0.264 0.263 1.000
What changed ? We now have propagated D47_correl
into T_correl_from_input
, and this is accounted for in the combined correlation matrix T_correl_from_both
. Within the framework of our initial assumptions (multivariate Gaussian errors, first-order linear propagation of uncertainties...), this constitutes the “best” (or rather, the most “information-complete”) description of uncertainties constraining our final T
estimates.
With increasing number of measurements, these correlation matrices become quite large, so that it becomes useless to print them out visually. To facilitate using the output of D47calib
as an input to another piece of software, one may use the -j
or --delimiter-out
option to use machine-readable delimiters such as commas or tabs, and the '-o'
or --output-file
option to save the output as a file instead of printing it out:
D47calib -i ' ' -j ',' -o output.csv input.csv
This will create the following output.csv
file:
Sample,D47,D47_SE,D47_correl,,,T,T_SE_from_calib,T_correl_from_calib,,,T_SE_from_input,T_correl_from_input,,,T_SE_from_both,T_correl_from_both,,
FOO-1,0.567,0.008,1.00,0.25,0.25,34.20,0.38,1.000,0.996,0.987,2.91,1.000,0.250,0.250,2.94,1.000,0.261,0.264
BAR-2,0.575,0.009,0.25,1.00,0.25,31.33,0.37,0.996,1.000,0.997,3.18,0.250,1.000,0.250,3.21,0.261,1.000,0.263
BAZ-3,0.582,0.007,0.25,0.25,1.00,28.89,0.36,0.987,0.997,1.000,2.42,0.250,0.250,1.000,2.44,0.264,0.263,1.000
Hint for Mac users: Quick Look (or “spacebar preview”, i.e. what happens when you select a file in the Finder and press the spacebar once) provides you with a nice view of a csv file when you just want to check the results visually, as long as you use a comma delimiter.
3.1.4 Converting from T
to D47
Everything described above works in the other direction as well, without changing anything to the command-line instruction:
T T_SE
0 0.5
10 1.0
20 2.0
Yields:
T T_SE D47 D47_SE_from_calib D47_correl_from_calib D47_SE_from_input D47_correl_from_input D47_SE_from_both D47_correl_from_both
0 0.5 0.6798 0.0016 1.000 0.969 0.848 0.0020 1.000 0.000 0.000 0.0025 1.000 0.210 0.091
10 1.0 0.6424 0.0013 0.969 1.000 0.952 0.0035 0.000 1.000 0.000 0.0038 0.210 1.000 0.056
20 2.0 0.6090 0.0011 0.848 0.952 1.000 0.0063 0.000 0.000 1.000 0.0064 0.091 0.056 1.000
3.2 Integration with D47crunch
Starting with the following input file rawdata.csv
:
UID Sample Session d45 d46 d47 d48 d49
1 ETH-3 Session_01 5.726647 11.144602 16.634507 22.275401 28.306614
2 ETH-2 Session_01 -5.973121 -5.894178 -12.599303 -12.037594 -18.023381
3 BAZ-3 Session_01 -5.394955 7.938127 1.887702 15.671857 9.739724
4 FOO-1 Session_01 -0.848380 2.872996 1.535960 5.431873 4.665655
5 ETH-1 Session_01 6.050787 10.800497 16.232192 21.429453 27.780042
6 ETH-3 Session_01 5.755765 11.179326 16.689870 22.294132 28.306614
7 BAR-2 Session_01 -9.941731 10.985508 0.208381 21.832546 10.707292
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-3 Session_01 5.734551 11.043266 16.556578 22.005474 28.306614
11 ETH-1 Session_01 6.009875 10.711152 16.087994 21.275325 27.780042
12 ETH-2 Session_01 -5.981599 -6.011356 -12.728742 -12.335559 -18.023381
13 ETH-1 Session_02 6.029949 10.682183 16.080186 21.182471 27.780042
14 ETH-1 Session_02 5.995178 10.702934 16.073811 21.169324 27.780042
15 ETH-3 Session_02 5.717666 11.175023 16.654427 22.251497 28.306614
16 ETH-2 Session_02 -5.993596 -6.086738 -12.816033 -12.452397 -18.023381
17 FOO-1 Session_02 -0.835316 2.868058 1.539147 5.483628 4.665655
18 BAZ-3 Session_02 -5.337619 7.818571 1.818572 15.400966 9.739724
19 ETH-3 Session_02 5.756394 11.109906 16.635806 22.189179 28.306614
20 BAR-2 Session_02 -9.951732 10.964153 0.171443 21.779172 10.707292
21 ETH-1 Session_02 6.019913 10.773652 16.165645 21.309536 27.780042
22 ETH-2 Session_02 -5.952660 -5.887239 -12.582248 -12.081588 -18.023381
23 ETH-2 Session_02 -5.983050 -5.953794 -12.679809 -12.224144 -18.023381
24 ETH-3 Session_02 5.720443 11.187928 16.689130 22.308575 28.306614
The following script will read thart raw data, fully process it, convert the standardized output to temperatures, and save the final results to a file named output.csv
:
D47crunch rawdata.csv
D47calib -o output.csv -j '>' output/D47_correl.csv
With the contents of output.csv
being:
Sample D47 D47_SE D47_correl T T_SE_from_calib T_correl_from_calib T_SE_from_input T_correl_from_input T_SE_from_both T_correl_from_both
BAR-2 0.6777 0.0066 1.0000 0.3586 0.2798 0.51 0.41 1.000 0.731 -0.036 1.68 1.000 0.359 0.280 1.73 1.000 0.373 0.262
BAZ-3 0.5894 0.0060 0.3586 1.0000 0.2473 26.34 0.35 0.731 1.000 0.654 2.02 0.359 1.000 0.247 2.05 0.373 1.000 0.263
FOO-1 0.4873 0.0056 0.2798 0.2473 1.0000 68.21 0.69 -0.036 0.654 1.000 2.82 0.280 0.247 1.000 2.90 0.262 0.263 1.000
If a simpler output is required, just add the --ignore-correl
or -g
option to the second line above, which should yield:
Sample D47 D47_SE T T_SE_from_calib T_SE_from_input T_SE_from_both
BAR-2 0.6777 0.0066 0.51 0.41 1.68 1.73
BAZ-3 0.5894 0.0060 26.34 0.35 2.02 2.05
FOO-1 0.4873 0.0056 68.21 0.69 2.82 2.90
3.3 Further customizing the CLI
A complete list of options is provided by D47calib --help
.
3.3.1 Using covariance instead of correlation matrix as input
Just provide D47_covar
(or T_covar
when converting in the other direction) in the input file instead of D47_SE
and D47_correl
.
3.3.2 Reporting covariance instead of correlation matrices in the output
Use the --return-covar
option.
3.3.3 Reporting neither covariances nor correlations in the output
If you don't care about all this covariance nonsense, or just wish for an output that does't hurt your eyes, you can use the --ignore-correl
option. Standard errors will still be reported.
3.3.4 Excluding or only including certain lines (samples) from the input
To filter the samples (lines) to process using --exclude-samples
and --include-samples
, first add a Sample
column to the input data, assign a sample name to each line.
Then to exclude some samples, provide the --exclude-samples
option with the name of a file where each line is one sample to exclude.
To exclude all samples except those listed in a file, provide the --include-samples
option with the name of that file, where each line is one sample to include.
3.3.5 Changing the numerical precision of the output
This is controlled by the following options:
--T-precision
or-p
(default: 2): AllT
andT_SE_*
values--D47-precision
or-q
(default: 4): AllD47
andD47_SE_*
values--correl-precision
or-r
(default: 3): All*_correl_*
values--covar-precision
or-s
(default: 3): All*_covar_*
values
3.3.6 Using a different Δ47 calibration
You may use a different calibration than the default 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/install.md 14.. include:: ../../docpages/calibs.md 15.. include:: ../../docpages/cli.md 16 17* * * 18""" 19 20__author__ = 'Mathieu Daëron' 21__contact__ = 'daeron@lsce.ipsl.fr' 22__copyright__ = 'Copyright (c) 2023 Mathieu Daëron' 23__license__ = 'MIT License - https://opensource.org/licenses/MIT' 24# __docformat__ = "restructuredtext" 25__date__ = '2023-09-18' 26__version__ = '1.0' 27 28 29import typer, sys 30from typing_extensions import Annotated 31import ogls as _ogls 32import numpy as _np 33from scipy.linalg import block_diag as _block_diag 34from scipy.interpolate import interp1d as _interp1d 35from matplotlib import pyplot as _ppl 36 37typer.rich_utils.STYLE_HELPTEXT = '' 38 39class D47calib(_ogls.InverseTPolynomial): 40 """ 41 Δ47 calibration class based on OGLS regression 42 of Δ47 as a polynomial function of inverse T. 43 """ 44 45 def __init__(self, 46 samples, T, D47, 47 sT = None, 48 sD47 = None, 49 degrees = [0,2], 50 xpower = 2, 51 name = '', 52 label = '', 53 description = '', 54 **kwargs, 55 ): 56 """ 57 ### Parameters 58 59 + **samples**: a list of N sample names. 60 + **T**: a 1-D array (or array-like) of temperatures values (in degrees C), of size N. 61 + **D47**: a 1-D array (or array-like) of Δ47 values (in permil), of size N. 62 + **sT**: uncertainties on `T`. If specified as: 63 + a scalar: `sT` is treated as the standard error applicable to all `T` values; 64 + a 1-D array-like of size N: `sT` is treated as the standard errors of `T`; 65 + a 2-D array-like of size (N, N): `sT` is treated as the (co)variance matrix of `T`. 66 + **sD47**: uncertainties on `D47`. If specified as: 67 + a scalar: `sD47` is treated as the standard error applicable to all `D47` values; 68 + a 1-D array-like of size N: `sD47` is treated as the standard errors of `D47`; 69 + a 2-D array-like of size (N, N): `sD47` is treated as the (co)variance matrix of `D47`. 70 + **degrees**: degrees of the polynomial regression, e.g., `[0, 2]` or `[0, 1, 2, 3, 4]`. 71 + **name**: a human-readable, short name assigned to the calibration. 72 + **label**: a short description of the calibration, e.g., to be used in legends. 73 + **description**: a longer description, including relevant references/DOIs. 74 This is not necessary when `bfp` and `CM_bfp` are specified at instantiation time. 75 + **kwargs**: keyword arguments passed to the underlying `ogls.InverseTPolynomial()` call. 76 77 ### Notable attributes 78 79 + **N**: 80 The total number of observations (samples) in the calibration data. 81 + **samples**: 82 The list sample names. 83 + **T**: 84 1-D `ndarray` of temperatures in degrees C. 85 + **D47**: 86 1-D `ndarray` of Δ47 values in permil. 87 + **sT**: 88 2-D `ndarray` equal to the full (co)variance matrix for `T`. 89 + **D47**: 90 2-D `ndarray` equal to the full (co)variance matrix for `D47`. 91 + **xpower**: 92 By default, all `D47calib` graphical methods plot Δ47 as a function of 1/T<sup>2</sup>. 93 It is possible to change this behavior to use a different power of 1/T. 94 This is done by redefining the `xpower` attribute to a different, non-zero `int` value 95 (e.g. `foo.xpower = 1` to plot as a function of 1/T instead of 1/T<sup>2</sup>). 96 + **bfp**: 97 The best-fit parameters of the regression. 98 This is a `dict` with keys equal to the polynomial coefficients (see `bff` definition below) 99 + **bff()**: 100 The best-fit polynomial function of inverse T, defined as: 101 `bff(x) = sum(bfp[f'a{k}'] * x**k for k in degrees)` 102 Note that `bff` takes `x = 1/(T+273.15)` (instead of `T`) as input. 103 104 105 ### Examples 106 107 A very simple example: 108 109 ````py 110 .. include:: ../../code_examples/D47calib_init/example.py 111 ```` 112 113 Should yield: 114 115 ```` 116 .. include:: ../../code_examples/D47calib_init/output.txt 117 ```` 118 119 """ 120 121 self.samples = samples[:] 122 self.name = name 123 self.label = label 124 self.description = description 125 self.D47 = _np.asarray(D47, dtype = 'float') 126 self.N = self.D47.size 127 128 if sD47 is None: 129 self.sD47 = _np.zeros((self.N, self.N)) 130 else: 131 self.sD47 = _np.asarray(sD47) 132 if len(self.sD47.shape) == 1: 133 self.sD47 = _np.diag(self.sD47**2) 134 elif len(self.sD47.shape) == 0: 135 self.sD47 = _np.eye(self.D47.size) * self.sD47**2 136 137 _ogls.InverseTPolynomial.__init__(self, T=T, Y=D47, sT=sT, sY=sD47, degrees = degrees, xpower = xpower, **kwargs) 138 139 if self.bfp is None: 140 self.regress() 141 142 self._bff_deriv = lambda x: _np.array([k * self.bfp[f'a{k}'] * x**(k-1) for k in degrees if k > 0]).sum(axis = 0) 143 144 xi = _np.linspace(0,200**-1,1001) 145 self._inv_bff = _interp1d(self.bff(xi), xi) 146 147 self._D47_from_T = lambda T: self.bff((T+273.15)**-1) 148 self._T_from_D47 = lambda D47: self._inv_bff(D47)**-1 - 273.15 149 self._D47_from_T_deriv = lambda T: -(T+273.15)**-2 * self._bff_deriv((T+273.15)**-1) 150 self._T_from_D47_deriv = lambda D47: self._D47_from_T_deriv(self._T_from_D47(D47))**-1 151 152 def __repr__(self): 153 return f'<D47calib: {self.name}>' 154 155 def invT_xaxis(self, 156 xlabel = None, 157 Ti = [0,20,50,100,250,1000], 158 ): 159 """ 160 Create and return an `Axes` object with X values equal to 1/T<sup>2</sup>, 161 but labeled in degrees Celsius. 162 163 ### Parameters 164 165 + **xlabel**: 166 Custom label for X axis (`r'$1\,/\,T^2$'` by default) 167 + **Ti**: 168 Specify tick locations for X axis, in degrees C. 169 170 ### Returns 171 172 + an `matplotlib.axes.Axes` instance 173 174 ### Examples 175 176 ````py 177 .. include:: ../../code_examples/D47calib_invT_xaxis/example_1.py 178 ```` 179 180 This should result in something like this: 181 182 <img align="center" src="example_invT_xaxis_1.png"> 183 184 It is also possible to define the X axis using a different power of 1/T 185 by first redefining the `xpower` attribute: 186 187 ````py 188 .. include:: ../../code_examples/D47calib_invT_xaxis/example_2.py 189 ```` 190 191 This should result in something like this: 192 193 <img align="center" src="example_invT_xaxis_2.png"> 194 """ 195 if xlabel is None: 196 xlabel = f'$1\\,/\\,T^{self.xpower}$' if self.xpower > 1 else '1/T' 197 _ppl.xlabel(xlabel) 198 _ppl.xticks([(273.15 + t) ** -self.xpower for t in sorted(Ti)[::-1]]) 199 ax = _ppl.gca() 200 ax.set_xticklabels([f"${t}\\,$°C" for t in sorted(Ti)[::-1]]) 201 ax.tick_params(which="major") 202 203 return ax 204 205 206 def plot_data(self, label = False, **kwargs): 207 """ 208 Plot Δ47 value of each sample as a function of 1/T<sup>2</sup>. 209 210 ### Parameters 211 212 + **label**: 213 + If `label` is a string, use this string as `label` for the underlyig 214 `matplotlib.pyplot.plot()` call. 215 + If `label = True`, use the caller's `label` attribute instead. 216 + If `label = False`, no label is specified (default behavior). 217 + **kwargs**: 218 keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call. 219 220 ### Returns 221 222 + the return value(s) of the underlying `matplotlib.pyplot.plot()` call. 223 224 ### Example 225 226 ````py 227 from matplotlib import pyplot as ppl 228 from D47calib import huyghe_2022 as calib 229 230 fig = ppl.figure(figsize = (5,3)) 231 ppl.subplots_adjust(bottom = .25, left = .15) 232 calib.invT_xaxis(Ti = [0,10,25]) 233 calib.plot_data(label = True) 234 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 235 ppl.legend() 236 ppl.savefig('example_plot_data.png', dpi = 100) 237 ````` 238 239 This should result in something like this: 240 241 <img align="center" src="example_plot_data.png"> 242 """ 243# if 'mec' not in kwargs: 244# kwargs['mec'] = self.color 245 if label is not False: 246 kwargs['label'] = self.label if label is True else label 247 return _ogls.InverseTPolynomial.plot_data(self, **kwargs) 248 249 250 def plot_error_bars(self, **kwargs): 251 """ 252 Plot Δ47 error bars (±1.96 SE) of each sample as a function of 1/T<sup>2</sup>. 253 254 ### Parameters 255 256 + **kwargs**: 257 keyword arguments passed to the underlying `matplotlib.pyplot.errrobar()` call. 258 259 ### Returns 260 261 + the return value(s) of the underlying `matplotlib.pyplot.errorbar()` call. 262 263 ### Example 264 265 ````py 266 from matplotlib import pyplot as ppl 267 from D47calib import huyghe_2022 as calib 268 269 fig = ppl.figure(figsize = (5,3)) 270 ppl.subplots_adjust(bottom = .25, left = .15) 271 calib.invT_xaxis(Ti = [0,10,25]) 272 calib.plot_error_bars(alpha = .4) 273 calib.plot_data(label = True) 274 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 275 ppl.legend() 276 ppl.savefig('example_plot_error_bars.png', dpi = 100) 277 ````` 278 279 This should result in something like this: 280 281 <img align="center" src="example_plot_error_bars.png"> 282 """ 283# if 'ecolor' not in kwargs: 284# kwargs['ecolor'] = self.color 285 return _ogls.InverseTPolynomial.plot_error_bars(self, **kwargs) 286 287 288 def plot_error_ellipses(self, **kwargs): 289 """ 290 Plot Δ47 error ellipses (95 % confidence) of each sample as a function of 1/T<sup>2</sup>. 291 292 ### Parameters 293 294 + **kwargs**: 295 keyword arguments passed to the underlying `matplotlib.patches.Ellipse()` call. 296 297 ### Returns 298 299 + the return value(s) of the underlying `matplotlib.patches.Ellipse()` call. 300 301 ### Example 302 303 ````py 304 from matplotlib import pyplot as ppl 305 from D47calib import huyghe_2022 as calib 306 307 fig = ppl.figure(figsize = (5,3)) 308 ppl.subplots_adjust(bottom = .25, left = .15) 309 calib.invT_xaxis(Ti = [0,10,25]) 310 calib.plot_error_ellipses(alpha = .4) 311 calib.plot_data(label = True) 312 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 313 ppl.legend() 314 ppl.savefig('example_plot_error_ellipses.png', dpi = 100) 315 ````` 316 317 This should result in something like this: 318 319 <img align="center" src="example_plot_error_ellipses.png"> 320 """ 321# if 'ec' not in kwargs: 322# kwargs['ec'] = self.color 323 return _ogls.InverseTPolynomial.plot_error_ellipses(self, **kwargs) 324 325 326 def plot_bff(self, label = False, **kwargs): 327 """ 328 Plot best-fit regression of Δ47 as a function of 1/T<sup>2</sup>. 329 330 ### Parameters 331 332 + **label**: 333 + If `label` is a string, use this string as `label` for the underlyig 334 `matplotlib.pyplot.plot()` call. 335 + If `label = True`, use the caller's `label` attribute instead. 336 + If `label = False`, no label is specified (default behavior). 337 + **kwargs**: 338 keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call. 339 340 ### Returns 341 342 + the return value(s) of the underlying `matplotlib.pyplot.plot()` call. 343 344 ### Example 345 346 ````py 347 from matplotlib import pyplot as ppl 348 from D47calib import huyghe_2022 as calib 349 350 fig = ppl.figure(figsize = (5,3)) 351 ppl.subplots_adjust(bottom = .25, left = .15) 352 calib.invT_xaxis(Ti = [0,10,25]) 353 calib.plot_bff(label = True, dashes = (8,2,2,2)) 354 calib.plot_data() 355 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 356 ppl.legend() 357 ppl.savefig('example_plot_bff.png', dpi = 100) 358 ````` 359 360 This should result in something like this: 361 362 <img align="center" src="example_plot_bff.png"> 363 """ 364# if 'color' not in kwargs: 365# kwargs['color'] = self.color 366 if label is not False: 367 kwargs['label'] = self.label if label is True else label 368 return _ogls.InverseTPolynomial.plot_bff(self, **kwargs) 369 370 371 def plot_bff_ci(self, **kwargs): 372 """ 373 Plot 95 % confidence region for best-fit regression of Δ47 as a function of 1/T<sup>2</sup>. 374 375 ### Parameters 376 377 + **label**: 378 + **kwargs**: 379 keyword arguments passed to the underlying `matplotlib.pyplot.fill_between()` call. 380 381 ### Returns 382 383 + the return value(s) of the underlying `matplotlib.pyplot.fill_between()` call. 384 385 ### Example 386 387 ````py 388 from matplotlib import pyplot as ppl 389 from D47calib import huyghe_2022 as calib 390 391 fig = ppl.figure(figsize = (5,3)) 392 ppl.subplots_adjust(bottom = .25, left = .15) 393 calib.invT_xaxis(Ti = [0,10,25]) 394 calib.plot_bff_ci(alpha = .15) 395 calib.plot_bff(label = True, dashes = (8,2,2,2)) 396 calib.plot_data() 397 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 398 ppl.legend() 399 ppl.savefig('example_plot_bff_ci.png', dpi = 100) 400 ````` 401 402 This should result in something like this: 403 404 <img align="center" src="example_plot_bff_ci.png"> 405 """ 406# if 'color' not in kwargs: 407# kwargs['color'] = self.color 408 return _ogls.InverseTPolynomial.plot_bff_ci(self, **kwargs) 409 410 def T47(self, 411 D47 = None, 412 sD47 = None, 413 T=None, 414 sT = None, 415 error_from = 'both', 416 return_covar = False, 417 ): 418 ''' 419 When `D47` is input, computes corresponding T value(s). 420 `D47` input may be specified as a scalar, or as a 1-D array. 421 `T` output will then have the same type and size as `D47`. 422 423 When `T` is input, computes corresponding Δ47 value(s). 424 `T` input may be specified as a scalar, or as a 1-D array. 425 `D47` output will then have the same type and size as `T`. 426 427 Only one of either `D47` or `T` may be specified as input. 428 429 **Arguments:** 430 431 * `D47`: Δ47 value(s) to convert into temperature (`float` or 1-D array) 432 * `sD47`: Δ47 uncertainties, which may be: 433 - `None` (default) 434 - `float` or `int` (uniform standard error on `D47`) 435 - 1-D array (standard errors on `D47`) 436 - 2-D array (covariance matrix for `D47`) 437 * `T`: T value(s) to convert into Δ47 (`float` or 1-D array), in degrees C 438 * `sT`: T uncertainties, which may be: 439 - `None` (default) 440 - `float` or `int` (uniform standard error on `T`) 441 - 1-D array (standard errors on `T`) 442 - 2-D array (variance-covariance matrix for `T`) 443 * `error_from`: if set to `'both'` (default), returned errors take into account 444 input uncertainties (`sT` or `sD47`) as well as calibration uncertainties; 445 if set to `'calib'`, only calibration uncertainties are accounted for; 446 if set to `'sT'` or `'sD47'`, calibration uncertainties are ignored. 447 * `return_covar`: (False by default) whether to return the full covariance matrix 448 for returned `T` or `D47` values, otherwise return standard errors for the returned 449 `T` or `D47` values instead. 450 451 **Returns (with `D47` input):** 452 453 * `T`: temperature value(s) computed from `D47` 454 * `sT`: uncertainties on `T` value(s), whether as standard error(s) or covariance matrix 455 456 **Returns (with `T` input):** 457 458 * `D47`: Δ47 value(s) computed from `D47` 459 * `sD47`: uncertainties on `D47` value(s), whether as standard error(s) or covariance matrix 460 461 ### Example 462 463 ````py 464 import numpy as np 465 from matplotlib import pyplot as ppl 466 from D47calib import 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''') 839 840def combine_D47calibs(calibs, degrees = [0,2], same_T = []): 841 ''' 842 Combine data from several `D47calib` instances. 843 844 ### Parameters 845 846 + **calibs**: 847 A list of `D47calib` instances 848 + **degrees**: 849 The polynomial degrees of the combined regression. 850 + **same_T**: 851 Use this `list` to specify when samples from different calibrations are known/postulated 852 to have formed at the same temperature (e.g. `DVH-2` and `DHC2-8` from the `fiebig_2021` 853 and `anderson_2021_lsce` data sets). Each element of `same_T` is a `list` with the names 854 of two or more samples formed at the same temperature. 855 856 For example, the `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 918 919from ._calibs import * 920 921def _covar2correl(C): 922 SE = _np.diag(C)**.5 923 return SE, _np.diag(SE**-1) @ C @ _np.diag(SE**-1) 924 925try: 926 app = typer.Typer( 927 add_completion = False, 928 context_settings={'help_option_names': ['-h', '--help']}, 929 rich_markup_mode = 'rich', 930 ) 931 932 @app.command() 933 def _cli( 934 input: Annotated[str, typer.Argument(help = "Specify either the path of an input file or just '-' to read input from stdin")] = '-', 935 include_samples: Annotated[str, typer.Option('--include-samples', '-u', help = 'Only include samples listed in this file')] = 'all', 936 exclude_samples: Annotated[str, typer.Option('--exclude-samples', '-x', help = 'Exclude samples listed in this file')] = 'none', 937 outfile: Annotated[str, typer.Option('--output-file', '-o', help = 'Write output to this file instead of printing to stdout')] = 'none', 938 calib: Annotated[str, typer.Option('--calib', '-c', help = 'D47 calibration function to use')] = 'ogls_2023', 939 delim_in: Annotated[str, typer.Option('--delimiter-in', '-i', help = "Delimiter used in the input.")] = ',', 940 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", 941 T_precision: Annotated[int, typer.Option('--T-precision', '-p', help = 'Precision for T output')] = 2, 942 D47_precision: Annotated[int, typer.Option('--D47-precision', '-q', help = 'Precision for D47 output')] = 4, 943 correl_precision: Annotated[int, typer.Option('--correl-precision', '-r', help = 'Precision for correlation output')] = 3, 944 covar_precision: Annotated[int, typer.Option('--covar-precision', '-s', help = 'Precision for covariance output')] = 3, 945 return_covar: Annotated[bool, typer.Option('--return-covar', '-v', help = 'Output covariance matrix instead of correlation matrix')] = False, 946 ignore_correl: Annotated[bool, typer.Option('--ignore-correl', '-g', help = 'Only consider and report standard errors without correlations')] = False, 947 ): 948 """ 949[b]Purpose:[/b] 950 951Reads data from an input file, converts between T and D47 values, and print out the results. 952 953The 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>. 954 955The first line of the input file must be one of the following: 956 957- [b]Option 1:[/b] T 958- [b]Option 2:[/b] T<sep>T_SE 959- [b]Option 3:[/b] T<sep>T_SE<sep>T_correl 960- [b]Option 4:[/b] T<sep>T_covar 961- [b]Option 5:[/b] D47 962- [b]Option 6:[/b] D47<sep>D47_SE 963- [b]Option 7:[/b] D47<sep>D47_SE<sep>D47_correl 964- [b]Option 8:[/b] D47<sep>D47_covar 965 966The rest of the input must be any number of lines with float values corresponding to the fields in the first line, separated by <sep>. 967 968[bold]Example input file:[/bold] 969 970[italic]D47 D47_SE D47_correl[/italic] 971[italic]0.6324 0.0101 1.00 0.25 0.25[/italic] 972[italic]0.6281 0.0087 0.25 1.00 0.25[/italic] 973[italic]0.6385 0.0095 0.25 0.25 1.00[/italic] 974 975The corresponding D47 (options 1-4) or T (options 4-8) values are computed, along with standard errors and error correlations from calibration uncertainties. 976 977For 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. 978 979The example above will thus result in an output with the following fields: 980 981[italic]- D47[/italic] 982[italic]- D47_SE[/italic] 983[italic]- D47_correl[/italic] 984[italic]- T[/italic] 985[italic]- T_SE_from_calib[/italic] 986[italic]- T_correl_from_calib[/italic] 987[italic]- T_SE_from_input[/italic] 988[italic]- T_correl_from_input[/italic] 989[italic]- T_SE_from_both[/italic] 990[italic]- T_correl_from_both[/italic] 991 992Results may also be saved to a file using [bold]--output-file <filename>[/bold] or [bold]-o <filename>[/bold]. 993 994To 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. 995Then 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. 996To 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. 997""" 998 999 ### INCOMPATIBILITY BETWEEN --ignore-correl AND --return-covar 1000 if ignore_correl: 1001 return_covar = False 1002 1003 ### USE WHITESPACE AS INPUT DELIMITER 1004 if delim_in == ' ': 1005 delim_in = None 1006 1007 ### SMART SELECTION OF OUTPUT DELIMITER 1008 if delim_out == "',' when writing to output file, '>' when printing to stdout": 1009 if outfile == 'none': 1010 delim_out = '>' 1011 else: 1012 delim_out = ',' 1013 1014 ### CALIBRATION 1015 if calib in globals() and type(globals()[calib]) == D47calib: 1016 calib = globals()[calib] 1017 else: 1018 with open(calib) as f: 1019 calibdata = _np.array([[c.strip() for c in l.strip().split(delim_in)] for l in f.readlines()[1:]], dtype = float) 1020 1021 degrees = [int(d) for d in calibdata[:,0]] 1022 bfp = {f'a{k}': a for k,a in zip(degrees, calibdata[:,1])} 1023 bfp_CM = calibdata[:,2:] 1024 if bfp_CM.shape[0] != bfp_CM.shape[1]: 1025 bfp_CM = _np.zeros((len(degrees), len(degrees))) 1026 calib = D47calib( 1027 samples = [], T = [], sT = [], D47 = [], sD47 = [], 1028 degrees = degrees, bfp = bfp, bfp_CM = bfp_CM, 1029 ) 1030 1031 ### READ INPUT STRINGS 1032 if input == '-': 1033 data = [[c.strip() for c in l.strip().split(delim_in)] for l in sys.stdin] 1034 else: 1035 with open(input) as f: 1036 data = [[c.strip() for c in l.strip().split(delim_in)] for l in f.readlines()] 1037 1038 if include_samples == 'all': 1039 samples_to_include = [] 1040 else: 1041 with open(include_samples) as f: 1042 samples_to_include = [l.strip() for l in f.readlines()] 1043 1044 if exclude_samples == 'none': 1045 samples_to_exclude = [] 1046 else: 1047 with open(exclude_samples) as f: 1048 samples_to_exclude = [l.strip() for l in f.readlines()] 1049 1050 if len(samples_to_include) > 0 or len(samples_to_exclude) > 0: 1051 try: 1052 k = data[0].index('Sample') 1053 except ValueError: 1054 raise KeyError("When using options --include-samples or --exclude-samples, the input file must have a column labeled 'Sample'.") 1055 1056 if len(samples_to_include) > 0: 1057 data = [data[0]] + [l for l in data[1:] if l[k] in samples_to_include] 1058 data = [data[0]] + [l for l in data[1:] if l[k] not in samples_to_exclude] 1059 1060 ### FIND FIRST DATA COLUMN 1061 k = 0 1062 while data[0][k] not in ['T', 'D47']: 1063 k += 1 1064 if k == len(data[0]): 1065 raise KeyError("None of the input column headers are 'T' or 'D47'.") 1066 data_out = [l[:k] for l in data] 1067 data = [l[k:] for l in data] 1068 1069 ### READ INPUT FIELDS 1070 fields = data[0] 1071 1072 ### CHECK FOR UNSUPPORTED FIELD COMBINATIONS 1073 if fields not in [ 1074 ['T'], 1075 ['T', 'T_SE'], 1076 ['T', 'T_covar'], 1077 ['T', 'T_SE', 'T_correl'], 1078 ['D47'], 1079 ['D47', 'D47_SE'], 1080 ['D47', 'D47_covar'], 1081 ['D47', 'D47_SE', 'D47_correl'], 1082 ]: 1083 raise KeyError("There is a problem with the combination of field names provided as input.") 1084 1085 ### BOOK-KEEPING 1086 infield = fields[0] 1087 X_precision = {'T': T_precision, 'D47': D47_precision}[infield] 1088 outfield = {'T': 'D47', 'D47': 'T'}[infield] 1089 Y_precision = {'T': T_precision, 'D47': D47_precision}[outfield] 1090 N = len(data)-1 1091 1092 ### READ INPUT DATA, ALSO SAVING ITS ORIGINAL STRINGS 1093 X_str = [l[0] for l in data[1:]] 1094 X = _np.array(X_str, dtype = float) 1095 1096 if len(fields) == 1: 1097 X_SE = X*0 1098 X_correl = _np.eye(N) 1099 X_covar = _np.zeros((N, N)) 1100 X_SE_str = [f'{c:.{X_precision}f}' for c in X_SE] 1101 X_correl_str = [[f'{c:.{correl_precision}f}' for c in l] for l in X_correl] 1102 X_covar_str = [[f'{c:.{covar_precision}e}' for c in l] for l in X_covar] 1103 if len(fields) == 2: 1104 if fields[1].endswith('_SE'): 1105 X_SE_str = [l[1] for l in data[1:]] 1106 X_SE = _np.array(X_SE_str, dtype = float) 1107 X_covar = _np.diag(X_SE**2) 1108 X_covar_str = [[f'{c:.{covar_precision}e}' for c in l] for l in X_covar] 1109 elif fields[1].endswith('_covar'): 1110 X_covar_str = [l[1:N+1] for l in data[1:]] 1111 X_covar = _np.array(X_covar_str, dtype = float) 1112 X_SE = _np.diag(X_covar)**.5 1113 X_SE_str = [f'{c:.{X_precision}f}' for c in X_SE] 1114 X_correl = _np.diag(X_SE**-1) @ X_covar @ _np.diag(X_SE**-1) 1115 X_correl_str = [[f'{c:.{correl_precision}f}' for c in l] for l in X_correl] 1116 elif len(fields) == 3: 1117 X_SE_str = [l[1] for l in data[1:]] 1118 X_SE = _np.array(X_SE_str, dtype = float) 1119 X_correl_str = [l[2:N+2] for l in data[1:]] 1120 X_correl = _np.array(X_correl_str, dtype = float) 1121 X_covar = _np.diag(X_SE) @ X_correl @ _np.diag(X_SE) 1122 X_covar_str = [[f'{c:.{covar_precision}e}' for c in l] for l in X_covar] 1123 1124 ### COMPUTE OUTPUT VALUES AND COVAR 1125 kwargs = {infield: X, f's{infield}': X_covar} 1126 Y, Y_covar_from_calib = calib.T47(**kwargs, error_from = 'calib', return_covar = True) 1127 Y, Y_covar_from_input = calib.T47(**kwargs, error_from = f's{infield}', return_covar = True) 1128 Y, Y_covar_from_both = calib.T47(**kwargs, error_from = 'both', return_covar = True) 1129 1130 Y_SE_from_calib = _np.diag(Y_covar_from_calib)**.5 1131 Y_SE_from_input = _np.diag(Y_covar_from_input)**.5 1132 Y_SE_from_both = _np.diag(Y_covar_from_both)**.5 1133 1134 if (Y_SE_from_calib**2).min(): 1135 Y_correl_from_calib = _np.diag(Y_SE_from_calib**-1) @ Y_covar_from_calib @ _np.diag(Y_SE_from_calib**-1) 1136 else: 1137 Y_correl_from_calib = _np.eye(N) 1138 1139 if (Y_SE_from_input**2).min(): 1140 Y_correl_from_input = _np.diag(Y_SE_from_input**-1) @ Y_covar_from_input @ _np.diag(Y_SE_from_input**-1) 1141 else: 1142 Y_correl_from_input = _np.eye(N) 1143 1144 if (Y_SE_from_both**2).min(): 1145 Y_correl_from_both = _np.diag(Y_SE_from_both**-1) @ Y_covar_from_both @ _np.diag(Y_SE_from_both**-1) 1146 else: 1147 Y_correl_from_both = _np.eye(N) 1148 1149 ### BUILD Y STRINGS 1150 Y_str = [f'{y:.{Y_precision}f}' for y in Y] 1151 1152 Y_SE_from_calib_str = [f'{sy:.{Y_precision}f}' for sy in Y_SE_from_calib] 1153 Y_SE_from_input_str = [f'{sy:.{Y_precision}f}' for sy in Y_SE_from_input] 1154 Y_SE_from_both_str = [f'{sy:.{Y_precision}f}' for sy in Y_SE_from_both] 1155 1156 Y_covar_from_calib_str = [[f'{c:.{covar_precision}e}' for c in l] for l in Y_covar_from_calib] 1157 Y_covar_from_input_str = [[f'{c:.{covar_precision}e}' for c in l] for l in Y_covar_from_input] 1158 Y_covar_from_both_str = [[f'{c:.{covar_precision}e}' for c in l] for l in Y_covar_from_both] 1159 1160 Y_correl_from_calib_str = [[f'{c:.{correl_precision}f}' for c in l] for l in Y_correl_from_calib] 1161 Y_correl_from_input_str = [[f'{c:.{correl_precision}f}' for c in l] for l in Y_correl_from_input] 1162 Y_correl_from_both_str = [[f'{c:.{correl_precision}f}' for c in l] for l in Y_correl_from_both] 1163 1164 ### ADD SE COLUMN TO INPUT 1165 if f'{infield}_covar' in fields: 1166 fields.insert(1, f'{infield}_SE') 1167 1168 ### ADD X COLUMNS TO OUTPUT DATA 1169 data_out[0] += [infield] 1170 for k in range(N): 1171 data_out[k+1] += [X_str[k]] 1172 for f in fields[1:]: 1173 if f.endswith('_SE'): 1174 data_out[0] += [f] 1175 for k in range(N): 1176 data_out[k+1] += [X_SE_str[k]] 1177 if f.endswith('_covar') or f.endswith('_correl'): 1178 if not ignore_correl: 1179 data_out[0] += [f] + ['' for _ in range(N-1)] 1180 for k in range(N): 1181 data_out[k+1] += (X_covar_str if f.endswith('_covar') else X_correl_str)[k][:] 1182 1183 ### ADD Y COLUMNS TO OUTPUT DATA 1184 data_out[0] += [outfield] 1185 for k in range(N): 1186 data_out[k+1] += [Y_str[k]] 1187 1188 data_out[0] += [f'{outfield}_SE_from_calib'] 1189 for k in range(N): 1190 data_out[k+1] += [Y_SE_from_calib_str[k]] 1191 if not ignore_correl: 1192 if return_covar: 1193 data_out[0] += [f'{outfield}_covar_from_calib'] + ['' for _ in range(N-1)] 1194 for k in range(N): 1195 data_out[k+1] += Y_covar_from_calib_str[k] 1196 else: 1197 data_out[0] += [f'{outfield}_correl_from_calib'] + ['' for _ in range(N-1)] 1198 for k in range(N): 1199 data_out[k+1] += Y_correl_from_calib_str[k] 1200 1201 data_out[0] += [f'{outfield}_SE_from_input'] 1202 for k in range(N): 1203 data_out[k+1] += [Y_SE_from_input_str[k]] 1204 if not ignore_correl: 1205 if return_covar: 1206 data_out[0] += [f'{outfield}_covar_from_input'] + ['' for _ in range(N-1)] 1207 for k in range(N): 1208 data_out[k+1] += Y_covar_from_input_str[k] 1209 else: 1210 data_out[0] += [f'{outfield}_correl_from_input'] + ['' for _ in range(N-1)] 1211 for k in range(N): 1212 data_out[k+1] += Y_correl_from_input_str[k] 1213 1214 data_out[0] += [f'{outfield}_SE_from_both'] 1215 for k in range(N): 1216 data_out[k+1] += [Y_SE_from_both_str[k]] 1217 if not ignore_correl: 1218 if return_covar: 1219 data_out[0] += [f'{outfield}_covar_from_both'] + ['' for _ in range(N-1)] 1220 for k in range(N): 1221 data_out[k+1] += Y_covar_from_both_str[k] 1222 else: 1223 data_out[0] += [f'{outfield}_correl_from_both'] + ['' for _ in range(N-1)] 1224 for k in range(N): 1225 data_out[k+1] += Y_correl_from_both_str[k] 1226 1227 1228 ### PRINT OUTPUT TO STDOUT OR SAVE IT TO FILE 1229 if delim_out in '<>': 1230 lengths = [max([len(data_out[j][k]) for j in range(len(data_out))]) for k in range(len(data_out[0]))] 1231 1232 txt = '' 1233 for l in data_out: 1234 for k in range(len(l)): 1235 if k > 0: 1236 txt += ' ' 1237 txt += f'{l[k]:{delim_out}{lengths[k]}s}' 1238 txt += '\n' 1239 1240 txt = txt[:-1] 1241 1242 else: 1243 txt = '\n'.join([delim_out.join(l) for l in data_out]) 1244 1245 if outfile == 'none': 1246 print(txt) 1247 else: 1248 with open(outfile, 'w') as f: 1249 f.write(txt) 1250 1251 def __cli(): 1252 app() 1253 1254except NameError: 1255 pass
40class D47calib(_ogls.InverseTPolynomial): 41 """ 42 Δ47 calibration class based on OGLS regression 43 of Δ47 as a polynomial function of inverse T. 44 """ 45 46 def __init__(self, 47 samples, T, D47, 48 sT = None, 49 sD47 = None, 50 degrees = [0,2], 51 xpower = 2, 52 name = '', 53 label = '', 54 description = '', 55 **kwargs, 56 ): 57 """ 58 ### Parameters 59 60 + **samples**: a list of N sample names. 61 + **T**: a 1-D array (or array-like) of temperatures values (in degrees C), of size N. 62 + **D47**: a 1-D array (or array-like) of Δ47 values (in permil), of size N. 63 + **sT**: uncertainties on `T`. If specified as: 64 + a scalar: `sT` is treated as the standard error applicable to all `T` values; 65 + a 1-D array-like of size N: `sT` is treated as the standard errors of `T`; 66 + a 2-D array-like of size (N, N): `sT` is treated as the (co)variance matrix of `T`. 67 + **sD47**: uncertainties on `D47`. If specified as: 68 + a scalar: `sD47` is treated as the standard error applicable to all `D47` values; 69 + a 1-D array-like of size N: `sD47` is treated as the standard errors of `D47`; 70 + a 2-D array-like of size (N, N): `sD47` is treated as the (co)variance matrix of `D47`. 71 + **degrees**: degrees of the polynomial regression, e.g., `[0, 2]` or `[0, 1, 2, 3, 4]`. 72 + **name**: a human-readable, short name assigned to the calibration. 73 + **label**: a short description of the calibration, e.g., to be used in legends. 74 + **description**: a longer description, including relevant references/DOIs. 75 This is not necessary when `bfp` and `CM_bfp` are specified at instantiation time. 76 + **kwargs**: keyword arguments passed to the underlying `ogls.InverseTPolynomial()` call. 77 78 ### Notable attributes 79 80 + **N**: 81 The total number of observations (samples) in the calibration data. 82 + **samples**: 83 The list sample names. 84 + **T**: 85 1-D `ndarray` of temperatures in degrees C. 86 + **D47**: 87 1-D `ndarray` of Δ47 values in permil. 88 + **sT**: 89 2-D `ndarray` equal to the full (co)variance matrix for `T`. 90 + **D47**: 91 2-D `ndarray` equal to the full (co)variance matrix for `D47`. 92 + **xpower**: 93 By default, all `D47calib` graphical methods plot Δ47 as a function of 1/T<sup>2</sup>. 94 It is possible to change this behavior to use a different power of 1/T. 95 This is done by redefining the `xpower` attribute to a different, non-zero `int` value 96 (e.g. `foo.xpower = 1` to plot as a function of 1/T instead of 1/T<sup>2</sup>). 97 + **bfp**: 98 The best-fit parameters of the regression. 99 This is a `dict` with keys equal to the polynomial coefficients (see `bff` definition below) 100 + **bff()**: 101 The best-fit polynomial function of inverse T, defined as: 102 `bff(x) = sum(bfp[f'a{k}'] * x**k for k in degrees)` 103 Note that `bff` takes `x = 1/(T+273.15)` (instead of `T`) as input. 104 105 106 ### Examples 107 108 A very simple example: 109 110 ````py 111 .. include:: ../../code_examples/D47calib_init/example.py 112 ```` 113 114 Should yield: 115 116 ```` 117 .. include:: ../../code_examples/D47calib_init/output.txt 118 ```` 119 120 """ 121 122 self.samples = samples[:] 123 self.name = name 124 self.label = label 125 self.description = description 126 self.D47 = _np.asarray(D47, dtype = 'float') 127 self.N = self.D47.size 128 129 if sD47 is None: 130 self.sD47 = _np.zeros((self.N, self.N)) 131 else: 132 self.sD47 = _np.asarray(sD47) 133 if len(self.sD47.shape) == 1: 134 self.sD47 = _np.diag(self.sD47**2) 135 elif len(self.sD47.shape) == 0: 136 self.sD47 = _np.eye(self.D47.size) * self.sD47**2 137 138 _ogls.InverseTPolynomial.__init__(self, T=T, Y=D47, sT=sT, sY=sD47, degrees = degrees, xpower = xpower, **kwargs) 139 140 if self.bfp is None: 141 self.regress() 142 143 self._bff_deriv = lambda x: _np.array([k * self.bfp[f'a{k}'] * x**(k-1) for k in degrees if k > 0]).sum(axis = 0) 144 145 xi = _np.linspace(0,200**-1,1001) 146 self._inv_bff = _interp1d(self.bff(xi), xi) 147 148 self._D47_from_T = lambda T: self.bff((T+273.15)**-1) 149 self._T_from_D47 = lambda D47: self._inv_bff(D47)**-1 - 273.15 150 self._D47_from_T_deriv = lambda T: -(T+273.15)**-2 * self._bff_deriv((T+273.15)**-1) 151 self._T_from_D47_deriv = lambda D47: self._D47_from_T_deriv(self._T_from_D47(D47))**-1 152 153 def __repr__(self): 154 return f'<D47calib: {self.name}>' 155 156 def invT_xaxis(self, 157 xlabel = None, 158 Ti = [0,20,50,100,250,1000], 159 ): 160 """ 161 Create and return an `Axes` object with X values equal to 1/T<sup>2</sup>, 162 but labeled in degrees Celsius. 163 164 ### Parameters 165 166 + **xlabel**: 167 Custom label for X axis (`r'$1\,/\,T^2$'` by default) 168 + **Ti**: 169 Specify tick locations for X axis, in degrees C. 170 171 ### Returns 172 173 + an `matplotlib.axes.Axes` instance 174 175 ### Examples 176 177 ````py 178 .. include:: ../../code_examples/D47calib_invT_xaxis/example_1.py 179 ```` 180 181 This should result in something like this: 182 183 <img align="center" src="example_invT_xaxis_1.png"> 184 185 It is also possible to define the X axis using a different power of 1/T 186 by first redefining the `xpower` attribute: 187 188 ````py 189 .. include:: ../../code_examples/D47calib_invT_xaxis/example_2.py 190 ```` 191 192 This should result in something like this: 193 194 <img align="center" src="example_invT_xaxis_2.png"> 195 """ 196 if xlabel is None: 197 xlabel = f'$1\\,/\\,T^{self.xpower}$' if self.xpower > 1 else '1/T' 198 _ppl.xlabel(xlabel) 199 _ppl.xticks([(273.15 + t) ** -self.xpower for t in sorted(Ti)[::-1]]) 200 ax = _ppl.gca() 201 ax.set_xticklabels([f"${t}\\,$°C" for t in sorted(Ti)[::-1]]) 202 ax.tick_params(which="major") 203 204 return ax 205 206 207 def plot_data(self, label = False, **kwargs): 208 """ 209 Plot Δ47 value of each sample as a function of 1/T<sup>2</sup>. 210 211 ### Parameters 212 213 + **label**: 214 + If `label` is a string, use this string as `label` for the underlyig 215 `matplotlib.pyplot.plot()` call. 216 + If `label = True`, use the caller's `label` attribute instead. 217 + If `label = False`, no label is specified (default behavior). 218 + **kwargs**: 219 keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call. 220 221 ### Returns 222 223 + the return value(s) of the underlying `matplotlib.pyplot.plot()` call. 224 225 ### Example 226 227 ````py 228 from matplotlib import pyplot as ppl 229 from D47calib import huyghe_2022 as calib 230 231 fig = ppl.figure(figsize = (5,3)) 232 ppl.subplots_adjust(bottom = .25, left = .15) 233 calib.invT_xaxis(Ti = [0,10,25]) 234 calib.plot_data(label = True) 235 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 236 ppl.legend() 237 ppl.savefig('example_plot_data.png', dpi = 100) 238 ````` 239 240 This should result in something like this: 241 242 <img align="center" src="example_plot_data.png"> 243 """ 244# if 'mec' not in kwargs: 245# kwargs['mec'] = self.color 246 if label is not False: 247 kwargs['label'] = self.label if label is True else label 248 return _ogls.InverseTPolynomial.plot_data(self, **kwargs) 249 250 251 def plot_error_bars(self, **kwargs): 252 """ 253 Plot Δ47 error bars (±1.96 SE) of each sample as a function of 1/T<sup>2</sup>. 254 255 ### Parameters 256 257 + **kwargs**: 258 keyword arguments passed to the underlying `matplotlib.pyplot.errrobar()` call. 259 260 ### Returns 261 262 + the return value(s) of the underlying `matplotlib.pyplot.errorbar()` call. 263 264 ### Example 265 266 ````py 267 from matplotlib import pyplot as ppl 268 from D47calib import huyghe_2022 as calib 269 270 fig = ppl.figure(figsize = (5,3)) 271 ppl.subplots_adjust(bottom = .25, left = .15) 272 calib.invT_xaxis(Ti = [0,10,25]) 273 calib.plot_error_bars(alpha = .4) 274 calib.plot_data(label = True) 275 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 276 ppl.legend() 277 ppl.savefig('example_plot_error_bars.png', dpi = 100) 278 ````` 279 280 This should result in something like this: 281 282 <img align="center" src="example_plot_error_bars.png"> 283 """ 284# if 'ecolor' not in kwargs: 285# kwargs['ecolor'] = self.color 286 return _ogls.InverseTPolynomial.plot_error_bars(self, **kwargs) 287 288 289 def plot_error_ellipses(self, **kwargs): 290 """ 291 Plot Δ47 error ellipses (95 % confidence) of each sample as a function of 1/T<sup>2</sup>. 292 293 ### Parameters 294 295 + **kwargs**: 296 keyword arguments passed to the underlying `matplotlib.patches.Ellipse()` call. 297 298 ### Returns 299 300 + the return value(s) of the underlying `matplotlib.patches.Ellipse()` call. 301 302 ### Example 303 304 ````py 305 from matplotlib import pyplot as ppl 306 from D47calib import huyghe_2022 as calib 307 308 fig = ppl.figure(figsize = (5,3)) 309 ppl.subplots_adjust(bottom = .25, left = .15) 310 calib.invT_xaxis(Ti = [0,10,25]) 311 calib.plot_error_ellipses(alpha = .4) 312 calib.plot_data(label = True) 313 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 314 ppl.legend() 315 ppl.savefig('example_plot_error_ellipses.png', dpi = 100) 316 ````` 317 318 This should result in something like this: 319 320 <img align="center" src="example_plot_error_ellipses.png"> 321 """ 322# if 'ec' not in kwargs: 323# kwargs['ec'] = self.color 324 return _ogls.InverseTPolynomial.plot_error_ellipses(self, **kwargs) 325 326 327 def plot_bff(self, label = False, **kwargs): 328 """ 329 Plot best-fit regression of Δ47 as a function of 1/T<sup>2</sup>. 330 331 ### Parameters 332 333 + **label**: 334 + If `label` is a string, use this string as `label` for the underlyig 335 `matplotlib.pyplot.plot()` call. 336 + If `label = True`, use the caller's `label` attribute instead. 337 + If `label = False`, no label is specified (default behavior). 338 + **kwargs**: 339 keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call. 340 341 ### Returns 342 343 + the return value(s) of the underlying `matplotlib.pyplot.plot()` call. 344 345 ### Example 346 347 ````py 348 from matplotlib import pyplot as ppl 349 from D47calib import huyghe_2022 as calib 350 351 fig = ppl.figure(figsize = (5,3)) 352 ppl.subplots_adjust(bottom = .25, left = .15) 353 calib.invT_xaxis(Ti = [0,10,25]) 354 calib.plot_bff(label = True, dashes = (8,2,2,2)) 355 calib.plot_data() 356 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 357 ppl.legend() 358 ppl.savefig('example_plot_bff.png', dpi = 100) 359 ````` 360 361 This should result in something like this: 362 363 <img align="center" src="example_plot_bff.png"> 364 """ 365# if 'color' not in kwargs: 366# kwargs['color'] = self.color 367 if label is not False: 368 kwargs['label'] = self.label if label is True else label 369 return _ogls.InverseTPolynomial.plot_bff(self, **kwargs) 370 371 372 def plot_bff_ci(self, **kwargs): 373 """ 374 Plot 95 % confidence region for best-fit regression of Δ47 as a function of 1/T<sup>2</sup>. 375 376 ### Parameters 377 378 + **label**: 379 + **kwargs**: 380 keyword arguments passed to the underlying `matplotlib.pyplot.fill_between()` call. 381 382 ### Returns 383 384 + the return value(s) of the underlying `matplotlib.pyplot.fill_between()` call. 385 386 ### Example 387 388 ````py 389 from matplotlib import pyplot as ppl 390 from D47calib import huyghe_2022 as calib 391 392 fig = ppl.figure(figsize = (5,3)) 393 ppl.subplots_adjust(bottom = .25, left = .15) 394 calib.invT_xaxis(Ti = [0,10,25]) 395 calib.plot_bff_ci(alpha = .15) 396 calib.plot_bff(label = True, dashes = (8,2,2,2)) 397 calib.plot_data() 398 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 399 ppl.legend() 400 ppl.savefig('example_plot_bff_ci.png', dpi = 100) 401 ````` 402 403 This should result in something like this: 404 405 <img align="center" src="example_plot_bff_ci.png"> 406 """ 407# if 'color' not in kwargs: 408# kwargs['color'] = self.color 409 return _ogls.InverseTPolynomial.plot_bff_ci(self, **kwargs) 410 411 def T47(self, 412 D47 = None, 413 sD47 = None, 414 T=None, 415 sT = None, 416 error_from = 'both', 417 return_covar = False, 418 ): 419 ''' 420 When `D47` is input, computes corresponding T value(s). 421 `D47` input may be specified as a scalar, or as a 1-D array. 422 `T` output will then have the same type and size as `D47`. 423 424 When `T` is input, computes corresponding Δ47 value(s). 425 `T` input may be specified as a scalar, or as a 1-D array. 426 `D47` output will then have the same type and size as `T`. 427 428 Only one of either `D47` or `T` may be specified as input. 429 430 **Arguments:** 431 432 * `D47`: Δ47 value(s) to convert into temperature (`float` or 1-D array) 433 * `sD47`: Δ47 uncertainties, which may be: 434 - `None` (default) 435 - `float` or `int` (uniform standard error on `D47`) 436 - 1-D array (standard errors on `D47`) 437 - 2-D array (covariance matrix for `D47`) 438 * `T`: T value(s) to convert into Δ47 (`float` or 1-D array), in degrees C 439 * `sT`: T uncertainties, which may be: 440 - `None` (default) 441 - `float` or `int` (uniform standard error on `T`) 442 - 1-D array (standard errors on `T`) 443 - 2-D array (variance-covariance matrix for `T`) 444 * `error_from`: if set to `'both'` (default), returned errors take into account 445 input uncertainties (`sT` or `sD47`) as well as calibration uncertainties; 446 if set to `'calib'`, only calibration uncertainties are accounted for; 447 if set to `'sT'` or `'sD47'`, calibration uncertainties are ignored. 448 * `return_covar`: (False by default) whether to return the full covariance matrix 449 for returned `T` or `D47` values, otherwise return standard errors for the returned 450 `T` or `D47` values instead. 451 452 **Returns (with `D47` input):** 453 454 * `T`: temperature value(s) computed from `D47` 455 * `sT`: uncertainties on `T` value(s), whether as standard error(s) or covariance matrix 456 457 **Returns (with `T` input):** 458 459 * `D47`: Δ47 value(s) computed from `D47` 460 * `sD47`: uncertainties on `D47` value(s), whether as standard error(s) or covariance matrix 461 462 ### Example 463 464 ````py 465 import numpy as np 466 from matplotlib import pyplot as ppl 467 from D47calib import ogls_2023 as calib 468 469 X = np.linspace(1473**-2, 270**-2) 470 D47, sD47 = calib.T47(T = X**-0.5 - 273.15) 471 472 fig = ppl.figure(figsize = (5,3)) 473 ppl.subplots_adjust(bottom = .25, left = .15) 474 calib.invT_xaxis() 475 ppl.plot(X, 1000 * sD47, 'r-') 476 ppl.ylabel('Calibration SE on $Δ_{47}$ values (ppm)') 477 ppl.savefig('example_SE47.png', dpi = 100) 478 ````` 479 480 This should result in something like this: 481 482 <img src="example_SE47.png"> 483 ''' 484 485 if D47 is None and T is None: 486 raise ValueError('Either D47 or T must be specified, but both are undefined.') 487 488 if D47 is not None and T is not None: 489 raise ValueError('Either D47 or T must be specified, but not both.') 490 491 if T is not None: 492 493 D47 = self._D47_from_T(T) 494 Np = len(self.degrees) 495 N = D47.size 496 497 ### Compute covariance matrix of (*bfp, *T): 498 CM = _np.zeros((Np+N, Np+N)) 499 500 if error_from in ['calib', 'both']: 501 CM[:Np, :Np] = self.bfp_CM[:,:] 502 503 if (sT is not None) and error_from in ['sT', 'both']: 504 _sT = _np.asarray(sT) 505 if _sT.ndim == 0: 506 for k in range(N): 507 CM[Np+k, Np+k] = _sT**2 508 elif _sT.ndim == 1: 509 for k in range(N): 510 CM[Np+k, Np+k] = _sT[k]**2 511 elif _sT.ndim == 2: 512 CM[-N:, -N:] = _sT[:,:] 513 514 ### Compute Jacobian of D47(T) relative to (*bfp, *T): 515 _T = _np.asarray(T) 516 if _T.ndim == 0: 517 _T = _np.expand_dims(_T, 0) 518 J = _np.zeros((N, Np+N)) 519 520 if (sT is not None) and error_from in ['sT', 'both']: 521 for k in range(N): 522 J[k, Np+k] = self._D47_from_T_deriv(_T[k]) 523 524 if error_from in ['calib', 'both']: 525 526 for k in range(Np): 527 528 p1 = {_: self.bfp[_] for _ in self.bfp} 529 p1[f'a{self.degrees[k]}'] += 0.001 * self.bfp_CM[k,k]**.5 530 531 p2 = {_: self.bfp[_] for _ in self.bfp} 532 p2[f'a{self.degrees[k]}'] -= 0.001 * self.bfp_CM[k,k]**.5 533 534 J[:, k] = (self.model_fun(p1, (_T+273.15)**-1) - self.model_fun(p2, (_T+273.15)**-1)) / (0.002 * self.bfp_CM[k,k]**.5) 535 536 ### Error propagation: 537 CM_D47 = J @ CM @ J.T 538 539 if return_covar: 540 return D47, CM_D47 541 else: 542 return D47, float(_np.diag(CM_D47)**.5) if D47.ndim == 0 else _np.diag(CM_D47)**.5 543 544 if D47 is not None: 545 546 T = self._T_from_D47(D47) 547 Np = len(self.degrees) 548 N = T.size 549 550 ### Compute covariance matrix of (*bfp, *T): 551 CM = _np.zeros((Np+N, Np+N)) 552 553 if error_from in ['calib', 'both']: 554 CM[:Np, :Np] = self.bfp_CM[:,:] 555 556 if (sD47 is not None) and error_from in ['sD47', 'both']: 557 _sD47 = _np.asarray(sD47) 558 if _sD47.ndim == 0: 559 for k in range(N): 560 CM[Np+k, Np+k] = _sD47**2 561 elif _sD47.ndim == 1: 562 for k in range(N): 563 CM[Np+k, Np+k] = _sD47[k]**2 564 elif _sD47.ndim == 2: 565 CM[-N:, -N:] = _sD47[:,:] 566 567 ### Compute Jacobian of T(D47) relative to (*bfp, *D47): 568 _D47 = _np.asarray(D47) 569 if _D47.ndim == 0: 570 _D47 = _np.expand_dims(_D47, 0) 571 J = _np.zeros((N, Np+N)) 572 if (sD47 is not None) and error_from in ['sD47', 'both']: 573 for k in range(N): 574 J[k, Np+k] = self._T_from_D47_deriv(_D47[k]) 575 if error_from in ['calib', 'both']: 576 577 xi = _np.linspace(0,200**-1,1001)[1:] 578 for k in range(Np): 579 580 if self.bfp_CM[k,k]: 581 _epsilon_ = self.bfp_CM[k,k]**.5 582 else: 583 _epsilon_ = 1e-6 584 585 p1 = {_: self.bfp[_] for _ in self.bfp} 586 p1[f'a{self.degrees[k]}'] += 0.001 * _epsilon_ 587 T_from_D47_p1 = _interp1d(self.model_fun(p1, xi), xi**-1 - 273.15) 588 589 p2 = {_: self.bfp[_] for _ in self.bfp} 590 p2[f'a{self.degrees[k]}'] -= 0.001 * _epsilon_ 591 T_from_D47_p2 = _interp1d(self.model_fun(p2, xi), xi**-1 - 273.15) 592 593 J[:, k] = (T_from_D47_p1(_D47) - T_from_D47_p2(_D47)) / (0.002 * _epsilon_) 594 595 ### Error propagation: 596 CM_T = J @ CM @ J.T 597 598 if return_covar: 599 return T, CM_T 600 else: 601 return T, float(_np.diag(CM_T)**.5) if T.ndim == 0 else _np.diag(CM_T)**.5 602 603 604 def plot_T47_errors( 605 self, 606 calibname = None, 607 rD47 = 0.010, 608 Nr = [2,4,8,12,20], 609 Tmin = 0, 610 Tmax = 120, 611 colors = [(1,0,0),(1,.5,0),(.25,.75,0),(0,.5,1),(0.5,0.5,0.5)], 612 yscale = 'lin', 613 ): 614 """ 615 Plot SE of T reconstructed using the calibration as a function of T for various 616 combinations of analytical precision and number of analytical replicates. 617 618 **Arguments** 619 620 + **calibname**: 621 Which calibration name to display. By default, use `label` attribute. 622 + **rD47**: 623 Analytical precision of a single analysis. 624 + **Nr**: 625 A list of lines to plot, each corresponding to a given number of replicates. 626 + **Tmin**: 627 Minimum T to plot. 628 + **Tmax**: 629 Maximum T to plot. 630 + **colors**: 631 A list of colors to distinguish the plotted lines. 632 + **yscale**: 633 + If `'lin'`, the Y axis uses a linear scale. 634 + If `'log'`, the Y axis uses a logarithmic scale. 635 636 **Example** 637 638 ````py 639 from matplotlib import pyplot as ppl 640 from D47calib import devils_laghetto_2023 as calib 641 642 fig = ppl.figure(figsize = (3.5,4)) 643 ppl.subplots_adjust(bottom = .2, left = .15) 644 calib.plot_T47_errors( 645 calibname = 'Devils Laghetto calibration', 646 Nr = [1,2,4,16], 647 Tmin =0, 648 Tmax = 40, 649 ) 650 ppl.savefig('example_SE_T.png', dpi = 100) 651 ```` 652 653 This should result in something like this: 654 655 <img src="example_SE_T.png"> 656 """ 657 658 if calibname is None: 659 calibname = self.label 660 661 Nr = _np.array(Nr) 662 if len(colors) < Nr.size: 663 print('WARNING: Too few colors to plot different numbers of replicates; generating new colors.') 664 from colorsys import hsv_to_rgb 665 hsv = [(x*1.0/Nr.size, 1, .9) for x in range(Nr.size)] 666 colors = [hsv_to_rgb(*x) for x in hsv] 667 668 Ti = _np.linspace(Tmin, Tmax) 669 D47i, _ = self.T47(T = Ti) 670 _, sT_calib = self.T47(D47 = D47i, error_from = 'calib') 671 672 ymax, ymin = 0, 1e6 673 for N,c in zip(Nr, colors): 674 _, sT = self.T47(D47 = D47i, sD47 = rD47 / N**.5, error_from = 'sD47') 675 _ppl.plot(Ti, sT, '-', color = c, label=f'SE for {N} replicate{"s" if N > 1 else ""}') 676 ymin = min(ymin, min(sT)) 677 ymax = max(ymax, max(sT)) 678 679 _ppl.plot(Ti, sT_calib, 'k--', label='SE from calibration') 680 681 _ppl.legend(fontsize=9) 682 _ppl.xlabel("T (°C)") 683 684 _ppl.ylabel("Standard error on reconstructed T (°C)") 685 686 # yticks([0,.5,1,1.5,2]) 687 _ppl.title(f"{calibname},\nassuming external Δ$_{{47}}$ repeatability of {rD47:.3f} ‰", size = 9) 688 _ppl.grid( alpha = .25) 689 if yscale == 'lin': 690 _ppl.axis([Ti[0], Ti[-1], 0, ymax*1.05]) 691 t1, t2 = self.T.min(), self.T.max() 692 _ppl.plot([t1, t2], [0, 0], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False) 693 _ppl.text((t1+t2)/2, 0, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic') 694 _ppl.axis([None, None, None, _ppl.axis()[-1]*1.25]) 695 elif yscale == 'log': 696 ymin /= 2 697 _ppl.axis([Ti[0], Ti[-1], ymin, ymax*1.05]) 698 _ppl.yscale('log') 699 t1, t2 = self.T.min(), self.T.max() 700 _ppl.plot([t1, t2], [ymin, ymin], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False) 701 _ppl.text((t1+t2)/2, ymin, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic') 702 703 def export_data(self, csvfile, sep = ',', label = False, T_correl = False, D47_correl = False): 704 """ 705 Write calibration data to a csv file. 706 707 ### Parameters 708 709 + **csvfile**: 710 The filename to write data to. 711 + **sep**: 712 The separator between CSV fields. 713 + **label**: 714 + If specified as `True`, include a `Dataset` column with the calibration's `label` attribute. 715 + If specified as a `str`, include a `Dataset` column with that string. 716 + If specified as `False`, do not include a `Dataset` column. 717 + **T_correl**: 718 + If `True`, include correlations between all `T` values. 719 + **D47_correl**: 720 + If `True`, include correlations between all `D47` values. 721 722 ### Example 723 724 ````py 725 D47calib.huyghe_2022.export_data( 726 csvfile = 'example_export_data.csv', 727 T_correl = True, 728 D47_correl = True, 729 ) 730 ```` 731 732 This should result in something like this ([link](example_export_data.csv)): 733 734 .. include:: ../../docs/example_export_data.md 735 736 """ 737 n = len(str(self.N)) 738 739 with open(csvfile, 'w') as f: 740 f.write(sep.join(['ID', 'Sample', 'T', 'SE_T', 'D47', 'SE_D47'])) 741 742 if label: 743 f.write(f'{sep}Dataset') 744 745 if T_correl: 746 inv_diag_sT = _np.diag(_np.diag(self.sT)**-.5) 747 Tcorrel = inv_diag_sT @ self.sT @ inv_diag_sT 748 f.write(sep.join(['']+[f'Tcorrel_{k+1:0{n}d}' for k in range(self.N)])) 749 750 if D47_correl: 751 inv_diag_sD47 = _np.diag(_np.diag(self.sD47)**-.5) 752 D47correl = inv_diag_sD47 @ self.sD47 @ inv_diag_sD47 753 f.write(sep.join(['']+[f'D47correl_{k+1:0{n}d}' for k in range(self.N)])) 754 755 for k, (s, T, sT, D47, sD47) in enumerate(zip( 756 self.samples, 757 self.T, 758 _np.diag(self.sT)**.5, 759 self.D47, 760 _np.diag(self.sD47)**.5, 761 )): 762 f.write('\n' + sep.join([f'{k+1:0{n}d}', s, f'{T:.2f}', f'{sT:.2f}', f'{D47:.4f}', f'{sD47:.4f}'])) 763 if label: 764 if label is True: 765 f.write(f'{sep}{self.label}') 766 else: 767 f.write(f'{sep}{label}') 768 if T_correl: 769 f.write(sep.join(['']+[ 770 f'{Tcorrel[k,_]:.0f}' 771 if f'{Tcorrel[k,_]:.6f}'[-6:] == '000000' 772 else f'{Tcorrel[k,_]:.6f}' 773 for _ in range(self.N)])) 774 if D47_correl: 775 f.write(sep.join(['']+[ 776 f'{D47correl[k,_]:.0f}' 777 if f'{D47correl[k,_]:.6f}'[-6:] == '000000' 778 else f'{D47correl[k,_]:.6f}' 779 for _ in range(self.N)])) 780 781 782 def export(self, name, filename): 783 """ 784 Save `D47calib` object as an importable file. 785 786 ### Parameters 787 788 + **name**: 789 The name of the variable to export. 790 + **filename**: 791 The filename to write to. 792 793 ### Example 794 795 ````py 796 D47calib.anderson_2021_lsce.export('foo', 'bar.py') 797 ```` 798 799 This should result in a `bar.py` file with the following contents: 800 801 ````py 802 foo = D47calib( 803 samples = ['LGB-2', 'DVH-2'], 804 T = [7.9, 33.7], 805 D47 = [0.6485720997671647, 0.5695972909966959], 806 sT = [[0.04000000000000001, 0.0], [0.0, 0.04000000000000001]], 807 sD47 = [[8.72797097773764e-06, 2.951894073404263e-06], [2.9518940734042614e-06, 7.498611746762038e-06]], 808 description = 'Devils Hole & Laghetto Basso from Anderson et al. (2021), processed in I-CDES', 809 label = 'Slow-growing calcites from Anderson et al. (2021)', 810 color = (0, 0.5, 0), 811 degrees = [0, 2], 812 bfp = {'a0': 0.1583220210575451, 'a2': 38724.41371782721}, 813 bfp_CM = [[0.00035908667755871876, -30.707016431538836], [-30.70701643153884, 2668091.396598919]], 814 chisq = 6.421311854486162e-27, 815 Nf = 0, 816 ) 817 ```` 818 """ 819 with open(filename, 'w') as f: 820 f.write(f''' 821{name} = D47calib( 822 samples = {self.samples}, 823 T = {list(self.T)}, 824 D47 = {list(self.D47)}, 825 sT = {[list(l) for l in self.sT]}, 826 sD47 = {[list(l) for l in self.sD47]}, 827 degrees = {self.degrees}, 828 description = {repr(self.description)}, 829 name = {repr(self.name)}, 830 label = {repr(self.label)}, 831 bfp = {self.bfp}, 832 bfp_CM = {[list(l) for l in self.bfp_CM]}, 833 chisq = {self.chisq}, 834 cholesky_residuals = {list(self.cholesky_residuals)}, 835 aic = {self.aic}, 836 bic = {self.bic}, 837 ks_pvalue = {self.ks_pvalue}, 838 ) 839''')
Δ47 calibration class based on OGLS regression of Δ47 as a polynomial function of inverse T.
46 def __init__(self, 47 samples, T, D47, 48 sT = None, 49 sD47 = None, 50 degrees = [0,2], 51 xpower = 2, 52 name = '', 53 label = '', 54 description = '', 55 **kwargs, 56 ): 57 """ 58 ### Parameters 59 60 + **samples**: a list of N sample names. 61 + **T**: a 1-D array (or array-like) of temperatures values (in degrees C), of size N. 62 + **D47**: a 1-D array (or array-like) of Δ47 values (in permil), of size N. 63 + **sT**: uncertainties on `T`. If specified as: 64 + a scalar: `sT` is treated as the standard error applicable to all `T` values; 65 + a 1-D array-like of size N: `sT` is treated as the standard errors of `T`; 66 + a 2-D array-like of size (N, N): `sT` is treated as the (co)variance matrix of `T`. 67 + **sD47**: uncertainties on `D47`. If specified as: 68 + a scalar: `sD47` is treated as the standard error applicable to all `D47` values; 69 + a 1-D array-like of size N: `sD47` is treated as the standard errors of `D47`; 70 + a 2-D array-like of size (N, N): `sD47` is treated as the (co)variance matrix of `D47`. 71 + **degrees**: degrees of the polynomial regression, e.g., `[0, 2]` or `[0, 1, 2, 3, 4]`. 72 + **name**: a human-readable, short name assigned to the calibration. 73 + **label**: a short description of the calibration, e.g., to be used in legends. 74 + **description**: a longer description, including relevant references/DOIs. 75 This is not necessary when `bfp` and `CM_bfp` are specified at instantiation time. 76 + **kwargs**: keyword arguments passed to the underlying `ogls.InverseTPolynomial()` call. 77 78 ### Notable attributes 79 80 + **N**: 81 The total number of observations (samples) in the calibration data. 82 + **samples**: 83 The list sample names. 84 + **T**: 85 1-D `ndarray` of temperatures in degrees C. 86 + **D47**: 87 1-D `ndarray` of Δ47 values in permil. 88 + **sT**: 89 2-D `ndarray` equal to the full (co)variance matrix for `T`. 90 + **D47**: 91 2-D `ndarray` equal to the full (co)variance matrix for `D47`. 92 + **xpower**: 93 By default, all `D47calib` graphical methods plot Δ47 as a function of 1/T<sup>2</sup>. 94 It is possible to change this behavior to use a different power of 1/T. 95 This is done by redefining the `xpower` attribute to a different, non-zero `int` value 96 (e.g. `foo.xpower = 1` to plot as a function of 1/T instead of 1/T<sup>2</sup>). 97 + **bfp**: 98 The best-fit parameters of the regression. 99 This is a `dict` with keys equal to the polynomial coefficients (see `bff` definition below) 100 + **bff()**: 101 The best-fit polynomial function of inverse T, defined as: 102 `bff(x) = sum(bfp[f'a{k}'] * x**k for k in degrees)` 103 Note that `bff` takes `x = 1/(T+273.15)` (instead of `T`) as input. 104 105 106 ### Examples 107 108 A very simple example: 109 110 ````py 111 .. include:: ../../code_examples/D47calib_init/example.py 112 ```` 113 114 Should yield: 115 116 ```` 117 .. include:: ../../code_examples/D47calib_init/output.txt 118 ```` 119 120 """ 121 122 self.samples = samples[:] 123 self.name = name 124 self.label = label 125 self.description = description 126 self.D47 = _np.asarray(D47, dtype = 'float') 127 self.N = self.D47.size 128 129 if sD47 is None: 130 self.sD47 = _np.zeros((self.N, self.N)) 131 else: 132 self.sD47 = _np.asarray(sD47) 133 if len(self.sD47.shape) == 1: 134 self.sD47 = _np.diag(self.sD47**2) 135 elif len(self.sD47.shape) == 0: 136 self.sD47 = _np.eye(self.D47.size) * self.sD47**2 137 138 _ogls.InverseTPolynomial.__init__(self, T=T, Y=D47, sT=sT, sY=sD47, degrees = degrees, xpower = xpower, **kwargs) 139 140 if self.bfp is None: 141 self.regress() 142 143 self._bff_deriv = lambda x: _np.array([k * self.bfp[f'a{k}'] * x**(k-1) for k in degrees if k > 0]).sum(axis = 0) 144 145 xi = _np.linspace(0,200**-1,1001) 146 self._inv_bff = _interp1d(self.bff(xi), xi) 147 148 self._D47_from_T = lambda T: self.bff((T+273.15)**-1) 149 self._T_from_D47 = lambda D47: self._inv_bff(D47)**-1 - 273.15 150 self._D47_from_T_deriv = lambda T: -(T+273.15)**-2 * self._bff_deriv((T+273.15)**-1) 151 self._T_from_D47_deriv = lambda D47: self._D47_from_T_deriv(self._T_from_D47(D47))**-1
Parameters
- samples: a list of N sample names.
- T: a 1-D array (or array-like) of temperatures values (in degrees C), of size N.
- D47: a 1-D array (or array-like) of Δ47 values (in permil), of size N.
- sT: uncertainties on
T
. If specified as:- a scalar:
sT
is treated as the standard error applicable to allT
values; - a 1-D array-like of size N:
sT
is treated as the standard errors ofT
; - a 2-D array-like of size (N, N):
sT
is treated as the (co)variance matrix ofT
.
- a scalar:
- sD47: uncertainties on
D47
. If specified as:- a scalar:
sD47
is treated as the standard error applicable to allD47
values; - a 1-D array-like of size N:
sD47
is treated as the standard errors ofD47
; - a 2-D array-like of size (N, N):
sD47
is treated as the (co)variance matrix ofD47
.
- a scalar:
- 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
andCM_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 forT
. - D47:
2-D
ndarray
equal to the full (co)variance matrix forD47
. - 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 thexpower
attribute to a different, non-zeroint
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 (seebff
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 thatbff
takesx = 1/(T+273.15)
(instead ofT
) 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
156 def invT_xaxis(self, 157 xlabel = None, 158 Ti = [0,20,50,100,250,1000], 159 ): 160 """ 161 Create and return an `Axes` object with X values equal to 1/T<sup>2</sup>, 162 but labeled in degrees Celsius. 163 164 ### Parameters 165 166 + **xlabel**: 167 Custom label for X axis (`r'$1\,/\,T^2$'` by default) 168 + **Ti**: 169 Specify tick locations for X axis, in degrees C. 170 171 ### Returns 172 173 + an `matplotlib.axes.Axes` instance 174 175 ### Examples 176 177 ````py 178 .. include:: ../../code_examples/D47calib_invT_xaxis/example_1.py 179 ```` 180 181 This should result in something like this: 182 183 <img align="center" src="example_invT_xaxis_1.png"> 184 185 It is also possible to define the X axis using a different power of 1/T 186 by first redefining the `xpower` attribute: 187 188 ````py 189 .. include:: ../../code_examples/D47calib_invT_xaxis/example_2.py 190 ```` 191 192 This should result in something like this: 193 194 <img align="center" src="example_invT_xaxis_2.png"> 195 """ 196 if xlabel is None: 197 xlabel = f'$1\\,/\\,T^{self.xpower}$' if self.xpower > 1 else '1/T' 198 _ppl.xlabel(xlabel) 199 _ppl.xticks([(273.15 + t) ** -self.xpower for t in sorted(Ti)[::-1]]) 200 ax = _ppl.gca() 201 ax.set_xticklabels([f"${t}\\,$°C" for t in sorted(Ti)[::-1]]) 202 ax.tick_params(which="major") 203 204 return ax
Create and return an Axes
object with X values equal to 1/T2,
but labeled in degrees Celsius.
Parameters
- xlabel:
Custom label for X axis (
r'$1\,/\,T^2$'
by default) - Ti: Specify tick locations for X axis, in degrees C.
Returns
- an
matplotlib.axes.Axes
instance
Examples
from matplotlib import pyplot as ppl
from D47calib import 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:
207 def plot_data(self, label = False, **kwargs): 208 """ 209 Plot Δ47 value of each sample as a function of 1/T<sup>2</sup>. 210 211 ### Parameters 212 213 + **label**: 214 + If `label` is a string, use this string as `label` for the underlyig 215 `matplotlib.pyplot.plot()` call. 216 + If `label = True`, use the caller's `label` attribute instead. 217 + If `label = False`, no label is specified (default behavior). 218 + **kwargs**: 219 keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call. 220 221 ### Returns 222 223 + the return value(s) of the underlying `matplotlib.pyplot.plot()` call. 224 225 ### Example 226 227 ````py 228 from matplotlib import pyplot as ppl 229 from D47calib import huyghe_2022 as calib 230 231 fig = ppl.figure(figsize = (5,3)) 232 ppl.subplots_adjust(bottom = .25, left = .15) 233 calib.invT_xaxis(Ti = [0,10,25]) 234 calib.plot_data(label = True) 235 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 236 ppl.legend() 237 ppl.savefig('example_plot_data.png', dpi = 100) 238 ````` 239 240 This should result in something like this: 241 242 <img align="center" src="example_plot_data.png"> 243 """ 244# if 'mec' not in kwargs: 245# kwargs['mec'] = self.color 246 if label is not False: 247 kwargs['label'] = self.label if label is True else label 248 return _ogls.InverseTPolynomial.plot_data(self, **kwargs)
Plot Δ47 value of each sample as a function of 1/T2.
Parameters
- label:
- If
label
is a string, use this string aslabel
for the underlyigmatplotlib.pyplot.plot()
call. - If
label = True
, use the caller'slabel
attribute instead. - If
label = False
, no label is specified (default behavior).
- If
- 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:
251 def plot_error_bars(self, **kwargs): 252 """ 253 Plot Δ47 error bars (±1.96 SE) of each sample as a function of 1/T<sup>2</sup>. 254 255 ### Parameters 256 257 + **kwargs**: 258 keyword arguments passed to the underlying `matplotlib.pyplot.errrobar()` call. 259 260 ### Returns 261 262 + the return value(s) of the underlying `matplotlib.pyplot.errorbar()` call. 263 264 ### Example 265 266 ````py 267 from matplotlib import pyplot as ppl 268 from D47calib import huyghe_2022 as calib 269 270 fig = ppl.figure(figsize = (5,3)) 271 ppl.subplots_adjust(bottom = .25, left = .15) 272 calib.invT_xaxis(Ti = [0,10,25]) 273 calib.plot_error_bars(alpha = .4) 274 calib.plot_data(label = True) 275 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 276 ppl.legend() 277 ppl.savefig('example_plot_error_bars.png', dpi = 100) 278 ````` 279 280 This should result in something like this: 281 282 <img align="center" src="example_plot_error_bars.png"> 283 """ 284# if 'ecolor' not in kwargs: 285# kwargs['ecolor'] = self.color 286 return _ogls.InverseTPolynomial.plot_error_bars(self, **kwargs)
Plot Δ47 error bars (±1.96 SE) of each sample as a function of 1/T2.
Parameters
- kwargs:
keyword arguments passed to the underlying
matplotlib.pyplot.errrobar()
call.
Returns
- the return value(s) of the underlying
matplotlib.pyplot.errorbar()
call.
Example
from matplotlib import pyplot as ppl
from D47calib import huyghe_2022 as calib
fig = ppl.figure(figsize = (5,3))
ppl.subplots_adjust(bottom = .25, left = .15)
calib.invT_xaxis(Ti = [0,10,25])
calib.plot_error_bars(alpha = .4)
calib.plot_data(label = True)
ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
ppl.legend()
ppl.savefig('example_plot_error_bars.png', dpi = 100)
This should result in something like this:
289 def plot_error_ellipses(self, **kwargs): 290 """ 291 Plot Δ47 error ellipses (95 % confidence) of each sample as a function of 1/T<sup>2</sup>. 292 293 ### Parameters 294 295 + **kwargs**: 296 keyword arguments passed to the underlying `matplotlib.patches.Ellipse()` call. 297 298 ### Returns 299 300 + the return value(s) of the underlying `matplotlib.patches.Ellipse()` call. 301 302 ### Example 303 304 ````py 305 from matplotlib import pyplot as ppl 306 from D47calib import huyghe_2022 as calib 307 308 fig = ppl.figure(figsize = (5,3)) 309 ppl.subplots_adjust(bottom = .25, left = .15) 310 calib.invT_xaxis(Ti = [0,10,25]) 311 calib.plot_error_ellipses(alpha = .4) 312 calib.plot_data(label = True) 313 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 314 ppl.legend() 315 ppl.savefig('example_plot_error_ellipses.png', dpi = 100) 316 ````` 317 318 This should result in something like this: 319 320 <img align="center" src="example_plot_error_ellipses.png"> 321 """ 322# if 'ec' not in kwargs: 323# kwargs['ec'] = self.color 324 return _ogls.InverseTPolynomial.plot_error_ellipses(self, **kwargs)
Plot Δ47 error ellipses (95 % confidence) of each sample as a function of 1/T2.
Parameters
- kwargs:
keyword arguments passed to the underlying
matplotlib.patches.Ellipse()
call.
Returns
- the return value(s) of the underlying
matplotlib.patches.Ellipse()
call.
Example
from matplotlib import pyplot as ppl
from D47calib import huyghe_2022 as calib
fig = ppl.figure(figsize = (5,3))
ppl.subplots_adjust(bottom = .25, left = .15)
calib.invT_xaxis(Ti = [0,10,25])
calib.plot_error_ellipses(alpha = .4)
calib.plot_data(label = True)
ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
ppl.legend()
ppl.savefig('example_plot_error_ellipses.png', dpi = 100)
This should result in something like this:
327 def plot_bff(self, label = False, **kwargs): 328 """ 329 Plot best-fit regression of Δ47 as a function of 1/T<sup>2</sup>. 330 331 ### Parameters 332 333 + **label**: 334 + If `label` is a string, use this string as `label` for the underlyig 335 `matplotlib.pyplot.plot()` call. 336 + If `label = True`, use the caller's `label` attribute instead. 337 + If `label = False`, no label is specified (default behavior). 338 + **kwargs**: 339 keyword arguments passed to the underlying `matplotlib.pyplot.plot()` call. 340 341 ### Returns 342 343 + the return value(s) of the underlying `matplotlib.pyplot.plot()` call. 344 345 ### Example 346 347 ````py 348 from matplotlib import pyplot as ppl 349 from D47calib import huyghe_2022 as calib 350 351 fig = ppl.figure(figsize = (5,3)) 352 ppl.subplots_adjust(bottom = .25, left = .15) 353 calib.invT_xaxis(Ti = [0,10,25]) 354 calib.plot_bff(label = True, dashes = (8,2,2,2)) 355 calib.plot_data() 356 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 357 ppl.legend() 358 ppl.savefig('example_plot_bff.png', dpi = 100) 359 ````` 360 361 This should result in something like this: 362 363 <img align="center" src="example_plot_bff.png"> 364 """ 365# if 'color' not in kwargs: 366# kwargs['color'] = self.color 367 if label is not False: 368 kwargs['label'] = self.label if label is True else label 369 return _ogls.InverseTPolynomial.plot_bff(self, **kwargs)
Plot best-fit regression of Δ47 as a function of 1/T2.
Parameters
- label:
- If
label
is a string, use this string aslabel
for the underlyigmatplotlib.pyplot.plot()
call. - If
label = True
, use the caller'slabel
attribute instead. - If
label = False
, no label is specified (default behavior).
- If
- 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:
372 def plot_bff_ci(self, **kwargs): 373 """ 374 Plot 95 % confidence region for best-fit regression of Δ47 as a function of 1/T<sup>2</sup>. 375 376 ### Parameters 377 378 + **label**: 379 + **kwargs**: 380 keyword arguments passed to the underlying `matplotlib.pyplot.fill_between()` call. 381 382 ### Returns 383 384 + the return value(s) of the underlying `matplotlib.pyplot.fill_between()` call. 385 386 ### Example 387 388 ````py 389 from matplotlib import pyplot as ppl 390 from D47calib import huyghe_2022 as calib 391 392 fig = ppl.figure(figsize = (5,3)) 393 ppl.subplots_adjust(bottom = .25, left = .15) 394 calib.invT_xaxis(Ti = [0,10,25]) 395 calib.plot_bff_ci(alpha = .15) 396 calib.plot_bff(label = True, dashes = (8,2,2,2)) 397 calib.plot_data() 398 ppl.ylabel('$Δ_{47}$ (‰ I-CDES)') 399 ppl.legend() 400 ppl.savefig('example_plot_bff_ci.png', dpi = 100) 401 ````` 402 403 This should result in something like this: 404 405 <img align="center" src="example_plot_bff_ci.png"> 406 """ 407# if 'color' not in kwargs: 408# kwargs['color'] = self.color 409 return _ogls.InverseTPolynomial.plot_bff_ci(self, **kwargs)
Plot 95 % confidence region for best-fit regression of Δ47 as a function of 1/T2.
Parameters
- label:
- kwargs:
keyword arguments passed to the underlying
matplotlib.pyplot.fill_between()
call.
Returns
- the return value(s) of the underlying
matplotlib.pyplot.fill_between()
call.
Example
from matplotlib import pyplot as ppl
from D47calib import huyghe_2022 as calib
fig = ppl.figure(figsize = (5,3))
ppl.subplots_adjust(bottom = .25, left = .15)
calib.invT_xaxis(Ti = [0,10,25])
calib.plot_bff_ci(alpha = .15)
calib.plot_bff(label = True, dashes = (8,2,2,2))
calib.plot_data()
ppl.ylabel('$Δ_{47}$ (‰ I-CDES)')
ppl.legend()
ppl.savefig('example_plot_bff_ci.png', dpi = 100)
This should result in something like this:
411 def T47(self, 412 D47 = None, 413 sD47 = None, 414 T=None, 415 sT = None, 416 error_from = 'both', 417 return_covar = False, 418 ): 419 ''' 420 When `D47` is input, computes corresponding T value(s). 421 `D47` input may be specified as a scalar, or as a 1-D array. 422 `T` output will then have the same type and size as `D47`. 423 424 When `T` is input, computes corresponding Δ47 value(s). 425 `T` input may be specified as a scalar, or as a 1-D array. 426 `D47` output will then have the same type and size as `T`. 427 428 Only one of either `D47` or `T` may be specified as input. 429 430 **Arguments:** 431 432 * `D47`: Δ47 value(s) to convert into temperature (`float` or 1-D array) 433 * `sD47`: Δ47 uncertainties, which may be: 434 - `None` (default) 435 - `float` or `int` (uniform standard error on `D47`) 436 - 1-D array (standard errors on `D47`) 437 - 2-D array (covariance matrix for `D47`) 438 * `T`: T value(s) to convert into Δ47 (`float` or 1-D array), in degrees C 439 * `sT`: T uncertainties, which may be: 440 - `None` (default) 441 - `float` or `int` (uniform standard error on `T`) 442 - 1-D array (standard errors on `T`) 443 - 2-D array (variance-covariance matrix for `T`) 444 * `error_from`: if set to `'both'` (default), returned errors take into account 445 input uncertainties (`sT` or `sD47`) as well as calibration uncertainties; 446 if set to `'calib'`, only calibration uncertainties are accounted for; 447 if set to `'sT'` or `'sD47'`, calibration uncertainties are ignored. 448 * `return_covar`: (False by default) whether to return the full covariance matrix 449 for returned `T` or `D47` values, otherwise return standard errors for the returned 450 `T` or `D47` values instead. 451 452 **Returns (with `D47` input):** 453 454 * `T`: temperature value(s) computed from `D47` 455 * `sT`: uncertainties on `T` value(s), whether as standard error(s) or covariance matrix 456 457 **Returns (with `T` input):** 458 459 * `D47`: Δ47 value(s) computed from `D47` 460 * `sD47`: uncertainties on `D47` value(s), whether as standard error(s) or covariance matrix 461 462 ### Example 463 464 ````py 465 import numpy as np 466 from matplotlib import pyplot as ppl 467 from D47calib import ogls_2023 as calib 468 469 X = np.linspace(1473**-2, 270**-2) 470 D47, sD47 = calib.T47(T = X**-0.5 - 273.15) 471 472 fig = ppl.figure(figsize = (5,3)) 473 ppl.subplots_adjust(bottom = .25, left = .15) 474 calib.invT_xaxis() 475 ppl.plot(X, 1000 * sD47, 'r-') 476 ppl.ylabel('Calibration SE on $Δ_{47}$ values (ppm)') 477 ppl.savefig('example_SE47.png', dpi = 100) 478 ````` 479 480 This should result in something like this: 481 482 <img src="example_SE47.png"> 483 ''' 484 485 if D47 is None and T is None: 486 raise ValueError('Either D47 or T must be specified, but both are undefined.') 487 488 if D47 is not None and T is not None: 489 raise ValueError('Either D47 or T must be specified, but not both.') 490 491 if T is not None: 492 493 D47 = self._D47_from_T(T) 494 Np = len(self.degrees) 495 N = D47.size 496 497 ### Compute covariance matrix of (*bfp, *T): 498 CM = _np.zeros((Np+N, Np+N)) 499 500 if error_from in ['calib', 'both']: 501 CM[:Np, :Np] = self.bfp_CM[:,:] 502 503 if (sT is not None) and error_from in ['sT', 'both']: 504 _sT = _np.asarray(sT) 505 if _sT.ndim == 0: 506 for k in range(N): 507 CM[Np+k, Np+k] = _sT**2 508 elif _sT.ndim == 1: 509 for k in range(N): 510 CM[Np+k, Np+k] = _sT[k]**2 511 elif _sT.ndim == 2: 512 CM[-N:, -N:] = _sT[:,:] 513 514 ### Compute Jacobian of D47(T) relative to (*bfp, *T): 515 _T = _np.asarray(T) 516 if _T.ndim == 0: 517 _T = _np.expand_dims(_T, 0) 518 J = _np.zeros((N, Np+N)) 519 520 if (sT is not None) and error_from in ['sT', 'both']: 521 for k in range(N): 522 J[k, Np+k] = self._D47_from_T_deriv(_T[k]) 523 524 if error_from in ['calib', 'both']: 525 526 for k in range(Np): 527 528 p1 = {_: self.bfp[_] for _ in self.bfp} 529 p1[f'a{self.degrees[k]}'] += 0.001 * self.bfp_CM[k,k]**.5 530 531 p2 = {_: self.bfp[_] for _ in self.bfp} 532 p2[f'a{self.degrees[k]}'] -= 0.001 * self.bfp_CM[k,k]**.5 533 534 J[:, k] = (self.model_fun(p1, (_T+273.15)**-1) - self.model_fun(p2, (_T+273.15)**-1)) / (0.002 * self.bfp_CM[k,k]**.5) 535 536 ### Error propagation: 537 CM_D47 = J @ CM @ J.T 538 539 if return_covar: 540 return D47, CM_D47 541 else: 542 return D47, float(_np.diag(CM_D47)**.5) if D47.ndim == 0 else _np.diag(CM_D47)**.5 543 544 if D47 is not None: 545 546 T = self._T_from_D47(D47) 547 Np = len(self.degrees) 548 N = T.size 549 550 ### Compute covariance matrix of (*bfp, *T): 551 CM = _np.zeros((Np+N, Np+N)) 552 553 if error_from in ['calib', 'both']: 554 CM[:Np, :Np] = self.bfp_CM[:,:] 555 556 if (sD47 is not None) and error_from in ['sD47', 'both']: 557 _sD47 = _np.asarray(sD47) 558 if _sD47.ndim == 0: 559 for k in range(N): 560 CM[Np+k, Np+k] = _sD47**2 561 elif _sD47.ndim == 1: 562 for k in range(N): 563 CM[Np+k, Np+k] = _sD47[k]**2 564 elif _sD47.ndim == 2: 565 CM[-N:, -N:] = _sD47[:,:] 566 567 ### Compute Jacobian of T(D47) relative to (*bfp, *D47): 568 _D47 = _np.asarray(D47) 569 if _D47.ndim == 0: 570 _D47 = _np.expand_dims(_D47, 0) 571 J = _np.zeros((N, Np+N)) 572 if (sD47 is not None) and error_from in ['sD47', 'both']: 573 for k in range(N): 574 J[k, Np+k] = self._T_from_D47_deriv(_D47[k]) 575 if error_from in ['calib', 'both']: 576 577 xi = _np.linspace(0,200**-1,1001)[1:] 578 for k in range(Np): 579 580 if self.bfp_CM[k,k]: 581 _epsilon_ = self.bfp_CM[k,k]**.5 582 else: 583 _epsilon_ = 1e-6 584 585 p1 = {_: self.bfp[_] for _ in self.bfp} 586 p1[f'a{self.degrees[k]}'] += 0.001 * _epsilon_ 587 T_from_D47_p1 = _interp1d(self.model_fun(p1, xi), xi**-1 - 273.15) 588 589 p2 = {_: self.bfp[_] for _ in self.bfp} 590 p2[f'a{self.degrees[k]}'] -= 0.001 * _epsilon_ 591 T_from_D47_p2 = _interp1d(self.model_fun(p2, xi), xi**-1 - 273.15) 592 593 J[:, k] = (T_from_D47_p1(_D47) - T_from_D47_p2(_D47)) / (0.002 * _epsilon_) 594 595 ### Error propagation: 596 CM_T = J @ CM @ J.T 597 598 if return_covar: 599 return T, CM_T 600 else: 601 return T, float(_np.diag(CM_T)**.5) if T.ndim == 0 else _np.diag(CM_T)**.5
When D47
is input, computes corresponding T value(s).
D47
input may be specified as a scalar, or as a 1-D array.
T
output will then have the same type and size as D47
.
When T
is input, computes corresponding Δ47 value(s).
T
input may be specified as a scalar, or as a 1-D array.
D47
output will then have the same type and size as T
.
Only one of either D47
or T
may be specified as input.
Arguments:
D47
: Δ47 value(s) to convert into temperature (float
or 1-D array)sD47
: Δ47 uncertainties, which may be:None
(default)float
orint
(uniform standard error onD47
)- 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 CsT
: T uncertainties, which may be:None
(default)float
orint
(uniform standard error onT
)- 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
orsD47
) 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 returnedT
orD47
values, otherwise return standard errors for the returnedT
orD47
values instead.
Returns (with D47
input):
T
: temperature value(s) computed fromD47
sT
: uncertainties onT
value(s), whether as standard error(s) or covariance matrix
Returns (with T
input):
D47
: Δ47 value(s) computed fromD47
sD47
: uncertainties onD47
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:
604 def plot_T47_errors( 605 self, 606 calibname = None, 607 rD47 = 0.010, 608 Nr = [2,4,8,12,20], 609 Tmin = 0, 610 Tmax = 120, 611 colors = [(1,0,0),(1,.5,0),(.25,.75,0),(0,.5,1),(0.5,0.5,0.5)], 612 yscale = 'lin', 613 ): 614 """ 615 Plot SE of T reconstructed using the calibration as a function of T for various 616 combinations of analytical precision and number of analytical replicates. 617 618 **Arguments** 619 620 + **calibname**: 621 Which calibration name to display. By default, use `label` attribute. 622 + **rD47**: 623 Analytical precision of a single analysis. 624 + **Nr**: 625 A list of lines to plot, each corresponding to a given number of replicates. 626 + **Tmin**: 627 Minimum T to plot. 628 + **Tmax**: 629 Maximum T to plot. 630 + **colors**: 631 A list of colors to distinguish the plotted lines. 632 + **yscale**: 633 + If `'lin'`, the Y axis uses a linear scale. 634 + If `'log'`, the Y axis uses a logarithmic scale. 635 636 **Example** 637 638 ````py 639 from matplotlib import pyplot as ppl 640 from D47calib import devils_laghetto_2023 as calib 641 642 fig = ppl.figure(figsize = (3.5,4)) 643 ppl.subplots_adjust(bottom = .2, left = .15) 644 calib.plot_T47_errors( 645 calibname = 'Devils Laghetto calibration', 646 Nr = [1,2,4,16], 647 Tmin =0, 648 Tmax = 40, 649 ) 650 ppl.savefig('example_SE_T.png', dpi = 100) 651 ```` 652 653 This should result in something like this: 654 655 <img src="example_SE_T.png"> 656 """ 657 658 if calibname is None: 659 calibname = self.label 660 661 Nr = _np.array(Nr) 662 if len(colors) < Nr.size: 663 print('WARNING: Too few colors to plot different numbers of replicates; generating new colors.') 664 from colorsys import hsv_to_rgb 665 hsv = [(x*1.0/Nr.size, 1, .9) for x in range(Nr.size)] 666 colors = [hsv_to_rgb(*x) for x in hsv] 667 668 Ti = _np.linspace(Tmin, Tmax) 669 D47i, _ = self.T47(T = Ti) 670 _, sT_calib = self.T47(D47 = D47i, error_from = 'calib') 671 672 ymax, ymin = 0, 1e6 673 for N,c in zip(Nr, colors): 674 _, sT = self.T47(D47 = D47i, sD47 = rD47 / N**.5, error_from = 'sD47') 675 _ppl.plot(Ti, sT, '-', color = c, label=f'SE for {N} replicate{"s" if N > 1 else ""}') 676 ymin = min(ymin, min(sT)) 677 ymax = max(ymax, max(sT)) 678 679 _ppl.plot(Ti, sT_calib, 'k--', label='SE from calibration') 680 681 _ppl.legend(fontsize=9) 682 _ppl.xlabel("T (°C)") 683 684 _ppl.ylabel("Standard error on reconstructed T (°C)") 685 686 # yticks([0,.5,1,1.5,2]) 687 _ppl.title(f"{calibname},\nassuming external Δ$_{{47}}$ repeatability of {rD47:.3f} ‰", size = 9) 688 _ppl.grid( alpha = .25) 689 if yscale == 'lin': 690 _ppl.axis([Ti[0], Ti[-1], 0, ymax*1.05]) 691 t1, t2 = self.T.min(), self.T.max() 692 _ppl.plot([t1, t2], [0, 0], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False) 693 _ppl.text((t1+t2)/2, 0, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic') 694 _ppl.axis([None, None, None, _ppl.axis()[-1]*1.25]) 695 elif yscale == 'log': 696 ymin /= 2 697 _ppl.axis([Ti[0], Ti[-1], ymin, ymax*1.05]) 698 _ppl.yscale('log') 699 t1, t2 = self.T.min(), self.T.max() 700 _ppl.plot([t1, t2], [ymin, ymin], 'k-', alpha = .25, lw = 8, solid_capstyle = 'butt', clip_on = False) 701 _ppl.text((t1+t2)/2, ymin, 'range of observations\n', alpha = .4, size = 7, ha = 'center', va = 'bottom', style = 'italic')
Plot SE of T reconstructed using the calibration as a function of T for various combinations of analytical precision and number of analytical replicates.
Arguments
- calibname:
Which calibration name to display. By default, use
label
attribute. - rD47: Analytical precision of a single analysis.
- Nr: A list of lines to plot, each corresponding to a given number of replicates.
- Tmin: Minimum T to plot.
- Tmax: Maximum T to plot.
- colors: A list of colors to distinguish the plotted lines.
- yscale:
- If
'lin'
, the Y axis uses a linear scale. - If
'log'
, the Y axis uses a logarithmic scale.
- If
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:
703 def export_data(self, csvfile, sep = ',', label = False, T_correl = False, D47_correl = False): 704 """ 705 Write calibration data to a csv file. 706 707 ### Parameters 708 709 + **csvfile**: 710 The filename to write data to. 711 + **sep**: 712 The separator between CSV fields. 713 + **label**: 714 + If specified as `True`, include a `Dataset` column with the calibration's `label` attribute. 715 + If specified as a `str`, include a `Dataset` column with that string. 716 + If specified as `False`, do not include a `Dataset` column. 717 + **T_correl**: 718 + If `True`, include correlations between all `T` values. 719 + **D47_correl**: 720 + If `True`, include correlations between all `D47` values. 721 722 ### Example 723 724 ````py 725 D47calib.huyghe_2022.export_data( 726 csvfile = 'example_export_data.csv', 727 T_correl = True, 728 D47_correl = True, 729 ) 730 ```` 731 732 This should result in something like this ([link](example_export_data.csv)): 733 734 .. include:: ../../docs/example_export_data.md 735 736 """ 737 n = len(str(self.N)) 738 739 with open(csvfile, 'w') as f: 740 f.write(sep.join(['ID', 'Sample', 'T', 'SE_T', 'D47', 'SE_D47'])) 741 742 if label: 743 f.write(f'{sep}Dataset') 744 745 if T_correl: 746 inv_diag_sT = _np.diag(_np.diag(self.sT)**-.5) 747 Tcorrel = inv_diag_sT @ self.sT @ inv_diag_sT 748 f.write(sep.join(['']+[f'Tcorrel_{k+1:0{n}d}' for k in range(self.N)])) 749 750 if D47_correl: 751 inv_diag_sD47 = _np.diag(_np.diag(self.sD47)**-.5) 752 D47correl = inv_diag_sD47 @ self.sD47 @ inv_diag_sD47 753 f.write(sep.join(['']+[f'D47correl_{k+1:0{n}d}' for k in range(self.N)])) 754 755 for k, (s, T, sT, D47, sD47) in enumerate(zip( 756 self.samples, 757 self.T, 758 _np.diag(self.sT)**.5, 759 self.D47, 760 _np.diag(self.sD47)**.5, 761 )): 762 f.write('\n' + sep.join([f'{k+1:0{n}d}', s, f'{T:.2f}', f'{sT:.2f}', f'{D47:.4f}', f'{sD47:.4f}'])) 763 if label: 764 if label is True: 765 f.write(f'{sep}{self.label}') 766 else: 767 f.write(f'{sep}{label}') 768 if T_correl: 769 f.write(sep.join(['']+[ 770 f'{Tcorrel[k,_]:.0f}' 771 if f'{Tcorrel[k,_]:.6f}'[-6:] == '000000' 772 else f'{Tcorrel[k,_]:.6f}' 773 for _ in range(self.N)])) 774 if D47_correl: 775 f.write(sep.join(['']+[ 776 f'{D47correl[k,_]:.0f}' 777 if f'{D47correl[k,_]:.6f}'[-6:] == '000000' 778 else f'{D47correl[k,_]:.6f}' 779 for _ in range(self.N)]))
Write calibration data to a csv file.
Parameters
- csvfile: The filename to write data to.
- sep: The separator between CSV fields.
- label:
- If specified as
True
, include aDataset
column with the calibration'slabel
attribute. - If specified as a
str
, include aDataset
column with that string. - If specified as
False
, do not include aDataset
column.
- If specified as
- T_correl:
- If
True
, include correlations between allT
values.
- If
- D47_correl:
- If
True
, include correlations between allD47
values.
- If
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 |
782 def export(self, name, filename): 783 """ 784 Save `D47calib` object as an importable file. 785 786 ### Parameters 787 788 + **name**: 789 The name of the variable to export. 790 + **filename**: 791 The filename to write to. 792 793 ### Example 794 795 ````py 796 D47calib.anderson_2021_lsce.export('foo', 'bar.py') 797 ```` 798 799 This should result in a `bar.py` file with the following contents: 800 801 ````py 802 foo = D47calib( 803 samples = ['LGB-2', 'DVH-2'], 804 T = [7.9, 33.7], 805 D47 = [0.6485720997671647, 0.5695972909966959], 806 sT = [[0.04000000000000001, 0.0], [0.0, 0.04000000000000001]], 807 sD47 = [[8.72797097773764e-06, 2.951894073404263e-06], [2.9518940734042614e-06, 7.498611746762038e-06]], 808 description = 'Devils Hole & Laghetto Basso from Anderson et al. (2021), processed in I-CDES', 809 label = 'Slow-growing calcites from Anderson et al. (2021)', 810 color = (0, 0.5, 0), 811 degrees = [0, 2], 812 bfp = {'a0': 0.1583220210575451, 'a2': 38724.41371782721}, 813 bfp_CM = [[0.00035908667755871876, -30.707016431538836], [-30.70701643153884, 2668091.396598919]], 814 chisq = 6.421311854486162e-27, 815 Nf = 0, 816 ) 817 ```` 818 """ 819 with open(filename, 'w') as f: 820 f.write(f''' 821{name} = D47calib( 822 samples = {self.samples}, 823 T = {list(self.T)}, 824 D47 = {list(self.D47)}, 825 sT = {[list(l) for l in self.sT]}, 826 sD47 = {[list(l) for l in self.sD47]}, 827 degrees = {self.degrees}, 828 description = {repr(self.description)}, 829 name = {repr(self.name)}, 830 label = {repr(self.label)}, 831 bfp = {self.bfp}, 832 bfp_CM = {[list(l) for l in self.bfp_CM]}, 833 chisq = {self.chisq}, 834 cholesky_residuals = {list(self.cholesky_residuals)}, 835 aic = {self.aic}, 836 bic = {self.bic}, 837 ks_pvalue = {self.ks_pvalue}, 838 ) 839''')
Save D47calib
object as an importable file.
Parameters
- name: The name of the variable to export.
- filename: The filename to write to.
Example
D47calib.anderson_2021_lsce.export('foo', 'bar.py')
This should result in a bar.py
file with the following contents:
foo = D47calib(
samples = ['LGB-2', 'DVH-2'],
T = [7.9, 33.7],
D47 = [0.6485720997671647, 0.5695972909966959],
sT = [[0.04000000000000001, 0.0], [0.0, 0.04000000000000001]],
sD47 = [[8.72797097773764e-06, 2.951894073404263e-06], [2.9518940734042614e-06, 7.498611746762038e-06]],
description = 'Devils Hole & Laghetto Basso from Anderson et al. (2021), processed in I-CDES',
label = 'Slow-growing calcites from Anderson et al. (2021)',
color = (0, 0.5, 0),
degrees = [0, 2],
bfp = {'a0': 0.1583220210575451, 'a2': 38724.41371782721},
bfp_CM = [[0.00035908667755871876, -30.707016431538836], [-30.70701643153884, 2668091.396598919]],
chisq = 6.421311854486162e-27,
Nf = 0,
)
841def combine_D47calibs(calibs, degrees = [0,2], same_T = []): 842 ''' 843 Combine data from several `D47calib` instances. 844 845 ### Parameters 846 847 + **calibs**: 848 A list of `D47calib` instances 849 + **degrees**: 850 The polynomial degrees of the combined regression. 851 + **same_T**: 852 Use this `list` to specify when samples from different calibrations are known/postulated 853 to have formed at the same temperature (e.g. `DVH-2` and `DHC2-8` from the `fiebig_2021` 854 and `anderson_2021_lsce` data sets). Each element of `same_T` is a `list` with the names 855 of two or more samples formed at the same temperature. 856 857 For example, the `ogls_2023` calibration is computed with: 858 859 `same_T = [['DVH-2', DHC-2-8'], ['ETH-1-1100-SAM', 'ETH-1-1100']]` 860 861 Note that when samples from different calibrations have the same name, 862 it is not necessary to explicitly list them in `same_T`. 863 864 Also note that the regression will fail if samples listed together in `same_T` 865 actually have different `T` values specified in the original calibrations. 866 867 ### Example 868 869 The `devils_laghetto_2023` calibration is computed using the following code: 870 871 ````py 872 K = [fiebig_2021.samples.index(_) for _ in ['LGB-2', 'DVH-2', 'DHC2-8']] 873 874 fiebig_temp = D47calib( 875 samples = [fiebig_2021.samples[_] for _ in K], 876 T = fiebig_2021.T[K], 877 D47 = fiebig_2021.D47[K], 878 sT = fiebig_2021.sT[K,:][:,K], 879 sD47 = fiebig_2021.sD47[K,:][:,K], 880 ) 881 882 devils_laghetto_2023 = combine_D47calibs( 883 calibs = [ 884 anderson_2021_lsce, 885 fiebig_temp, 886 ], 887 degrees = [0,2], 888 same_T = [ 889 {'DVH-2', 'DHC2-8'}, 890 ], 891 ) 892 ```` 893 ''' 894 895 samples = [s for c in calibs for s in c.samples] 896 T = [t for c in calibs for t in c.T] 897 D47 = [x for c in calibs for x in c.D47] 898 sD47 = _block_diag(*[c.sD47 for c in calibs]) 899 sT = _block_diag(*[c.sT for c in calibs]) 900 901 for i in range(len(samples)): 902 for j in range(len(samples)): 903 if i != j: 904 if (samples[i] == samples[j] or 905 any([samples[i] in _ and samples[j] in _ for _ in same_T])): 906 907 sT[i,j] = (sT[i,i] * sT[j,j])**.5 908 909 calib = D47calib( 910 samples = samples, 911 T = T, 912 D47 = D47, 913 sT = sT, 914 sD47 = sD47, 915 degrees = degrees, 916 ) 917 918 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
andDHC2-8
from thefiebig_2021
andanderson_2021_lsce
data sets). Each element ofsame_T
is alist
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'},
],
)