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