D47crunch
Standardization and analytical error propagation of Δ47 and Δ48 clumped-isotope measurements
Process and standardize carbonate and/or CO2 clumped-isotope analyses, from low-level data out of a dual-inlet mass spectrometer to final, “absolute” Δ47 and Δ48 values with fully propagated analytical error estimates (Daëron, 2021).
The tutorial section takes you through a series of simple steps to import/process data and print out the results. The how-to section provides instructions applicable to various specific tasks.
1. Tutorial
1.1 Installation
The easy option is to use pip
; open a shell terminal and simply type:
python -m pip install D47crunch
For those wishing to experiment with the bleeding-edge development version, this can be done through the following steps:
- Download the
dev
branch source code here and rename it toD47crunch.py
. - Do any of the following:
- copy
D47crunch.py
to somewhere in your Python path - copy
D47crunch.py
to a working directory (import D47crunch
will only work if called within that directory) - copy
D47crunch.py
to any other location (e.g.,/foo/bar
) and then use the following code snippet in your own code to importD47crunch
:
- copy
import sys
sys.path.append('/foo/bar')
import D47crunch
Documentation for the development version can be downloaded here (save html file and open it locally).
1.2 Usage
Start by creating a file named rawdata.csv
with the following contents:
UID, Sample, d45, d46, d47, d48, d49
A01, ETH-1, 5.79502, 11.62767, 16.89351, 24.56708, 0.79486
A02, MYSAMPLE-1, 6.21907, 11.49107, 17.27749, 24.58270, 1.56318
A03, ETH-2, -6.05868, -4.81718, -11.63506, -10.32578, 0.61352
A04, MYSAMPLE-2, -3.86184, 4.94184, 0.60612, 10.52732, 0.57118
A05, ETH-3, 5.54365, 12.05228, 17.40555, 25.96919, 0.74608
A06, ETH-2, -6.06706, -4.87710, -11.69927, -10.64421, 1.61234
A07, ETH-1, 5.78821, 11.55910, 16.80191, 24.56423, 1.47963
A08, MYSAMPLE-2, -3.87692, 4.86889, 0.52185, 10.40390, 1.07032
Then instantiate a D47data
object which will store and process this data:
import D47crunch
mydata = D47crunch.D47data()
For now, this object is empty:
>>> print(mydata)
[]
To load the analyses saved in rawdata.csv
into our D47data
object and process the data:
mydata.read('rawdata.csv')
# compute δ13C, δ18O of working gas:
mydata.wg()
# compute δ13C, δ18O, raw Δ47 values for each analysis:
mydata.crunch()
# compute absolute Δ47 values for each analysis
# as well as average Δ47 values for each sample:
mydata.standardize()
We can now print a summary of the data processing:
>>> mydata.summary(verbose = True, save_to_file = False)
[summary]
––––––––––––––––––––––––––––––– –––––––––
N samples (anchors + unknowns) 5 (3 + 2)
N analyses (anchors + unknowns) 8 (5 + 3)
Repeatability of δ13C_VPDB 4.2 ppm
Repeatability of δ18O_VSMOW 47.5 ppm
Repeatability of Δ47 (anchors) 13.4 ppm
Repeatability of Δ47 (unknowns) 2.5 ppm
Repeatability of Δ47 (all) 9.6 ppm
Model degrees of freedom 3
Student's 95% t-factor 3.18
Standardization method pooled
––––––––––––––––––––––––––––––– –––––––––
This tells us that our data set contains 5 different samples: 3 anchors (ETH-1, ETH-2, ETH-3) and 2 unknowns (MYSAMPLE-1, MYSAMPLE-2). The total number of analyses is 8, with 5 anchor analyses and 3 unknown analyses. We get an estimate of the analytical repeatability (i.e. the overall, pooled standard deviation) for δ13C, δ18O and Δ47, as well as the number of degrees of freedom (here, 3) that these estimated standard deviations are based on, along with the corresponding Student's t-factor (here, 3.18) for 95 % confidence limits. Finally, the summary indicates that we used a “pooled” standardization approach (see [Daëron, 2021]).
To see the actual results:
>>> mydata.table_of_samples(verbose = True, save_to_file = False)
[table_of_samples]
–––––––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– ––––––––
Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene
–––––––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– ––––––––
ETH-1 2 2.01 37.01 0.2052 0.0131
ETH-2 2 -10.17 19.88 0.2085 0.0026
ETH-3 1 1.73 37.49 0.6132
MYSAMPLE-1 1 2.48 36.90 0.2996 0.0091 ± 0.0291
MYSAMPLE-2 2 -8.17 30.05 0.6600 0.0115 ± 0.0366 0.0025
–––––––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– ––––––––
This table lists, for each sample, the number of analytical replicates, average δ13C and δ18O values (for the analyte CO2 , not for the carbonate itself), the average Δ47 value and the SD of Δ47 for all replicates of this sample. For unknown samples, the SE and 95 % confidence limits for mean Δ47 are also listed These 95 % CL take into account the number of degrees of freedom of the regression model, so that in large datasets the 95 % CL will tend to 1.96 times the SE, but in this case the applicable t-factor is much larger.
We can also generate a table of all analyses in the data set (again, note that d18O_VSMOW
is the composition of the CO2 analyte):
>>> mydata.table_of_analyses(verbose = True, save_to_file = False)
[table_of_analyses]
––– ––––––––– –––––––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––– –––––––––– –––––––––– ––––––––– ––––––––– –––––––––– ––––––––
UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47
––– ––––––––– –––––––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––– –––––––––– –––––––––– ––––––––– ––––––––– –––––––––– ––––––––
A01 mySession ETH-1 -3.807 24.921 5.795020 11.627670 16.893510 24.567080 0.794860 2.014086 37.041843 -0.574686 1.149684 -27.690250 0.214454
A02 mySession MYSAMPLE-1 -3.807 24.921 6.219070 11.491070 17.277490 24.582700 1.563180 2.476827 36.898281 -0.499264 1.435380 -27.122614 0.299589
A03 mySession ETH-2 -3.807 24.921 -6.058680 -4.817180 -11.635060 -10.325780 0.613520 -10.166796 19.907706 -0.685979 -0.721617 16.716901 0.206693
A04 mySession MYSAMPLE-2 -3.807 24.921 -3.861840 4.941840 0.606120 10.527320 0.571180 -8.159927 30.087230 -0.248531 0.613099 -4.979413 0.658270
A05 mySession ETH-3 -3.807 24.921 5.543650 12.052280 17.405550 25.969190 0.746080 1.727029 37.485567 -0.226150 1.678699 -28.280301 0.613200
A06 mySession ETH-2 -3.807 24.921 -6.067060 -4.877100 -11.699270 -10.644210 1.612340 -10.173599 19.845192 -0.683054 -0.922832 17.861363 0.210328
A07 mySession ETH-1 -3.807 24.921 5.788210 11.559100 16.801910 24.564230 1.479630 2.009281 36.970298 -0.591129 1.282632 -26.888335 0.195926
A08 mySession MYSAMPLE-2 -3.807 24.921 -3.876920 4.868890 0.521850 10.403900 1.070320 -8.173486 30.011134 -0.245768 0.636159 -4.324964 0.661803
––– ––––––––– –––––––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––– –––––––––– –––––––––– ––––––––– ––––––––– –––––––––– ––––––––
2. How-to
2.1 Simulate a virtual data set to play with
It is sometimes convenient to quickly build a virtual data set of analyses, for instance to assess the final analytical precision achievable for a given combination of anchor and unknown analyses (see also Fig. 6 of Daëron, 2021).
This can be achieved with virtual_data()
. The example below creates a dataset with four sessions, each of which comprises four analyses of anchor ETH-1, five of ETH-2, six of ETH-3, and two analyses of an unknown sample named FOO
with an arbitrarily defined isotopic composition. Analytical repeatabilities for Δ47 and Δ48 are also specified arbitrarily. See the virtual_data()
documentation for additional configuration parameters.
from D47crunch import *
args = dict(
samples = [
dict(Sample = 'ETH-1', N = 4),
dict(Sample = 'ETH-2', N = 5),
dict(Sample = 'ETH-3', N = 6),
dict(
Sample = 'FOO',
N = 2,
d13C_VPDB = -5.,
d18O_VPDB = -10.,
D47 = 0.3,
D48 = 0.15
),
],
rD47 = 0.010,
rD48 = 0.030,
)
session1 = virtual_data(session = 'Session_01', **args)
session2 = virtual_data(session = 'Session_02', **args)
session3 = virtual_data(session = 'Session_03', **args)
session4 = virtual_data(session = 'Session_04', **args)
D = D47data(session1 + session2 + session3 + session4)
D.crunch()
D.standardize()
D.table_of_sessions(verbose = True, save_to_file = False)
D.table_of_samples(verbose = True, save_to_file = False)
D.table_of_analyses(verbose = True, save_to_file = False)
2.2 Control data quality
D47crunch
offers several tools to visualize processed data. The examples below use the same virtual data set, generated with:
from D47crunch import *
from random import shuffle
# generate virtual data:
args = dict(
samples = [
dict(Sample = 'ETH-1', N = 8),
dict(Sample = 'ETH-2', N = 8),
dict(Sample = 'ETH-3', N = 8),
dict(Sample = 'FOO', N = 4,
d13C_VPDB = -5., d18O_VPDB = -10.,
D47 = 0.3, D48 = 0.15),
dict(Sample = 'BAR', N = 4,
d13C_VPDB = -15., d18O_VPDB = -15.,
D47 = 0.5, D48 = 0.2),
])
sessions = [
virtual_data(session = f'Session_{k+1:02.0f}', seed = int('1234567890'[:k+1]), **args)
for k in range(10)]
# shuffle the data:
data = [r for s in sessions for r in s]
shuffle(data)
data = sorted(data, key = lambda r: r['Session'])
# create D47data instance:
data47 = D47data(data)
# process D47data instance:
data47.crunch()
data47.standardize()
2.2.1 Plotting the distribution of analyses through time
data47.plot_distribution_of_analyses(filename = 'time_distribution.pdf')
The plot above shows the succession of analyses as if they were all distributed at regular time intervals. See D4xdata.plot_distribution_of_analyses()
for how to plot analyses as a function of “true” time (based on the TimeTag
for each analysis).
2.2.2 Generating session plots
data47.plot_sessions()
Below is one of the resulting sessions plots. Each cross marker is an analysis. Anchors are in red and unknowns in blue. Short horizontal lines show the nominal Δ47 value for anchors, in red, or the average Δ47 value for unknowns, in blue (overall average for all sessions). Curved grey contours correspond to Δ47 standardization errors in this session.
2.2.3 Plotting Δ47 or Δ48 residuals
data47.plot_residuals(filename = 'residuals.pdf')
Again, note that this plot only shows the succession of analyses as if they were all distributed at regular time intervals.
2.3 Use a different set of anchors, change anchor nominal values, and/or change oxygen-17 correction parameters
Nominal values for various carbonate standards are defined in four places:
D4xdata.Nominal_d13C_VPDB
D4xdata.Nominal_d18O_VPDB
D47data.Nominal_D4x
(also accessible throughD47data.Nominal_D47
)D48data.Nominal_D4x
(also accessible throughD48data.Nominal_D48
)
17O correction parameters are defined by:
D4xdata.R13_VPDB
D4xdata.R18_VSMOW
D4xdata.R18_VPDB
D4xdata.LAMBDA_17
D4xdata.R17_VSMOW
D4xdata.R17_VPDB
When creating a new instance of D47data
or D48data
, the current values of these variables are copied as properties of the new object. Applying custom values for, e.g., R17_VSMOW
and Nominal_D47
can thus be done in several ways:
Option 1: by redefining D4xdata.R17_VSMOW
and D47data.Nominal_D47
_before_ creating a D47data
object:
from D47crunch import D4xdata, D47data
# redefine R17_VSMOW:
D4xdata.R17_VSMOW = 0.00037 # new value
# redefine R17_VPDB for consistency:
D4xdata.R17_VPDB = D4xdata.R17_VSMOW * (D4xdata.R18_VPDB/D4xdata.R18_VSMOW) ** D4xdata.LAMBDA_17
# edit Nominal_D47 to only include ETH-1/2/3:
D47data.Nominal_D4x = {
a: D47data.Nominal_D4x[a]
for a in ['ETH-1', 'ETH-2', 'ETH-3']
}
# redefine ETH-3:
D47data.Nominal_D4x['ETH-3'] = 0.600
# only now create D47data object:
mydata = D47data()
# check the results:
print(mydata.R17_VSMOW, mydata.R17_VPDB)
print(mydata.Nominal_D47)
# NB: mydata.Nominal_D47 is just an alias for mydata.Nominal_D4x
# should print out:
# 0.00037 0.00037599710894149464
# {'ETH-1': 0.2052, 'ETH-2': 0.2085, 'ETH-3': 0.6}
Option 2: by redefining R17_VSMOW
and Nominal_D47
_after_ creating a D47data
object:
from D47crunch import D47data
# first create D47data object:
mydata = D47data()
# redefine R17_VSMOW:
mydata.R17_VSMOW = 0.00037 # new value
# redefine R17_VPDB for consistency:
mydata.R17_VPDB = mydata.R17_VSMOW * (mydata.R18_VPDB/mydata.R18_VSMOW) ** mydata.LAMBDA_17
# edit Nominal_D47 to only include ETH-1/2/3:
mydata.Nominal_D47 = {
a: mydata.Nominal_D47[a]
for a in ['ETH-1', 'ETH-2', 'ETH-3']
}
# redefine ETH-3:
mydata.Nominal_D47['ETH-3'] = 0.600
# check the results:
print(mydata.R17_VSMOW, mydata.R17_VPDB)
print(mydata.Nominal_D47)
# should print out:
# 0.00037 0.00037599710894149464
# {'ETH-1': 0.2052, 'ETH-2': 0.2085, 'ETH-3': 0.6}
The two options above are equivalent, but the latter provides a simple way to compare different data processing choices:
from D47crunch import D47data
# create two D47data objects:
foo = D47data()
bar = D47data()
# modify foo in various ways:
foo.LAMBDA_17 = 0.52
foo.R17_VSMOW = 0.00037 # new value
foo.R17_VPDB = foo.R17_VSMOW * (foo.R18_VPDB/foo.R18_VSMOW) ** foo.LAMBDA_17
foo.Nominal_D47 = {
'ETH-1': foo.Nominal_D47['ETH-1'],
'ETH-2': foo.Nominal_D47['ETH-1'],
'IAEA-C2': foo.Nominal_D47['IAEA-C2'],
'INLAB_REF_MATERIAL': 0.666,
}
# now import the same raw data into foo and bar:
foo.read('rawdata.csv')
foo.wg() # compute δ13C, δ18O of working gas
foo.crunch() # compute all δ13C, δ18O and raw Δ47 values
foo.standardize() # compute absolute Δ47 values
bar.read('rawdata.csv')
bar.wg() # compute δ13C, δ18O of working gas
bar.crunch() # compute all δ13C, δ18O and raw Δ47 values
bar.standardize() # compute absolute Δ47 values
# and compare the final results:
foo.table_of_samples(verbose = True, save_to_file = False)
bar.table_of_samples(verbose = True, save_to_file = False)
2.4 Process paired Δ47 and Δ48 values
Purely in terms of data processing, it is not obvious why Δ47 and Δ48 data should not be handled separately. For now, D47crunch
uses two independent classes — D47data
and D48data
— which crunch numbers and deal with standardization in very similar ways. The following example demonstrates how to print out combined outputs for D47data
and D48data
.
from D47crunch import *
# generate virtual data:
args = dict(
samples = [
dict(Sample = 'ETH-1', N = 3),
dict(Sample = 'ETH-2', N = 3),
dict(Sample = 'ETH-3', N = 3),
dict(Sample = 'FOO', N = 3,
d13C_VPDB = -5., d18O_VPDB = -10.,
D47 = 0.3, D48 = 0.15),
], rD47 = 0.010, rD48 = 0.030)
session1 = virtual_data(session = 'Session_01', **args)
session2 = virtual_data(session = 'Session_02', **args)
# create D47data instance:
data47 = D47data(session1 + session2)
# process D47data instance:
data47.crunch()
data47.standardize()
# create D48data instance:
data48 = D48data(data47) # alternatively: data48 = D48data(session1 + session2)
# process D48data instance:
data48.crunch()
data48.standardize()
# output combined results:
table_of_sessions(data47, data48)
table_of_samples(data47, data48)
table_of_analyses(data47, data48)
Expected output:
–––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– ––––––––––––––– –––––––––––––– –––––– ––––––––––––– ––––––––––––––– ––––––––––––––
Session Na Nu d13Cwg_VPDB d18Owg_VSMOW r_d13C r_d18O r_D47 a_47 ± SE 1e3 x b_47 ± SE c_47 ± SE r_D48 a_48 ± SE 1e3 x b_48 ± SE c_48 ± SE
–––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– ––––––––––––––– –––––––––––––– –––––– ––––––––––––– ––––––––––––––– ––––––––––––––
Session_01 9 3 -4.000 26.000 0.0000 0.0000 0.0098 1.021 ± 0.019 -0.398 ± 0.260 -0.903 ± 0.006 0.0486 0.540 ± 0.151 1.235 ± 0.607 -0.390 ± 0.025
Session_02 9 3 -4.000 26.000 0.0000 0.0000 0.0090 1.015 ± 0.019 0.376 ± 0.260 -0.905 ± 0.006 0.0186 1.350 ± 0.156 -0.871 ± 0.608 -0.504 ± 0.027
–––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– ––––––––––––––– –––––––––––––– –––––– ––––––––––––– ––––––––––––––– ––––––––––––––
–––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– –––––– –––––– –––––––– –––––– ––––––––
Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene D48 SE 95% CL SD p_Levene
–––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– –––––– –––––– –––––––– –––––– ––––––––
ETH-1 6 2.02 37.02 0.2052 0.0078 0.1380 0.0223
ETH-2 6 -10.17 19.88 0.2085 0.0036 0.1380 0.0482
ETH-3 6 1.71 37.45 0.6132 0.0080 0.2700 0.0176
FOO 6 -5.00 28.91 0.3026 0.0044 ± 0.0093 0.0121 0.164 0.1397 0.0121 ± 0.0255 0.0267 0.127
–––––– – ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– –––––– –––––– –––––––– –––––– ––––––––
––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– ––––––––
UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47 D48
––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– ––––––––
1 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.120787 21.286237 27.780042 2.020000 37.024281 -0.708176 -0.316435 -0.000013 0.197297 0.087763
2 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.132240 21.307795 27.780042 2.020000 37.024281 -0.696913 -0.295333 -0.000013 0.208328 0.126791
3 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.132438 21.313884 27.780042 2.020000 37.024281 -0.696718 -0.289374 -0.000013 0.208519 0.137813
4 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700300 -12.210735 -18.023381 -10.170000 19.875825 -0.683938 -0.297902 -0.000002 0.209785 0.198705
5 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.707421 -12.270781 -18.023381 -10.170000 19.875825 -0.691145 -0.358673 -0.000002 0.202726 0.086308
6 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700061 -12.278310 -18.023381 -10.170000 19.875825 -0.683696 -0.366292 -0.000002 0.210022 0.072215
7 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.684379 22.225827 28.306614 1.710000 37.450394 -0.273094 -0.216392 -0.000014 0.623472 0.270873
8 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.660163 22.233729 28.306614 1.710000 37.450394 -0.296906 -0.208664 -0.000014 0.600150 0.285167
9 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.675191 22.215632 28.306614 1.710000 37.450394 -0.282128 -0.226363 -0.000014 0.614623 0.252432
10 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.328380 5.374933 4.665655 -5.000000 28.907344 -0.582131 -0.288924 -0.000006 0.314928 0.175105
11 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.302220 5.384454 4.665655 -5.000000 28.907344 -0.608241 -0.279457 -0.000006 0.289356 0.192614
12 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.322530 5.372841 4.665655 -5.000000 28.907344 -0.587970 -0.291004 -0.000006 0.309209 0.171257
13 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.140853 21.267202 27.780042 2.020000 37.024281 -0.688442 -0.335067 -0.000013 0.207730 0.138730
14 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.127087 21.256983 27.780042 2.020000 37.024281 -0.701980 -0.345071 -0.000013 0.194396 0.131311
15 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.148253 21.287779 27.780042 2.020000 37.024281 -0.681165 -0.314926 -0.000013 0.214898 0.153668
16 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715859 -12.204791 -18.023381 -10.170000 19.875825 -0.699685 -0.291887 -0.000002 0.207349 0.149128
17 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.709763 -12.188685 -18.023381 -10.170000 19.875825 -0.693516 -0.275587 -0.000002 0.213426 0.161217
18 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715427 -12.253049 -18.023381 -10.170000 19.875825 -0.699249 -0.340727 -0.000002 0.207780 0.112907
19 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.685994 22.249463 28.306614 1.710000 37.450394 -0.271506 -0.193275 -0.000014 0.618328 0.244431
20 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.681351 22.298166 28.306614 1.710000 37.450394 -0.276071 -0.145641 -0.000014 0.613831 0.279758
21 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.676169 22.306848 28.306614 1.710000 37.450394 -0.281167 -0.137150 -0.000014 0.608813 0.286056
22 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.324359 5.339497 4.665655 -5.000000 28.907344 -0.586144 -0.324160 -0.000006 0.314015 0.136535
23 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.297658 5.325854 4.665655 -5.000000 28.907344 -0.612794 -0.337727 -0.000006 0.287767 0.126473
24 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.310185 5.339898 4.665655 -5.000000 28.907344 -0.600291 -0.323761 -0.000006 0.300082 0.136830
––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– ––––––––
API Documentation
1''' 2Standardization and analytical error propagation of Δ47 and Δ48 clumped-isotope measurements 3 4Process and standardize carbonate and/or CO2 clumped-isotope analyses, 5from low-level data out of a dual-inlet mass spectrometer to final, “absolute” 6Δ47 and Δ48 values with fully propagated analytical error estimates 7([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). 8 9The **tutorial** section takes you through a series of simple steps to import/process data and print out the results. 10The **how-to** section provides instructions applicable to various specific tasks. 11 12.. include:: ../docs/tutorial.md 13.. include:: ../docs/howto.md 14 15## API Documentation 16''' 17 18__docformat__ = "restructuredtext" 19__author__ = 'Mathieu Daëron' 20__contact__ = 'daeron@lsce.ipsl.fr' 21__copyright__ = 'Copyright (c) 2023 Mathieu Daëron' 22__license__ = 'Modified BSD License - https://opensource.org/licenses/BSD-3-Clause' 23__date__ = '2023-05-13' 24__version__ = '2.0.6' 25 26import os 27import numpy as np 28from statistics import stdev 29from scipy.stats import t as tstudent 30from scipy.stats import levene 31from scipy.interpolate import interp1d 32from numpy import linalg 33from lmfit import Minimizer, Parameters, report_fit 34from matplotlib import pyplot as ppl 35from datetime import datetime as dt 36from functools import wraps 37from colorsys import hls_to_rgb 38from matplotlib import rcParams 39 40rcParams['font.family'] = 'sans-serif' 41rcParams['font.sans-serif'] = 'Helvetica' 42rcParams['font.size'] = 10 43rcParams['mathtext.fontset'] = 'custom' 44rcParams['mathtext.rm'] = 'sans' 45rcParams['mathtext.bf'] = 'sans:bold' 46rcParams['mathtext.it'] = 'sans:italic' 47rcParams['mathtext.cal'] = 'sans:italic' 48rcParams['mathtext.default'] = 'rm' 49rcParams['xtick.major.size'] = 4 50rcParams['xtick.major.width'] = 1 51rcParams['ytick.major.size'] = 4 52rcParams['ytick.major.width'] = 1 53rcParams['axes.grid'] = False 54rcParams['axes.linewidth'] = 1 55rcParams['grid.linewidth'] = .75 56rcParams['grid.linestyle'] = '-' 57rcParams['grid.alpha'] = .15 58rcParams['savefig.dpi'] = 150 59 60Petersen_etal_CO2eqD47 = np.array([[-12, 1.147113572], [-11, 1.139961218], [-10, 1.132872856], [-9, 1.125847677], [-8, 1.118884889], [-7, 1.111983708], [-6, 1.105143366], [-5, 1.098363105], [-4, 1.091642182], [-3, 1.084979862], [-2, 1.078375423], [-1, 1.071828156], [0, 1.065337360], [1, 1.058902349], [2, 1.052522443], [3, 1.046196976], [4, 1.039925291], [5, 1.033706741], [6, 1.027540690], [7, 1.021426510], [8, 1.015363585], [9, 1.009351306], [10, 1.003389075], [11, 0.997476303], [12, 0.991612409], [13, 0.985796821], [14, 0.980028975], [15, 0.974308318], [16, 0.968634304], [17, 0.963006392], [18, 0.957424055], [19, 0.951886769], [20, 0.946394020], [21, 0.940945302], [22, 0.935540114], [23, 0.930177964], [24, 0.924858369], [25, 0.919580851], [26, 0.914344938], [27, 0.909150167], [28, 0.903996080], [29, 0.898882228], [30, 0.893808167], [31, 0.888773459], [32, 0.883777672], [33, 0.878820382], [34, 0.873901170], [35, 0.869019623], [36, 0.864175334], [37, 0.859367901], [38, 0.854596929], [39, 0.849862028], [40, 0.845162813], [41, 0.840498905], [42, 0.835869931], [43, 0.831275522], [44, 0.826715314], [45, 0.822188950], [46, 0.817696075], [47, 0.813236341], [48, 0.808809404], [49, 0.804414926], [50, 0.800052572], [51, 0.795722012], [52, 0.791422922], [53, 0.787154979], [54, 0.782917869], [55, 0.778711277], [56, 0.774534898], [57, 0.770388426], [58, 0.766271562], [59, 0.762184010], [60, 0.758125479], [61, 0.754095680], [62, 0.750094329], [63, 0.746121147], [64, 0.742175856], [65, 0.738258184], [66, 0.734367860], [67, 0.730504620], [68, 0.726668201], [69, 0.722858343], [70, 0.719074792], [71, 0.715317295], [72, 0.711585602], [73, 0.707879469], [74, 0.704198652], [75, 0.700542912], [76, 0.696912012], [77, 0.693305719], [78, 0.689723802], [79, 0.686166034], [80, 0.682632189], [81, 0.679122047], [82, 0.675635387], [83, 0.672171994], [84, 0.668731654], [85, 0.665314156], [86, 0.661919291], [87, 0.658546854], [88, 0.655196641], [89, 0.651868451], [90, 0.648562087], [91, 0.645277352], [92, 0.642014054], [93, 0.638771999], [94, 0.635551001], [95, 0.632350872], [96, 0.629171428], [97, 0.626012487], [98, 0.622873870], [99, 0.619755397], [100, 0.616656895], [102, 0.610519107], [104, 0.604459143], [106, 0.598475670], [108, 0.592567388], [110, 0.586733026], [112, 0.580971342], [114, 0.575281125], [116, 0.569661187], [118, 0.564110371], [120, 0.558627545], [122, 0.553211600], [124, 0.547861454], [126, 0.542576048], [128, 0.537354347], [130, 0.532195337], [132, 0.527098028], [134, 0.522061450], [136, 0.517084654], [138, 0.512166711], [140, 0.507306712], [142, 0.502503768], [144, 0.497757006], [146, 0.493065573], [148, 0.488428634], [150, 0.483845370], [152, 0.479314980], [154, 0.474836677], [156, 0.470409692], [158, 0.466033271], [160, 0.461706674], [162, 0.457429176], [164, 0.453200067], [166, 0.449018650], [168, 0.444884242], [170, 0.440796174], [172, 0.436753787], [174, 0.432756438], [176, 0.428803494], [178, 0.424894334], [180, 0.421028350], [182, 0.417204944], [184, 0.413423530], [186, 0.409683531], [188, 0.405984383], [190, 0.402325531], [192, 0.398706429], [194, 0.395126543], [196, 0.391585347], [198, 0.388082324], [200, 0.384616967], [202, 0.381188778], [204, 0.377797268], [206, 0.374441954], [208, 0.371122364], [210, 0.367838033], [212, 0.364588505], [214, 0.361373329], [216, 0.358192065], [218, 0.355044277], [220, 0.351929540], [222, 0.348847432], [224, 0.345797540], [226, 0.342779460], [228, 0.339792789], [230, 0.336837136], [232, 0.333912113], [234, 0.331017339], [236, 0.328152439], [238, 0.325317046], [240, 0.322510795], [242, 0.319733329], [244, 0.316984297], [246, 0.314263352], [248, 0.311570153], [250, 0.308904364], [252, 0.306265654], [254, 0.303653699], [256, 0.301068176], [258, 0.298508771], [260, 0.295975171], [262, 0.293467070], [264, 0.290984167], [266, 0.288526163], [268, 0.286092765], [270, 0.283683684], [272, 0.281298636], [274, 0.278937339], [276, 0.276599517], [278, 0.274284898], [280, 0.271993211], [282, 0.269724193], [284, 0.267477582], [286, 0.265253121], [288, 0.263050554], [290, 0.260869633], [292, 0.258710110], [294, 0.256571741], [296, 0.254454286], [298, 0.252357508], [300, 0.250281174], [302, 0.248225053], [304, 0.246188917], [306, 0.244172542], [308, 0.242175707], [310, 0.240198194], [312, 0.238239786], [314, 0.236300272], [316, 0.234379441], [318, 0.232477087], [320, 0.230593005], [322, 0.228726993], [324, 0.226878853], [326, 0.225048388], [328, 0.223235405], [330, 0.221439711], [332, 0.219661118], [334, 0.217899439], [336, 0.216154491], [338, 0.214426091], [340, 0.212714060], [342, 0.211018220], [344, 0.209338398], [346, 0.207674420], [348, 0.206026115], [350, 0.204393315], [355, 0.200378063], [360, 0.196456139], [365, 0.192625077], [370, 0.188882487], [375, 0.185226048], [380, 0.181653511], [385, 0.178162694], [390, 0.174751478], [395, 0.171417807], [400, 0.168159686], [405, 0.164975177], [410, 0.161862398], [415, 0.158819521], [420, 0.155844772], [425, 0.152936426], [430, 0.150092806], [435, 0.147312286], [440, 0.144593281], [445, 0.141934254], [450, 0.139333710], [455, 0.136790195], [460, 0.134302294], [465, 0.131868634], [470, 0.129487876], [475, 0.127158722], [480, 0.124879906], [485, 0.122650197], [490, 0.120468398], [495, 0.118333345], [500, 0.116243903], [505, 0.114198970], [510, 0.112197471], [515, 0.110238362], [520, 0.108320625], [525, 0.106443271], [530, 0.104605335], [535, 0.102805877], [540, 0.101043985], [545, 0.099318768], [550, 0.097629359], [555, 0.095974915], [560, 0.094354612], [565, 0.092767650], [570, 0.091213248], [575, 0.089690648], [580, 0.088199108], [585, 0.086737906], [590, 0.085306341], [595, 0.083903726], [600, 0.082529395], [605, 0.081182697], [610, 0.079862998], [615, 0.078569680], [620, 0.077302141], [625, 0.076059794], [630, 0.074842066], [635, 0.073648400], [640, 0.072478251], [645, 0.071331090], [650, 0.070206399], [655, 0.069103674], [660, 0.068022424], [665, 0.066962168], [670, 0.065922439], [675, 0.064902780], [680, 0.063902748], [685, 0.062921909], [690, 0.061959837], [695, 0.061016122], [700, 0.060090360], [705, 0.059182157], [710, 0.058291131], [715, 0.057416907], [720, 0.056559120], [725, 0.055717414], [730, 0.054891440], [735, 0.054080860], [740, 0.053285343], [745, 0.052504565], [750, 0.051738210], [755, 0.050985971], [760, 0.050247546], [765, 0.049522643], [770, 0.048810974], [775, 0.048112260], [780, 0.047426227], [785, 0.046752609], [790, 0.046091145], [795, 0.045441581], [800, 0.044803668], [805, 0.044177164], [810, 0.043561831], [815, 0.042957438], [820, 0.042363759], [825, 0.041780573], [830, 0.041207664], [835, 0.040644822], [840, 0.040091839], [845, 0.039548516], [850, 0.039014654], [855, 0.038490063], [860, 0.037974554], [865, 0.037467944], [870, 0.036970054], [875, 0.036480707], [880, 0.035999734], [885, 0.035526965], [890, 0.035062238], [895, 0.034605393], [900, 0.034156272], [905, 0.033714724], [910, 0.033280598], [915, 0.032853749], [920, 0.032434032], [925, 0.032021309], [930, 0.031615443], [935, 0.031216300], [940, 0.030823749], [945, 0.030437663], [950, 0.030057915], [955, 0.029684385], [960, 0.029316951], [965, 0.028955498], [970, 0.028599910], [975, 0.028250075], [980, 0.027905884], [985, 0.027567229], [990, 0.027234006], [995, 0.026906112], [1000, 0.026583445], [1005, 0.026265908], [1010, 0.025953405], [1015, 0.025645841], [1020, 0.025343124], [1025, 0.025045163], [1030, 0.024751871], [1035, 0.024463160], [1040, 0.024178947], [1045, 0.023899147], [1050, 0.023623680], [1055, 0.023352467], [1060, 0.023085429], [1065, 0.022822491], [1070, 0.022563577], [1075, 0.022308615], [1080, 0.022057533], [1085, 0.021810260], [1090, 0.021566729], [1095, 0.021326872], [1100, 0.021090622]]) 61_fCO2eqD47_Petersen = interp1d(Petersen_etal_CO2eqD47[:,0], Petersen_etal_CO2eqD47[:,1]) 62def fCO2eqD47_Petersen(T): 63 ''' 64 CO2 equilibrium Δ47 value as a function of T (in degrees C) 65 according to [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127). 66 67 ''' 68 return float(_fCO2eqD47_Petersen(T)) 69 70 71Wang_etal_CO2eqD47 = np.array([[-83., 1.8954], [-73., 1.7530], [-63., 1.6261], [-53., 1.5126], [-43., 1.4104], [-33., 1.3182], [-23., 1.2345], [-13., 1.1584], [-3., 1.0888], [7., 1.0251], [17., 0.9665], [27., 0.9125], [37., 0.8626], [47., 0.8164], [57., 0.7734], [67., 0.7334], [87., 0.6612], [97., 0.6286], [107., 0.5980], [117., 0.5693], [127., 0.5423], [137., 0.5169], [147., 0.4930], [157., 0.4704], [167., 0.4491], [177., 0.4289], [187., 0.4098], [197., 0.3918], [207., 0.3747], [217., 0.3585], [227., 0.3431], [237., 0.3285], [247., 0.3147], [257., 0.3015], [267., 0.2890], [277., 0.2771], [287., 0.2657], [297., 0.2550], [307., 0.2447], [317., 0.2349], [327., 0.2256], [337., 0.2167], [347., 0.2083], [357., 0.2002], [367., 0.1925], [377., 0.1851], [387., 0.1781], [397., 0.1714], [407., 0.1650], [417., 0.1589], [427., 0.1530], [437., 0.1474], [447., 0.1421], [457., 0.1370], [467., 0.1321], [477., 0.1274], [487., 0.1229], [497., 0.1186], [507., 0.1145], [517., 0.1105], [527., 0.1068], [537., 0.1031], [547., 0.0997], [557., 0.0963], [567., 0.0931], [577., 0.0901], [587., 0.0871], [597., 0.0843], [607., 0.0816], [617., 0.0790], [627., 0.0765], [637., 0.0741], [647., 0.0718], [657., 0.0695], [667., 0.0674], [677., 0.0654], [687., 0.0634], [697., 0.0615], [707., 0.0597], [717., 0.0579], [727., 0.0562], [737., 0.0546], [747., 0.0530], [757., 0.0515], [767., 0.0500], [777., 0.0486], [787., 0.0472], [797., 0.0459], [807., 0.0447], [817., 0.0435], [827., 0.0423], [837., 0.0411], [847., 0.0400], [857., 0.0390], [867., 0.0380], [877., 0.0370], [887., 0.0360], [897., 0.0351], [907., 0.0342], [917., 0.0333], [927., 0.0325], [937., 0.0317], [947., 0.0309], [957., 0.0302], [967., 0.0294], [977., 0.0287], [987., 0.0281], [997., 0.0274], [1007., 0.0268], [1017., 0.0261], [1027., 0.0255], [1037., 0.0249], [1047., 0.0244], [1057., 0.0238], [1067., 0.0233], [1077., 0.0228], [1087., 0.0223], [1097., 0.0218]]) 72_fCO2eqD47_Wang = interp1d(Wang_etal_CO2eqD47[:,0] - 0.15, Wang_etal_CO2eqD47[:,1]) 73def fCO2eqD47_Wang(T): 74 ''' 75 CO2 equilibrium Δ47 value as a function of `T` (in degrees C) 76 according to [Wang et al. (2004)](https://doi.org/10.1016/j.gca.2004.05.039) 77 (supplementary data of [Dennis et al., 2011](https://doi.org/10.1016/j.gca.2011.09.025)). 78 ''' 79 return float(_fCO2eqD47_Wang(T)) 80 81 82def correlated_sum(X, C, w = None): 83 ''' 84 Compute covariance-aware linear combinations 85 86 **Parameters** 87 88 + `X`: list or 1-D array of values to sum 89 + `C`: covariance matrix for the elements of `X` 90 + `w`: list or 1-D array of weights to apply to the elements of `X` 91 (all equal to 1 by default) 92 93 Return the sum (and its SE) of the elements of `X`, with optional weights equal 94 to the elements of `w`, accounting for covariances between the elements of `X`. 95 ''' 96 if w is None: 97 w = [1 for x in X] 98 return np.dot(w,X), (np.dot(w,np.dot(C,w)))**.5 99 100 101def make_csv(x, hsep = ',', vsep = '\n'): 102 ''' 103 Formats a list of lists of strings as a CSV 104 105 **Parameters** 106 107 + `x`: the list of lists of strings to format 108 + `hsep`: the field separator (`,` by default) 109 + `vsep`: the line-ending convention to use (`\\n` by default) 110 111 **Example** 112 113 ```py 114 print(make_csv([['a', 'b', 'c'], ['d', 'e', 'f']])) 115 ``` 116 117 outputs: 118 119 ```py 120 a,b,c 121 d,e,f 122 ``` 123 ''' 124 return vsep.join([hsep.join(l) for l in x]) 125 126 127def pf(txt): 128 ''' 129 Modify string `txt` to follow `lmfit.Parameter()` naming rules. 130 ''' 131 return txt.replace('-','_').replace('.','_').replace(' ','_') 132 133 134def smart_type(x): 135 ''' 136 Tries to convert string `x` to a float if it includes a decimal point, or 137 to an integer if it does not. If both attempts fail, return the original 138 string unchanged. 139 ''' 140 try: 141 y = float(x) 142 except ValueError: 143 return x 144 if '.' not in x: 145 return int(y) 146 return y 147 148 149def pretty_table(x, header = 1, hsep = ' ', vsep = '–', align = '<'): 150 ''' 151 Reads a list of lists of strings and outputs an ascii table 152 153 **Parameters** 154 155 + `x`: a list of lists of strings 156 + `header`: the number of lines to treat as header lines 157 + `hsep`: the horizontal separator between columns 158 + `vsep`: the character to use as vertical separator 159 + `align`: string of left (`<`) or right (`>`) alignment characters. 160 161 **Example** 162 163 ```py 164 x = [['A', 'B', 'C'], ['1', '1.9999', 'foo'], ['10', 'x', 'bar']] 165 print(pretty_table(x)) 166 ``` 167 yields: 168 ``` 169 -- ------ --- 170 A B C 171 -- ------ --- 172 1 1.9999 foo 173 10 x bar 174 -- ------ --- 175 ``` 176 177 ''' 178 txt = [] 179 widths = [np.max([len(e) for e in c]) for c in zip(*x)] 180 181 if len(widths) > len(align): 182 align += '>' * (len(widths)-len(align)) 183 sepline = hsep.join([vsep*w for w in widths]) 184 txt += [sepline] 185 for k,l in enumerate(x): 186 if k and k == header: 187 txt += [sepline] 188 txt += [hsep.join([f'{e:{a}{w}}' for e, w, a in zip(l, widths, align)])] 189 txt += [sepline] 190 txt += [''] 191 return '\n'.join(txt) 192 193 194def transpose_table(x): 195 ''' 196 Transpose a list if lists 197 198 **Parameters** 199 200 + `x`: a list of lists 201 202 **Example** 203 204 ```py 205 x = [[1, 2], [3, 4]] 206 print(transpose_table(x)) # yields: [[1, 3], [2, 4]] 207 ``` 208 ''' 209 return [[e for e in c] for c in zip(*x)] 210 211 212def w_avg(X, sX) : 213 ''' 214 Compute variance-weighted average 215 216 Returns the value and SE of the weighted average of the elements of `X`, 217 with relative weights equal to their inverse variances (`1/sX**2`). 218 219 **Parameters** 220 221 + `X`: array-like of elements to average 222 + `sX`: array-like of the corresponding SE values 223 224 **Tip** 225 226 If `X` and `sX` are initially arranged as a list of `(x, sx)` doublets, 227 they may be rearranged using `zip()`: 228 229 ```python 230 foo = [(0, 1), (1, 0.5), (2, 0.5)] 231 print(w_avg(*zip(*foo))) # yields: (1.3333333333333333, 0.3333333333333333) 232 ``` 233 ''' 234 X = [ x for x in X ] 235 sX = [ sx for sx in sX ] 236 W = [ sx**-2 for sx in sX ] 237 W = [ w/sum(W) for w in W ] 238 Xavg = sum([ w*x for w,x in zip(W,X) ]) 239 sXavg = sum([ w**2*sx**2 for w,sx in zip(W,sX) ])**.5 240 return Xavg, sXavg 241 242 243def read_csv(filename, sep = ''): 244 ''' 245 Read contents of `filename` in csv format and return a list of dictionaries. 246 247 In the csv string, spaces before and after field separators (`','` by default) 248 are optional. 249 250 **Parameters** 251 252 + `filename`: the csv file to read 253 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 254 whichever appers most often in the contents of `filename`. 255 ''' 256 with open(filename) as fid: 257 txt = fid.read() 258 259 if sep == '': 260 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 261 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 262 return [{k: smart_type(v) for k,v in zip(txt[0], l) if v} for l in txt[1:]] 263 264 265def simulate_single_analysis( 266 sample = 'MYSAMPLE', 267 d13Cwg_VPDB = -4., d18Owg_VSMOW = 26., 268 d13C_VPDB = None, d18O_VPDB = None, 269 D47 = None, D48 = None, D49 = 0., D17O = 0., 270 a47 = 1., b47 = 0., c47 = -0.9, 271 a48 = 1., b48 = 0., c48 = -0.45, 272 Nominal_D47 = None, 273 Nominal_D48 = None, 274 Nominal_d13C_VPDB = None, 275 Nominal_d18O_VPDB = None, 276 ALPHA_18O_ACID_REACTION = None, 277 R13_VPDB = None, 278 R17_VSMOW = None, 279 R18_VSMOW = None, 280 LAMBDA_17 = None, 281 R18_VPDB = None, 282 ): 283 ''' 284 Compute working-gas delta values for a single analysis, assuming a stochastic working 285 gas and a “perfect” measurement (i.e. raw Δ values are identical to absolute values). 286 287 **Parameters** 288 289 + `sample`: sample name 290 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 291 (respectively –4 and +26 ‰ by default) 292 + `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 293 + `D47`, `D48`, `D49`, `D17O`: clumped-isotope and oxygen-17 anomalies 294 of the carbonate sample 295 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and 296 Δ48 values if `D47` or `D48` are not specified 297 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 298 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 299 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 300 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 301 correction parameters (by default equal to the `D4xdata` default values) 302 303 Returns a dictionary with fields 304 `['Sample', 'D17O', 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'd45', 'd46', 'd47', 'd48', 'd49']`. 305 ''' 306 307 if Nominal_d13C_VPDB is None: 308 Nominal_d13C_VPDB = D4xdata().Nominal_d13C_VPDB 309 310 if Nominal_d18O_VPDB is None: 311 Nominal_d18O_VPDB = D4xdata().Nominal_d18O_VPDB 312 313 if ALPHA_18O_ACID_REACTION is None: 314 ALPHA_18O_ACID_REACTION = D4xdata().ALPHA_18O_ACID_REACTION 315 316 if R13_VPDB is None: 317 R13_VPDB = D4xdata().R13_VPDB 318 319 if R17_VSMOW is None: 320 R17_VSMOW = D4xdata().R17_VSMOW 321 322 if R18_VSMOW is None: 323 R18_VSMOW = D4xdata().R18_VSMOW 324 325 if LAMBDA_17 is None: 326 LAMBDA_17 = D4xdata().LAMBDA_17 327 328 if R18_VPDB is None: 329 R18_VPDB = D4xdata().R18_VPDB 330 331 R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW) ** LAMBDA_17 332 333 if Nominal_D47 is None: 334 Nominal_D47 = D47data().Nominal_D47 335 336 if Nominal_D48 is None: 337 Nominal_D48 = D48data().Nominal_D48 338 339 if d13C_VPDB is None: 340 if sample in Nominal_d13C_VPDB: 341 d13C_VPDB = Nominal_d13C_VPDB[sample] 342 else: 343 raise KeyError(f"Sample {sample} is missing d13C_VDP value, and it is not defined in Nominal_d13C_VDP.") 344 345 if d18O_VPDB is None: 346 if sample in Nominal_d18O_VPDB: 347 d18O_VPDB = Nominal_d18O_VPDB[sample] 348 else: 349 raise KeyError(f"Sample {sample} is missing d18O_VPDB value, and it is not defined in Nominal_d18O_VPDB.") 350 351 if D47 is None: 352 if sample in Nominal_D47: 353 D47 = Nominal_D47[sample] 354 else: 355 raise KeyError(f"Sample {sample} is missing D47 value, and it is not defined in Nominal_D47.") 356 357 if D48 is None: 358 if sample in Nominal_D48: 359 D48 = Nominal_D48[sample] 360 else: 361 raise KeyError(f"Sample {sample} is missing D48 value, and it is not defined in Nominal_D48.") 362 363 X = D4xdata() 364 X.R13_VPDB = R13_VPDB 365 X.R17_VSMOW = R17_VSMOW 366 X.R18_VSMOW = R18_VSMOW 367 X.LAMBDA_17 = LAMBDA_17 368 X.R18_VPDB = R18_VPDB 369 X.R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW)**LAMBDA_17 370 371 R45wg, R46wg, R47wg, R48wg, R49wg = X.compute_isobar_ratios( 372 R13 = R13_VPDB * (1 + d13Cwg_VPDB/1000), 373 R18 = R18_VSMOW * (1 + d18Owg_VSMOW/1000), 374 ) 375 R45, R46, R47, R48, R49 = X.compute_isobar_ratios( 376 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 377 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 378 D17O=D17O, D47=D47, D48=D48, D49=D49, 379 ) 380 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = X.compute_isobar_ratios( 381 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 382 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 383 D17O=D17O, 384 ) 385 386 d45 = 1000 * (R45/R45wg - 1) 387 d46 = 1000 * (R46/R46wg - 1) 388 d47 = 1000 * (R47/R47wg - 1) 389 d48 = 1000 * (R48/R48wg - 1) 390 d49 = 1000 * (R49/R49wg - 1) 391 392 for k in range(3): # dumb iteration to adjust for small changes in d47 393 R47raw = (1 + (a47 * D47 + b47 * d47 + c47)/1000) * R47stoch 394 R48raw = (1 + (a48 * D48 + b48 * d48 + c48)/1000) * R48stoch 395 d47 = 1000 * (R47raw/R47wg - 1) 396 d48 = 1000 * (R48raw/R48wg - 1) 397 398 return dict( 399 Sample = sample, 400 D17O = D17O, 401 d13Cwg_VPDB = d13Cwg_VPDB, 402 d18Owg_VSMOW = d18Owg_VSMOW, 403 d45 = d45, 404 d46 = d46, 405 d47 = d47, 406 d48 = d48, 407 d49 = d49, 408 ) 409 410 411def virtual_data( 412 samples = [], 413 a47 = 1., b47 = 0., c47 = -0.9, 414 a48 = 1., b48 = 0., c48 = -0.45, 415 rD47 = 0.015, rD48 = 0.045, 416 d13Cwg_VPDB = None, d18Owg_VSMOW = None, 417 session = None, 418 Nominal_D47 = None, Nominal_D48 = None, 419 Nominal_d13C_VPDB = None, Nominal_d18O_VPDB = None, 420 ALPHA_18O_ACID_REACTION = None, 421 R13_VPDB = None, 422 R17_VSMOW = None, 423 R18_VSMOW = None, 424 LAMBDA_17 = None, 425 R18_VPDB = None, 426 seed = 0, 427 ): 428 ''' 429 Return list with simulated analyses from a single session. 430 431 **Parameters** 432 433 + `samples`: a list of entries; each entry is a dictionary with the following fields: 434 * `Sample`: the name of the sample 435 * `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 436 * `D47`, `D48`, `D49`, `D17O` (all optional): clumped-isotope and oxygen-17 anomalies of the carbonate sample 437 * `N`: how many analyses to generate for this sample 438 + `a47`: scrambling factor for Δ47 439 + `b47`: compositional nonlinearity for Δ47 440 + `c47`: working gas offset for Δ47 441 + `a48`: scrambling factor for Δ48 442 + `b48`: compositional nonlinearity for Δ48 443 + `c48`: working gas offset for Δ48 444 + `rD47`: analytical repeatability of Δ47 445 + `rD48`: analytical repeatability of Δ48 446 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 447 (by default equal to the `simulate_single_analysis` default values) 448 + `session`: name of the session (no name by default) 449 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and Δ48 values 450 if `D47` or `D48` are not specified (by default equal to the `simulate_single_analysis` defaults) 451 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 452 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 453 (by default equal to the `simulate_single_analysis` defaults) 454 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 455 (by default equal to the `simulate_single_analysis` defaults) 456 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 457 correction parameters (by default equal to the `simulate_single_analysis` default) 458 + `seed`: explicitly set to a non-zero value to achieve random but repeatable simulations 459 460 461 Here is an example of using this method to generate an arbitrary combination of 462 anchors and unknowns for a bunch of sessions: 463 464 ```py 465 args = dict( 466 samples = [ 467 dict(Sample = 'ETH-1', N = 4), 468 dict(Sample = 'ETH-2', N = 5), 469 dict(Sample = 'ETH-3', N = 6), 470 dict(Sample = 'FOO', N = 2, 471 d13C_VPDB = -5., d18O_VPDB = -10., 472 D47 = 0.3, D48 = 0.15), 473 ], rD47 = 0.010, rD48 = 0.030) 474 475 session1 = virtual_data(session = 'Session_01', **args, seed = 123) 476 session2 = virtual_data(session = 'Session_02', **args, seed = 1234) 477 session3 = virtual_data(session = 'Session_03', **args, seed = 12345) 478 session4 = virtual_data(session = 'Session_04', **args, seed = 123456) 479 480 D = D47data(session1 + session2 + session3 + session4) 481 482 D.crunch() 483 D.standardize() 484 485 D.table_of_sessions(verbose = True, save_to_file = False) 486 D.table_of_samples(verbose = True, save_to_file = False) 487 D.table_of_analyses(verbose = True, save_to_file = False) 488 ``` 489 490 This should output something like: 491 492 ``` 493 [table_of_sessions] 494 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 495 Session Na Nu d13Cwg_VPDB d18Owg_VSMOW r_d13C r_d18O r_D47 a ± SE 1e3 x b ± SE c ± SE 496 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 497 Session_01 15 2 -4.000 26.000 0.0000 0.0000 0.0110 0.997 ± 0.017 -0.097 ± 0.244 -0.896 ± 0.006 498 Session_02 15 2 -4.000 26.000 0.0000 0.0000 0.0109 1.002 ± 0.017 -0.110 ± 0.244 -0.901 ± 0.006 499 Session_03 15 2 -4.000 26.000 0.0000 0.0000 0.0107 1.010 ± 0.017 -0.037 ± 0.244 -0.904 ± 0.006 500 Session_04 15 2 -4.000 26.000 0.0000 0.0000 0.0106 1.001 ± 0.017 -0.181 ± 0.244 -0.894 ± 0.006 501 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 502 503 [table_of_samples] 504 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 505 Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene 506 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 507 ETH-1 16 2.02 37.02 0.2052 0.0079 508 ETH-2 20 -10.17 19.88 0.2085 0.0100 509 ETH-3 24 1.71 37.45 0.6132 0.0105 510 FOO 8 -5.00 28.91 0.2989 0.0040 ± 0.0080 0.0101 0.638 511 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 512 513 [table_of_analyses] 514 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 515 UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47 516 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 517 1 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.122986 21.273526 27.780042 2.020000 37.024281 -0.706013 -0.328878 -0.000013 0.192554 518 2 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.130144 21.282615 27.780042 2.020000 37.024281 -0.698974 -0.319981 -0.000013 0.199615 519 3 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.149219 21.299572 27.780042 2.020000 37.024281 -0.680215 -0.303383 -0.000013 0.218429 520 4 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.136616 21.233128 27.780042 2.020000 37.024281 -0.692609 -0.368421 -0.000013 0.205998 521 5 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.697171 -12.203054 -18.023381 -10.170000 19.875825 -0.680771 -0.290128 -0.000002 0.215054 522 6 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701124 -12.184422 -18.023381 -10.170000 19.875825 -0.684772 -0.271272 -0.000002 0.211041 523 7 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715105 -12.195251 -18.023381 -10.170000 19.875825 -0.698923 -0.282232 -0.000002 0.196848 524 8 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701529 -12.204963 -18.023381 -10.170000 19.875825 -0.685182 -0.292061 -0.000002 0.210630 525 9 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.711420 -12.228478 -18.023381 -10.170000 19.875825 -0.695193 -0.315859 -0.000002 0.200589 526 10 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.666719 22.296486 28.306614 1.710000 37.450394 -0.290459 -0.147284 -0.000014 0.609363 527 11 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.671553 22.291060 28.306614 1.710000 37.450394 -0.285706 -0.152592 -0.000014 0.614130 528 12 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.652854 22.273271 28.306614 1.710000 37.450394 -0.304093 -0.169990 -0.000014 0.595689 529 13 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.684168 22.263156 28.306614 1.710000 37.450394 -0.273302 -0.179883 -0.000014 0.626572 530 14 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.662702 22.253578 28.306614 1.710000 37.450394 -0.294409 -0.189251 -0.000014 0.605401 531 15 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.681957 22.230907 28.306614 1.710000 37.450394 -0.275476 -0.211424 -0.000014 0.624391 532 16 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.312044 5.395798 4.665655 -5.000000 28.907344 -0.598436 -0.268176 -0.000006 0.298996 533 17 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.328123 5.307086 4.665655 -5.000000 28.907344 -0.582387 -0.356389 -0.000006 0.315092 534 18 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.122201 21.340606 27.780042 2.020000 37.024281 -0.706785 -0.263217 -0.000013 0.195135 535 19 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.134868 21.305714 27.780042 2.020000 37.024281 -0.694328 -0.297370 -0.000013 0.207564 536 20 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.140008 21.261931 27.780042 2.020000 37.024281 -0.689273 -0.340227 -0.000013 0.212607 537 21 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.135540 21.298472 27.780042 2.020000 37.024281 -0.693667 -0.304459 -0.000013 0.208224 538 22 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701213 -12.202602 -18.023381 -10.170000 19.875825 -0.684862 -0.289671 -0.000002 0.213842 539 23 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.685649 -12.190405 -18.023381 -10.170000 19.875825 -0.669108 -0.277327 -0.000002 0.229559 540 24 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.719003 -12.257955 -18.023381 -10.170000 19.875825 -0.702869 -0.345692 -0.000002 0.195876 541 25 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700592 -12.204641 -18.023381 -10.170000 19.875825 -0.684233 -0.291735 -0.000002 0.214469 542 26 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720426 -12.214561 -18.023381 -10.170000 19.875825 -0.704308 -0.301774 -0.000002 0.194439 543 27 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.673044 22.262090 28.306614 1.710000 37.450394 -0.284240 -0.180926 -0.000014 0.616730 544 28 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.666542 22.263401 28.306614 1.710000 37.450394 -0.290634 -0.179643 -0.000014 0.610350 545 29 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.680487 22.243486 28.306614 1.710000 37.450394 -0.276921 -0.199121 -0.000014 0.624031 546 30 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.663900 22.245175 28.306614 1.710000 37.450394 -0.293231 -0.197469 -0.000014 0.607759 547 31 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.674379 22.301309 28.306614 1.710000 37.450394 -0.282927 -0.142568 -0.000014 0.618039 548 32 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.660825 22.270466 28.306614 1.710000 37.450394 -0.296255 -0.172733 -0.000014 0.604742 549 33 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.294076 5.349940 4.665655 -5.000000 28.907344 -0.616369 -0.313776 -0.000006 0.283707 550 34 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.313775 5.292121 4.665655 -5.000000 28.907344 -0.596708 -0.371269 -0.000006 0.303323 551 35 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.121613 21.259909 27.780042 2.020000 37.024281 -0.707364 -0.342207 -0.000013 0.194934 552 36 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.145714 21.304889 27.780042 2.020000 37.024281 -0.683661 -0.298178 -0.000013 0.218401 553 37 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.126573 21.325093 27.780042 2.020000 37.024281 -0.702485 -0.278401 -0.000013 0.199764 554 38 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.132057 21.323211 27.780042 2.020000 37.024281 -0.697092 -0.280244 -0.000013 0.205104 555 39 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.708448 -12.232023 -18.023381 -10.170000 19.875825 -0.692185 -0.319447 -0.000002 0.208915 556 40 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.714417 -12.202504 -18.023381 -10.170000 19.875825 -0.698226 -0.289572 -0.000002 0.202934 557 41 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720039 -12.264469 -18.023381 -10.170000 19.875825 -0.703917 -0.352285 -0.000002 0.197300 558 42 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701953 -12.228550 -18.023381 -10.170000 19.875825 -0.685611 -0.315932 -0.000002 0.215423 559 43 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.704535 -12.213634 -18.023381 -10.170000 19.875825 -0.688224 -0.300836 -0.000002 0.212837 560 44 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.652920 22.230043 28.306614 1.710000 37.450394 -0.304028 -0.212269 -0.000014 0.594265 561 45 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.691485 22.261017 28.306614 1.710000 37.450394 -0.266106 -0.181975 -0.000014 0.631810 562 46 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.679119 22.305357 28.306614 1.710000 37.450394 -0.278266 -0.138609 -0.000014 0.619771 563 47 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.663623 22.327286 28.306614 1.710000 37.450394 -0.293503 -0.117161 -0.000014 0.604685 564 48 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.678524 22.282103 28.306614 1.710000 37.450394 -0.278851 -0.161352 -0.000014 0.619192 565 49 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.666246 22.283361 28.306614 1.710000 37.450394 -0.290925 -0.160121 -0.000014 0.607238 566 50 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.309929 5.340249 4.665655 -5.000000 28.907344 -0.600546 -0.323413 -0.000006 0.300148 567 51 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.317548 5.334102 4.665655 -5.000000 28.907344 -0.592942 -0.329524 -0.000006 0.307676 568 52 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.136865 21.300298 27.780042 2.020000 37.024281 -0.692364 -0.302672 -0.000013 0.204033 569 53 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.133538 21.291260 27.780042 2.020000 37.024281 -0.695637 -0.311519 -0.000013 0.200762 570 54 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.139991 21.319865 27.780042 2.020000 37.024281 -0.689290 -0.283519 -0.000013 0.207107 571 55 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.145748 21.330075 27.780042 2.020000 37.024281 -0.683629 -0.273524 -0.000013 0.212766 572 56 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702989 -12.202762 -18.023381 -10.170000 19.875825 -0.686660 -0.289833 -0.000002 0.204507 573 57 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.692830 -12.240287 -18.023381 -10.170000 19.875825 -0.676377 -0.327811 -0.000002 0.214786 574 58 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702899 -12.180291 -18.023381 -10.170000 19.875825 -0.686568 -0.267091 -0.000002 0.204598 575 59 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.709282 -12.282257 -18.023381 -10.170000 19.875825 -0.693029 -0.370287 -0.000002 0.198140 576 60 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.679330 -12.235994 -18.023381 -10.170000 19.875825 -0.662712 -0.323466 -0.000002 0.228446 577 61 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.695594 22.238663 28.306614 1.710000 37.450394 -0.262066 -0.203838 -0.000014 0.634200 578 62 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.663504 22.286354 28.306614 1.710000 37.450394 -0.293620 -0.157194 -0.000014 0.602656 579 63 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666457 22.254290 28.306614 1.710000 37.450394 -0.290717 -0.188555 -0.000014 0.605558 580 64 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666910 22.223232 28.306614 1.710000 37.450394 -0.290271 -0.218930 -0.000014 0.606004 581 65 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.679662 22.257256 28.306614 1.710000 37.450394 -0.277732 -0.185653 -0.000014 0.618539 582 66 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.676768 22.267680 28.306614 1.710000 37.450394 -0.280578 -0.175459 -0.000014 0.615693 583 67 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.307663 5.317330 4.665655 -5.000000 28.907344 -0.602808 -0.346202 -0.000006 0.290853 584 68 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.308562 5.331400 4.665655 -5.000000 28.907344 -0.601911 -0.332212 -0.000006 0.291749 585 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 586 ``` 587 ''' 588 589 kwargs = locals().copy() 590 591 from numpy import random as nprandom 592 if seed: 593 rng = nprandom.default_rng(seed) 594 else: 595 rng = nprandom.default_rng() 596 597 N = sum([s['N'] for s in samples]) 598 errors47 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 599 errors47 *= rD47 / stdev(errors47) # scale errors to rD47 600 errors48 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 601 errors48 *= rD48 / stdev(errors48) # scale errors to rD48 602 603 k = 0 604 out = [] 605 for s in samples: 606 kw = {} 607 kw['sample'] = s['Sample'] 608 kw = { 609 **kw, 610 **{var: kwargs[var] 611 for var in [ 612 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'ALPHA_18O_ACID_REACTION', 613 'Nominal_D47', 'Nominal_D48', 'Nominal_d13C_VPDB', 'Nominal_d18O_VPDB', 614 'R13_VPDB', 'R17_VSMOW', 'R18_VSMOW', 'LAMBDA_17', 'R18_VPDB', 615 'a47', 'b47', 'c47', 'a48', 'b48', 'c48', 616 ] 617 if kwargs[var] is not None}, 618 **{var: s[var] 619 for var in ['d13C_VPDB', 'd18O_VPDB', 'D47', 'D48', 'D49', 'D17O'] 620 if var in s}, 621 } 622 623 sN = s['N'] 624 while sN: 625 out.append(simulate_single_analysis(**kw)) 626 out[-1]['d47'] += errors47[k] * a47 627 out[-1]['d48'] += errors48[k] * a48 628 sN -= 1 629 k += 1 630 631 if session is not None: 632 for r in out: 633 r['Session'] = session 634 return out 635 636def table_of_samples( 637 data47 = None, 638 data48 = None, 639 dir = 'output', 640 filename = None, 641 save_to_file = True, 642 print_out = True, 643 output = None, 644 ): 645 ''' 646 Print out, save to disk and/or return a combined table of samples 647 for a pair of `D47data` and `D48data` objects. 648 649 **Parameters** 650 651 + `data47`: `D47data` instance 652 + `data48`: `D48data` instance 653 + `dir`: the directory in which to save the table 654 + `filename`: the name to the csv file to write to 655 + `save_to_file`: whether to save the table to disk 656 + `print_out`: whether to print out the table 657 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 658 if set to `'raw'`: return a list of list of strings 659 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 660 ''' 661 if data47 is None: 662 if data48 is None: 663 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 664 else: 665 return data48.table_of_samples( 666 dir = dir, 667 filename = filename, 668 save_to_file = save_to_file, 669 print_out = print_out, 670 output = output 671 ) 672 else: 673 if data48 is None: 674 return data47.table_of_samples( 675 dir = dir, 676 filename = filename, 677 save_to_file = save_to_file, 678 print_out = print_out, 679 output = output 680 ) 681 else: 682 out47 = data47.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 683 out48 = data48.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 684 out = transpose_table(transpose_table(out47) + transpose_table(out48)[4:]) 685 686 if save_to_file: 687 if not os.path.exists(dir): 688 os.makedirs(dir) 689 if filename is None: 690 filename = f'D47D48_samples.csv' 691 with open(f'{dir}/{filename}', 'w') as fid: 692 fid.write(make_csv(out)) 693 if print_out: 694 print('\n'+pretty_table(out)) 695 if output == 'raw': 696 return out 697 elif output == 'pretty': 698 return pretty_table(out) 699 700 701def table_of_sessions( 702 data47 = None, 703 data48 = None, 704 dir = 'output', 705 filename = None, 706 save_to_file = True, 707 print_out = True, 708 output = None, 709 ): 710 ''' 711 Print out, save to disk and/or return a combined table of sessions 712 for a pair of `D47data` and `D48data` objects. 713 ***Only applicable if the sessions in `data47` and those in `data48` 714 consist of the exact same sets of analyses.*** 715 716 **Parameters** 717 718 + `data47`: `D47data` instance 719 + `data48`: `D48data` instance 720 + `dir`: the directory in which to save the table 721 + `filename`: the name to the csv file to write to 722 + `save_to_file`: whether to save the table to disk 723 + `print_out`: whether to print out the table 724 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 725 if set to `'raw'`: return a list of list of strings 726 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 727 ''' 728 if data47 is None: 729 if data48 is None: 730 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 731 else: 732 return data48.table_of_sessions( 733 dir = dir, 734 filename = filename, 735 save_to_file = save_to_file, 736 print_out = print_out, 737 output = output 738 ) 739 else: 740 if data48 is None: 741 return data47.table_of_sessions( 742 dir = dir, 743 filename = filename, 744 save_to_file = save_to_file, 745 print_out = print_out, 746 output = output 747 ) 748 else: 749 out47 = data47.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 750 out48 = data48.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 751 for k,x in enumerate(out47[0]): 752 if k>7: 753 out47[0][k] = out47[0][k].replace('a', 'a_47').replace('b', 'b_47').replace('c', 'c_47') 754 out48[0][k] = out48[0][k].replace('a', 'a_48').replace('b', 'b_48').replace('c', 'c_48') 755 out = transpose_table(transpose_table(out47) + transpose_table(out48)[7:]) 756 757 if save_to_file: 758 if not os.path.exists(dir): 759 os.makedirs(dir) 760 if filename is None: 761 filename = f'D47D48_sessions.csv' 762 with open(f'{dir}/{filename}', 'w') as fid: 763 fid.write(make_csv(out)) 764 if print_out: 765 print('\n'+pretty_table(out)) 766 if output == 'raw': 767 return out 768 elif output == 'pretty': 769 return pretty_table(out) 770 771 772def table_of_analyses( 773 data47 = None, 774 data48 = None, 775 dir = 'output', 776 filename = None, 777 save_to_file = True, 778 print_out = True, 779 output = None, 780 ): 781 ''' 782 Print out, save to disk and/or return a combined table of analyses 783 for a pair of `D47data` and `D48data` objects. 784 785 If the sessions in `data47` and those in `data48` do not consist of 786 the exact same sets of analyses, the table will have two columns 787 `Session_47` and `Session_48` instead of a single `Session` column. 788 789 **Parameters** 790 791 + `data47`: `D47data` instance 792 + `data48`: `D48data` instance 793 + `dir`: the directory in which to save the table 794 + `filename`: the name to the csv file to write to 795 + `save_to_file`: whether to save the table to disk 796 + `print_out`: whether to print out the table 797 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 798 if set to `'raw'`: return a list of list of strings 799 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 800 ''' 801 if data47 is None: 802 if data48 is None: 803 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 804 else: 805 return data48.table_of_analyses( 806 dir = dir, 807 filename = filename, 808 save_to_file = save_to_file, 809 print_out = print_out, 810 output = output 811 ) 812 else: 813 if data48 is None: 814 return data47.table_of_analyses( 815 dir = dir, 816 filename = filename, 817 save_to_file = save_to_file, 818 print_out = print_out, 819 output = output 820 ) 821 else: 822 out47 = data47.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 823 out48 = data48.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 824 825 if [l[1] for l in out47[1:]] == [l[1] for l in out48[1:]]: # if sessions are identical 826 out = transpose_table(transpose_table(out47) + transpose_table(out48)[-1:]) 827 else: 828 out47[0][1] = 'Session_47' 829 out48[0][1] = 'Session_48' 830 out47 = transpose_table(out47) 831 out48 = transpose_table(out48) 832 out = transpose_table(out47[:2] + out48[1:2] + out47[2:] + out48[-1:]) 833 834 if save_to_file: 835 if not os.path.exists(dir): 836 os.makedirs(dir) 837 if filename is None: 838 filename = f'D47D48_sessions.csv' 839 with open(f'{dir}/{filename}', 'w') as fid: 840 fid.write(make_csv(out)) 841 if print_out: 842 print('\n'+pretty_table(out)) 843 if output == 'raw': 844 return out 845 elif output == 'pretty': 846 return pretty_table(out) 847 848 849def _fullcovar(minresult, epsilon = 0.01, named = False): 850 ''' 851 Construct full covariance matrix in the case of constrained parameters 852 ''' 853 854 import asteval 855 856 def f(values): 857 interp = asteval.Interpreter() 858 for n,v in zip(minresult.var_names, values): 859 interp(f'{n} = {v}') 860 for q in minresult.params: 861 if minresult.params[q].expr: 862 interp(f'{q} = {minresult.params[q].expr}') 863 return np.array([interp.symtable[q] for q in minresult.params]) 864 865 # construct Jacobian 866 J = np.zeros((minresult.nvarys, len(minresult.params))) 867 X = np.array([minresult.params[p].value for p in minresult.var_names]) 868 sX = np.array([minresult.params[p].stderr for p in minresult.var_names]) 869 870 for j in range(minresult.nvarys): 871 x1 = [_ for _ in X] 872 x1[j] += epsilon * sX[j] 873 x2 = [_ for _ in X] 874 x2[j] -= epsilon * sX[j] 875 J[j,:] = (f(x1) - f(x2)) / (2 * epsilon * sX[j]) 876 877 _names = [q for q in minresult.params] 878 _covar = J.T @ minresult.covar @ J 879 _se = np.diag(_covar)**.5 880 _correl = _covar.copy() 881 for k,s in enumerate(_se): 882 if s: 883 _correl[k,:] /= s 884 _correl[:,k] /= s 885 886 if named: 887 _covar = {i: {j:_covar[i,j] for j in minresult.params} for i in minresult.params} 888 _se = {i: _se[i] for i in minresult.params} 889 _correl = {i: {j:_correl[i,j] for j in minresult.params} for i in minresult.params} 890 891 return _names, _covar, _se, _correl 892 893 894class D4xdata(list): 895 ''' 896 Store and process data for a large set of Δ47 and/or Δ48 897 analyses, usually comprising more than one analytical session. 898 ''' 899 900 ### 17O CORRECTION PARAMETERS 901 R13_VPDB = 0.01118 # (Chang & Li, 1990) 902 ''' 903 Absolute (13C/12C) ratio of VPDB. 904 By default equal to 0.01118 ([Chang & Li, 1990](http://www.cnki.com.cn/Article/CJFDTotal-JXTW199004006.htm)) 905 ''' 906 907 R18_VSMOW = 0.0020052 # (Baertschi, 1976) 908 ''' 909 Absolute (18O/16C) ratio of VSMOW. 910 By default equal to 0.0020052 ([Baertschi, 1976](https://doi.org/10.1016/0012-821X(76)90115-1)) 911 ''' 912 913 LAMBDA_17 = 0.528 # (Barkan & Luz, 2005) 914 ''' 915 Mass-dependent exponent for triple oxygen isotopes. 916 By default equal to 0.528 ([Barkan & Luz, 2005](https://doi.org/10.1002/rcm.2250)) 917 ''' 918 919 R17_VSMOW = 0.00038475 # (Assonov & Brenninkmeijer, 2003, rescaled to R13_VPDB) 920 ''' 921 Absolute (17O/16C) ratio of VSMOW. 922 By default equal to 0.00038475 923 ([Assonov & Brenninkmeijer, 2003](https://dx.doi.org/10.1002/rcm.1011), 924 rescaled to `R13_VPDB`) 925 ''' 926 927 R18_VPDB = R18_VSMOW * 1.03092 928 ''' 929 Absolute (18O/16C) ratio of VPDB. 930 By definition equal to `R18_VSMOW * 1.03092`. 931 ''' 932 933 R17_VPDB = R17_VSMOW * 1.03092 ** LAMBDA_17 934 ''' 935 Absolute (17O/16C) ratio of VPDB. 936 By definition equal to `R17_VSMOW * 1.03092 ** LAMBDA_17`. 937 ''' 938 939 LEVENE_REF_SAMPLE = 'ETH-3' 940 ''' 941 After the Δ4x standardization step, each sample is tested to 942 assess whether the Δ4x variance within all analyses for that 943 sample differs significantly from that observed for a given reference 944 sample (using [Levene's test](https://en.wikipedia.org/wiki/Levene%27s_test), 945 which yields a p-value corresponding to the null hypothesis that the 946 underlying variances are equal). 947 948 `LEVENE_REF_SAMPLE` (by default equal to `'ETH-3'`) specifies which 949 sample should be used as a reference for this test. 950 ''' 951 952 ALPHA_18O_ACID_REACTION = round(np.exp(3.59 / (90 + 273.15) - 1.79e-3), 6) # (Kim et al., 2007, calcite) 953 ''' 954 Specifies the 18O/16O fractionation factor generally applicable 955 to acid reactions in the dataset. Currently used by `D4xdata.wg()`, 956 `D4xdata.standardize_d13C`, and `D4xdata.standardize_d18O`. 957 958 By default equal to 1.008129 (calcite reacted at 90 °C, 959 [Kim et al., 2007](https://dx.doi.org/10.1016/j.chemgeo.2007.08.005)). 960 ''' 961 962 Nominal_d13C_VPDB = { 963 'ETH-1': 2.02, 964 'ETH-2': -10.17, 965 'ETH-3': 1.71, 966 } # (Bernasconi et al., 2018) 967 ''' 968 Nominal δ13C_VPDB values assigned to carbonate standards, used by 969 `D4xdata.standardize_d13C()`. 970 971 By default equal to `{'ETH-1': 2.02, 'ETH-2': -10.17, 'ETH-3': 1.71}` after 972 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 973 ''' 974 975 Nominal_d18O_VPDB = { 976 'ETH-1': -2.19, 977 'ETH-2': -18.69, 978 'ETH-3': -1.78, 979 } # (Bernasconi et al., 2018) 980 ''' 981 Nominal δ18O_VPDB values assigned to carbonate standards, used by 982 `D4xdata.standardize_d18O()`. 983 984 By default equal to `{'ETH-1': -2.19, 'ETH-2': -18.69, 'ETH-3': -1.78}` after 985 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 986 ''' 987 988 d13C_STANDARDIZATION_METHOD = '2pt' 989 ''' 990 Method by which to standardize δ13C values: 991 992 + `none`: do not apply any δ13C standardization. 993 + `'1pt'`: within each session, offset all initial δ13C values so as to 994 minimize the difference between final δ13C_VPDB values and 995 `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` is defined). 996 + `'2pt'`: within each session, apply a affine trasformation to all δ13C 997 values so as to minimize the difference between final δ13C_VPDB 998 values and `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` 999 is defined). 1000 ''' 1001 1002 d18O_STANDARDIZATION_METHOD = '2pt' 1003 ''' 1004 Method by which to standardize δ18O values: 1005 1006 + `none`: do not apply any δ18O standardization. 1007 + `'1pt'`: within each session, offset all initial δ18O values so as to 1008 minimize the difference between final δ18O_VPDB values and 1009 `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` is defined). 1010 + `'2pt'`: within each session, apply a affine trasformation to all δ18O 1011 values so as to minimize the difference between final δ18O_VPDB 1012 values and `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` 1013 is defined). 1014 ''' 1015 1016 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 1017 ''' 1018 **Parameters** 1019 1020 + `l`: a list of dictionaries, with each dictionary including at least the keys 1021 `Sample`, `d45`, `d46`, and `d47` or `d48`. 1022 + `mass`: `'47'` or `'48'` 1023 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 1024 + `session`: define session name for analyses without a `Session` key 1025 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 1026 1027 Returns a `D4xdata` object derived from `list`. 1028 ''' 1029 self._4x = mass 1030 self.verbose = verbose 1031 self.prefix = 'D4xdata' 1032 self.logfile = logfile 1033 list.__init__(self, l) 1034 self.Nf = None 1035 self.repeatability = {} 1036 self.refresh(session = session) 1037 1038 1039 def make_verbal(oldfun): 1040 ''' 1041 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 1042 ''' 1043 @wraps(oldfun) 1044 def newfun(*args, verbose = '', **kwargs): 1045 myself = args[0] 1046 oldprefix = myself.prefix 1047 myself.prefix = oldfun.__name__ 1048 if verbose != '': 1049 oldverbose = myself.verbose 1050 myself.verbose = verbose 1051 out = oldfun(*args, **kwargs) 1052 myself.prefix = oldprefix 1053 if verbose != '': 1054 myself.verbose = oldverbose 1055 return out 1056 return newfun 1057 1058 1059 def msg(self, txt): 1060 ''' 1061 Log a message to `self.logfile`, and print it out if `verbose = True` 1062 ''' 1063 self.log(txt) 1064 if self.verbose: 1065 print(f'{f"[{self.prefix}]":<16} {txt}') 1066 1067 1068 def vmsg(self, txt): 1069 ''' 1070 Log a message to `self.logfile` and print it out 1071 ''' 1072 self.log(txt) 1073 print(txt) 1074 1075 1076 def log(self, *txts): 1077 ''' 1078 Log a message to `self.logfile` 1079 ''' 1080 if self.logfile: 1081 with open(self.logfile, 'a') as fid: 1082 for txt in txts: 1083 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}') 1084 1085 1086 def refresh(self, session = 'mySession'): 1087 ''' 1088 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1089 ''' 1090 self.fill_in_missing_info(session = session) 1091 self.refresh_sessions() 1092 self.refresh_samples() 1093 1094 1095 def refresh_sessions(self): 1096 ''' 1097 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1098 to `False` for all sessions. 1099 ''' 1100 self.sessions = { 1101 s: {'data': [r for r in self if r['Session'] == s]} 1102 for s in sorted({r['Session'] for r in self}) 1103 } 1104 for s in self.sessions: 1105 self.sessions[s]['scrambling_drift'] = False 1106 self.sessions[s]['slope_drift'] = False 1107 self.sessions[s]['wg_drift'] = False 1108 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1109 self.sessions[s]['d18O_standardization_method'] = self.d18O_STANDARDIZATION_METHOD 1110 1111 1112 def refresh_samples(self): 1113 ''' 1114 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1115 ''' 1116 self.samples = { 1117 s: {'data': [r for r in self if r['Sample'] == s]} 1118 for s in sorted({r['Sample'] for r in self}) 1119 } 1120 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1121 self.unknowns = {s: self.samples[s] for s in self.samples if s not in self.Nominal_D4x} 1122 1123 1124 def read(self, filename, sep = '', session = ''): 1125 ''' 1126 Read file in csv format to load data into a `D47data` object. 1127 1128 In the csv file, spaces before and after field separators (`','` by default) 1129 are optional. Each line corresponds to a single analysis. 1130 1131 The required fields are: 1132 1133 + `UID`: a unique identifier 1134 + `Session`: an identifier for the analytical session 1135 + `Sample`: a sample identifier 1136 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1137 1138 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1139 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1140 and `d49` are optional, and set to NaN by default. 1141 1142 **Parameters** 1143 1144 + `fileneme`: the path of the file to read 1145 + `sep`: csv separator delimiting the fields 1146 + `session`: set `Session` field to this string for all analyses 1147 ''' 1148 with open(filename) as fid: 1149 self.input(fid.read(), sep = sep, session = session) 1150 1151 1152 def input(self, txt, sep = '', session = ''): 1153 ''' 1154 Read `txt` string in csv format to load analysis data into a `D47data` object. 1155 1156 In the csv string, spaces before and after field separators (`','` by default) 1157 are optional. Each line corresponds to a single analysis. 1158 1159 The required fields are: 1160 1161 + `UID`: a unique identifier 1162 + `Session`: an identifier for the analytical session 1163 + `Sample`: a sample identifier 1164 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1165 1166 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1167 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1168 and `d49` are optional, and set to NaN by default. 1169 1170 **Parameters** 1171 1172 + `txt`: the csv string to read 1173 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1174 whichever appers most often in `txt`. 1175 + `session`: set `Session` field to this string for all analyses 1176 ''' 1177 if sep == '': 1178 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1179 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1180 data = [{k: v if k in ['UID', 'Session', 'Sample'] else smart_type(v) for k,v in zip(txt[0], l) if v != ''} for l in txt[1:]] 1181 1182 if session != '': 1183 for r in data: 1184 r['Session'] = session 1185 1186 self += data 1187 self.refresh() 1188 1189 1190 @make_verbal 1191 def wg(self, samples = None, a18_acid = None): 1192 ''' 1193 Compute bulk composition of the working gas for each session based on 1194 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1195 `self.Nominal_d18O_VPDB`. 1196 ''' 1197 1198 self.msg('Computing WG composition:') 1199 1200 if a18_acid is None: 1201 a18_acid = self.ALPHA_18O_ACID_REACTION 1202 if samples is None: 1203 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1204 1205 assert a18_acid, f'Acid fractionation factor should not be zero.' 1206 1207 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1208 R45R46_standards = {} 1209 for sample in samples: 1210 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1211 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1212 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1213 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1214 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1215 1216 C12_s = 1 / (1 + R13_s) 1217 C13_s = R13_s / (1 + R13_s) 1218 C16_s = 1 / (1 + R17_s + R18_s) 1219 C17_s = R17_s / (1 + R17_s + R18_s) 1220 C18_s = R18_s / (1 + R17_s + R18_s) 1221 1222 C626_s = C12_s * C16_s ** 2 1223 C627_s = 2 * C12_s * C16_s * C17_s 1224 C628_s = 2 * C12_s * C16_s * C18_s 1225 C636_s = C13_s * C16_s ** 2 1226 C637_s = 2 * C13_s * C16_s * C17_s 1227 C727_s = C12_s * C17_s ** 2 1228 1229 R45_s = (C627_s + C636_s) / C626_s 1230 R46_s = (C628_s + C637_s + C727_s) / C626_s 1231 R45R46_standards[sample] = (R45_s, R46_s) 1232 1233 for s in self.sessions: 1234 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1235 assert db, f'No sample from {samples} found in session "{s}".' 1236# dbsamples = sorted({r['Sample'] for r in db}) 1237 1238 X = [r['d45'] for r in db] 1239 Y = [R45R46_standards[r['Sample']][0] for r in db] 1240 x1, x2 = np.min(X), np.max(X) 1241 1242 if x1 < x2: 1243 wgcoord = x1/(x1-x2) 1244 else: 1245 wgcoord = 999 1246 1247 if wgcoord < -.5 or wgcoord > 1.5: 1248 # unreasonable to extrapolate to d45 = 0 1249 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1250 else : 1251 # d45 = 0 is reasonably well bracketed 1252 R45_wg = np.polyfit(X, Y, 1)[1] 1253 1254 X = [r['d46'] for r in db] 1255 Y = [R45R46_standards[r['Sample']][1] for r in db] 1256 x1, x2 = np.min(X), np.max(X) 1257 1258 if x1 < x2: 1259 wgcoord = x1/(x1-x2) 1260 else: 1261 wgcoord = 999 1262 1263 if wgcoord < -.5 or wgcoord > 1.5: 1264 # unreasonable to extrapolate to d46 = 0 1265 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1266 else : 1267 # d46 = 0 is reasonably well bracketed 1268 R46_wg = np.polyfit(X, Y, 1)[1] 1269 1270 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1271 1272 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1273 1274 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1275 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1276 for r in self.sessions[s]['data']: 1277 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1278 r['d18Owg_VSMOW'] = d18Owg_VSMOW 1279 1280 1281 def compute_bulk_delta(self, R45, R46, D17O = 0): 1282 ''' 1283 Compute δ13C_VPDB and δ18O_VSMOW, 1284 by solving the generalized form of equation (17) from 1285 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1286 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1287 solving the corresponding second-order Taylor polynomial. 1288 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1289 ''' 1290 1291 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1292 1293 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1294 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1295 C = 2 * self.R18_VSMOW 1296 D = -R46 1297 1298 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1299 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1300 cc = A + B + C + D 1301 1302 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1303 1304 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1305 R17 = K * R18 ** self.LAMBDA_17 1306 R13 = R45 - 2 * R17 1307 1308 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1309 1310 return d13C_VPDB, d18O_VSMOW 1311 1312 1313 @make_verbal 1314 def crunch(self, verbose = ''): 1315 ''' 1316 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1317 ''' 1318 for r in self: 1319 self.compute_bulk_and_clumping_deltas(r) 1320 self.standardize_d13C() 1321 self.standardize_d18O() 1322 self.msg(f"Crunched {len(self)} analyses.") 1323 1324 1325 def fill_in_missing_info(self, session = 'mySession'): 1326 ''' 1327 Fill in optional fields with default values 1328 ''' 1329 for i,r in enumerate(self): 1330 if 'D17O' not in r: 1331 r['D17O'] = 0. 1332 if 'UID' not in r: 1333 r['UID'] = f'{i+1}' 1334 if 'Session' not in r: 1335 r['Session'] = session 1336 for k in ['d47', 'd48', 'd49']: 1337 if k not in r: 1338 r[k] = np.nan 1339 1340 1341 def standardize_d13C(self): 1342 ''' 1343 Perform δ13C standadization within each session `s` according to 1344 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1345 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1346 may be redefined abitrarily at a later stage. 1347 ''' 1348 for s in self.sessions: 1349 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1350 XY = [(r['d13C_VPDB'], self.Nominal_d13C_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d13C_VPDB] 1351 X,Y = zip(*XY) 1352 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1353 offset = np.mean(Y) - np.mean(X) 1354 for r in self.sessions[s]['data']: 1355 r['d13C_VPDB'] += offset 1356 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1357 a,b = np.polyfit(X,Y,1) 1358 for r in self.sessions[s]['data']: 1359 r['d13C_VPDB'] = a * r['d13C_VPDB'] + b 1360 1361 def standardize_d18O(self): 1362 ''' 1363 Perform δ18O standadization within each session `s` according to 1364 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1365 which is defined by default by `D47data.refresh_sessions()`as equal to 1366 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1367 ''' 1368 for s in self.sessions: 1369 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1370 XY = [(r['d18O_VSMOW'], self.Nominal_d18O_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d18O_VPDB] 1371 X,Y = zip(*XY) 1372 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1373 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1374 offset = np.mean(Y) - np.mean(X) 1375 for r in self.sessions[s]['data']: 1376 r['d18O_VSMOW'] += offset 1377 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1378 a,b = np.polyfit(X,Y,1) 1379 for r in self.sessions[s]['data']: 1380 r['d18O_VSMOW'] = a * r['d18O_VSMOW'] + b 1381 1382 1383 def compute_bulk_and_clumping_deltas(self, r): 1384 ''' 1385 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1386 ''' 1387 1388 # Compute working gas R13, R18, and isobar ratios 1389 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1390 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1391 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1392 1393 # Compute analyte isobar ratios 1394 R45 = (1 + r['d45'] / 1000) * R45_wg 1395 R46 = (1 + r['d46'] / 1000) * R46_wg 1396 R47 = (1 + r['d47'] / 1000) * R47_wg 1397 R48 = (1 + r['d48'] / 1000) * R48_wg 1398 R49 = (1 + r['d49'] / 1000) * R49_wg 1399 1400 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1401 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1402 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1403 1404 # Compute stochastic isobar ratios of the analyte 1405 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1406 R13, R18, D17O = r['D17O'] 1407 ) 1408 1409 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1410 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1411 if (R45 / R45stoch - 1) > 5e-8: 1412 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1413 if (R46 / R46stoch - 1) > 5e-8: 1414 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1415 1416 # Compute raw clumped isotope anomalies 1417 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1418 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1419 r['D49raw'] = 1000 * (R49 / R49stoch - 1) 1420 1421 1422 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1423 ''' 1424 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1425 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1426 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1427 ''' 1428 1429 # Compute R17 1430 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1431 1432 # Compute isotope concentrations 1433 C12 = (1 + R13) ** -1 1434 C13 = C12 * R13 1435 C16 = (1 + R17 + R18) ** -1 1436 C17 = C16 * R17 1437 C18 = C16 * R18 1438 1439 # Compute stochastic isotopologue concentrations 1440 C626 = C16 * C12 * C16 1441 C627 = C16 * C12 * C17 * 2 1442 C628 = C16 * C12 * C18 * 2 1443 C636 = C16 * C13 * C16 1444 C637 = C16 * C13 * C17 * 2 1445 C638 = C16 * C13 * C18 * 2 1446 C727 = C17 * C12 * C17 1447 C728 = C17 * C12 * C18 * 2 1448 C737 = C17 * C13 * C17 1449 C738 = C17 * C13 * C18 * 2 1450 C828 = C18 * C12 * C18 1451 C838 = C18 * C13 * C18 1452 1453 # Compute stochastic isobar ratios 1454 R45 = (C636 + C627) / C626 1455 R46 = (C628 + C637 + C727) / C626 1456 R47 = (C638 + C728 + C737) / C626 1457 R48 = (C738 + C828) / C626 1458 R49 = C838 / C626 1459 1460 # Account for stochastic anomalies 1461 R47 *= 1 + D47 / 1000 1462 R48 *= 1 + D48 / 1000 1463 R49 *= 1 + D49 / 1000 1464 1465 # Return isobar ratios 1466 return R45, R46, R47, R48, R49 1467 1468 1469 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1470 ''' 1471 Split unknown samples by UID (treat all analyses as different samples) 1472 or by session (treat analyses of a given sample in different sessions as 1473 different samples). 1474 1475 **Parameters** 1476 1477 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1478 + `grouping`: `by_uid` | `by_session` 1479 ''' 1480 if samples_to_split == 'all': 1481 samples_to_split = [s for s in self.unknowns] 1482 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1483 self.grouping = grouping.lower() 1484 if self.grouping in gkeys: 1485 gkey = gkeys[self.grouping] 1486 for r in self: 1487 if r['Sample'] in samples_to_split: 1488 r['Sample_original'] = r['Sample'] 1489 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1490 elif r['Sample'] in self.unknowns: 1491 r['Sample_original'] = r['Sample'] 1492 self.refresh_samples() 1493 1494 1495 def unsplit_samples(self, tables = False): 1496 ''' 1497 Reverse the effects of `D47data.split_samples()`. 1498 1499 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1500 1501 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1502 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1503 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1504 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1505 that case session-averaged Δ4x values are statistically independent). 1506 ''' 1507 unknowns_old = sorted({s for s in self.unknowns}) 1508 CM_old = self.standardization.covar[:,:] 1509 VD_old = self.standardization.params.valuesdict().copy() 1510 vars_old = self.standardization.var_names 1511 1512 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1513 1514 Ns = len(vars_old) - len(unknowns_old) 1515 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1516 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1517 1518 W = np.zeros((len(vars_new), len(vars_old))) 1519 W[:Ns,:Ns] = np.eye(Ns) 1520 for u in unknowns_new: 1521 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1522 if self.grouping == 'by_session': 1523 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1524 elif self.grouping == 'by_uid': 1525 weights = [1 for s in splits] 1526 sw = sum(weights) 1527 weights = [w/sw for w in weights] 1528 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1529 1530 CM_new = W @ CM_old @ W.T 1531 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1532 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1533 1534 self.standardization.covar = CM_new 1535 self.standardization.params.valuesdict = lambda : VD_new 1536 self.standardization.var_names = vars_new 1537 1538 for r in self: 1539 if r['Sample'] in self.unknowns: 1540 r['Sample_split'] = r['Sample'] 1541 r['Sample'] = r['Sample_original'] 1542 1543 self.refresh_samples() 1544 self.consolidate_samples() 1545 self.repeatabilities() 1546 1547 if tables: 1548 self.table_of_analyses() 1549 self.table_of_samples() 1550 1551 def assign_timestamps(self): 1552 ''' 1553 Assign a time field `t` of type `float` to each analysis. 1554 1555 If `TimeTag` is one of the data fields, `t` is equal within a given session 1556 to `TimeTag` minus the mean value of `TimeTag` for that session. 1557 Otherwise, `TimeTag` is by default equal to the index of each analysis 1558 in the dataset and `t` is defined as above. 1559 ''' 1560 for session in self.sessions: 1561 sdata = self.sessions[session]['data'] 1562 try: 1563 t0 = np.mean([r['TimeTag'] for r in sdata]) 1564 for r in sdata: 1565 r['t'] = r['TimeTag'] - t0 1566 except KeyError: 1567 t0 = (len(sdata)-1)/2 1568 for t,r in enumerate(sdata): 1569 r['t'] = t - t0 1570 1571 1572 def report(self): 1573 ''' 1574 Prints a report on the standardization fit. 1575 Only applicable after `D4xdata.standardize(method='pooled')`. 1576 ''' 1577 report_fit(self.standardization) 1578 1579 1580 def combine_samples(self, sample_groups): 1581 ''' 1582 Combine analyses of different samples to compute weighted average Δ4x 1583 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1584 dictionary. 1585 1586 Caution: samples are weighted by number of replicate analyses, which is a 1587 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1588 correlated analytical errors for one or more samples). 1589 1590 Returns a tuplet of: 1591 1592 + the list of group names 1593 + an array of the corresponding Δ4x values 1594 + the corresponding (co)variance matrix 1595 1596 **Parameters** 1597 1598 + `sample_groups`: a dictionary of the form: 1599 ```py 1600 {'group1': ['sample_1', 'sample_2'], 1601 'group2': ['sample_3', 'sample_4', 'sample_5']} 1602 ``` 1603 ''' 1604 1605 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1606 groups = sorted(sample_groups.keys()) 1607 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1608 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1609 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1610 W = np.array([ 1611 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1612 for j in groups]) 1613 D4x_new = W @ D4x_old 1614 CM_new = W @ CM_old @ W.T 1615 1616 return groups, D4x_new[:,0], CM_new 1617 1618 1619 @make_verbal 1620 def standardize(self, 1621 method = 'pooled', 1622 weighted_sessions = [], 1623 consolidate = True, 1624 consolidate_tables = False, 1625 consolidate_plots = False, 1626 constraints = {}, 1627 ): 1628 ''' 1629 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1630 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1631 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1632 i.e. that their true Δ4x value does not change between sessions, 1633 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1634 `'indep_sessions'`, the standardization processes each session independently, based only 1635 on anchors analyses. 1636 ''' 1637 1638 self.standardization_method = method 1639 self.assign_timestamps() 1640 1641 if method == 'pooled': 1642 if weighted_sessions: 1643 for session_group in weighted_sessions: 1644 if self._4x == '47': 1645 X = D47data([r for r in self if r['Session'] in session_group]) 1646 elif self._4x == '48': 1647 X = D48data([r for r in self if r['Session'] in session_group]) 1648 X.Nominal_D4x = self.Nominal_D4x.copy() 1649 X.refresh() 1650 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1651 w = np.sqrt(result.redchi) 1652 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1653 for r in X: 1654 r[f'wD{self._4x}raw'] *= w 1655 else: 1656 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1657 for r in self: 1658 r[f'wD{self._4x}raw'] = 1. 1659 1660 params = Parameters() 1661 for k,session in enumerate(self.sessions): 1662 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1663 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1664 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1665 s = pf(session) 1666 params.add(f'a_{s}', value = 0.9) 1667 params.add(f'b_{s}', value = 0.) 1668 params.add(f'c_{s}', value = -0.9) 1669 params.add(f'a2_{s}', value = 0., 1670# vary = self.sessions[session]['scrambling_drift'], 1671 ) 1672 params.add(f'b2_{s}', value = 0., 1673# vary = self.sessions[session]['slope_drift'], 1674 ) 1675 params.add(f'c2_{s}', value = 0., 1676# vary = self.sessions[session]['wg_drift'], 1677 ) 1678 if not self.sessions[session]['scrambling_drift']: 1679 params[f'a2_{s}'].expr = '0' 1680 if not self.sessions[session]['slope_drift']: 1681 params[f'b2_{s}'].expr = '0' 1682 if not self.sessions[session]['wg_drift']: 1683 params[f'c2_{s}'].expr = '0' 1684 1685 for sample in self.unknowns: 1686 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1687 1688 for k in constraints: 1689 params[k].expr = constraints[k] 1690 1691 def residuals(p): 1692 R = [] 1693 for r in self: 1694 session = pf(r['Session']) 1695 sample = pf(r['Sample']) 1696 if r['Sample'] in self.Nominal_D4x: 1697 R += [ ( 1698 r[f'D{self._4x}raw'] - ( 1699 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1700 + p[f'b_{session}'] * r[f'd{self._4x}'] 1701 + p[f'c_{session}'] 1702 + r['t'] * ( 1703 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1704 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1705 + p[f'c2_{session}'] 1706 ) 1707 ) 1708 ) / r[f'wD{self._4x}raw'] ] 1709 else: 1710 R += [ ( 1711 r[f'D{self._4x}raw'] - ( 1712 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1713 + p[f'b_{session}'] * r[f'd{self._4x}'] 1714 + p[f'c_{session}'] 1715 + r['t'] * ( 1716 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1717 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1718 + p[f'c2_{session}'] 1719 ) 1720 ) 1721 ) / r[f'wD{self._4x}raw'] ] 1722 return R 1723 1724 M = Minimizer(residuals, params) 1725 result = M.least_squares() 1726 self.Nf = result.nfree 1727 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1728 new_names, new_covar, new_se = _fullcovar(result)[:3] 1729 result.var_names = new_names 1730 result.covar = new_covar 1731 1732 for r in self: 1733 s = pf(r["Session"]) 1734 a = result.params.valuesdict()[f'a_{s}'] 1735 b = result.params.valuesdict()[f'b_{s}'] 1736 c = result.params.valuesdict()[f'c_{s}'] 1737 a2 = result.params.valuesdict()[f'a2_{s}'] 1738 b2 = result.params.valuesdict()[f'b2_{s}'] 1739 c2 = result.params.valuesdict()[f'c2_{s}'] 1740 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1741 1742 self.standardization = result 1743 1744 for session in self.sessions: 1745 self.sessions[session]['Np'] = 3 1746 for k in ['scrambling', 'slope', 'wg']: 1747 if self.sessions[session][f'{k}_drift']: 1748 self.sessions[session]['Np'] += 1 1749 1750 if consolidate: 1751 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1752 return result 1753 1754 1755 elif method == 'indep_sessions': 1756 1757 if weighted_sessions: 1758 for session_group in weighted_sessions: 1759 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1760 X.Nominal_D4x = self.Nominal_D4x.copy() 1761 X.refresh() 1762 # This is only done to assign r['wD47raw'] for r in X: 1763 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1764 self.msg(f'D{self._4x}raw weights set to {1000*X[0][f"wD{self._4x}raw"]:.1f} ppm for sessions in {session_group}') 1765 else: 1766 self.msg('All weights set to 1 ‰') 1767 for r in self: 1768 r[f'wD{self._4x}raw'] = 1 1769 1770 for session in self.sessions: 1771 s = self.sessions[session] 1772 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1773 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1774 s['Np'] = sum(p_active) 1775 sdata = s['data'] 1776 1777 A = np.array([ 1778 [ 1779 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1780 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1781 1 / r[f'wD{self._4x}raw'], 1782 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1783 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1784 r['t'] / r[f'wD{self._4x}raw'] 1785 ] 1786 for r in sdata if r['Sample'] in self.anchors 1787 ])[:,p_active] # only keep columns for the active parameters 1788 Y = np.array([[r[f'D{self._4x}raw'] / r[f'wD{self._4x}raw']] for r in sdata if r['Sample'] in self.anchors]) 1789 s['Na'] = Y.size 1790 CM = linalg.inv(A.T @ A) 1791 bf = (CM @ A.T @ Y).T[0,:] 1792 k = 0 1793 for n,a in zip(p_names, p_active): 1794 if a: 1795 s[n] = bf[k] 1796# self.msg(f'{n} = {bf[k]}') 1797 k += 1 1798 else: 1799 s[n] = 0. 1800# self.msg(f'{n} = 0.0') 1801 1802 for r in sdata : 1803 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1804 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1805 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1806 1807 s['CM'] = np.zeros((6,6)) 1808 i = 0 1809 k_active = [j for j,a in enumerate(p_active) if a] 1810 for j,a in enumerate(p_active): 1811 if a: 1812 s['CM'][j,k_active] = CM[i,:] 1813 i += 1 1814 1815 if not weighted_sessions: 1816 w = self.rmswd()['rmswd'] 1817 for r in self: 1818 r[f'wD{self._4x}'] *= w 1819 r[f'wD{self._4x}raw'] *= w 1820 for session in self.sessions: 1821 self.sessions[session]['CM'] *= w**2 1822 1823 for session in self.sessions: 1824 s = self.sessions[session] 1825 s['SE_a'] = s['CM'][0,0]**.5 1826 s['SE_b'] = s['CM'][1,1]**.5 1827 s['SE_c'] = s['CM'][2,2]**.5 1828 s['SE_a2'] = s['CM'][3,3]**.5 1829 s['SE_b2'] = s['CM'][4,4]**.5 1830 s['SE_c2'] = s['CM'][5,5]**.5 1831 1832 if not weighted_sessions: 1833 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1834 else: 1835 self.Nf = 0 1836 for sg in weighted_sessions: 1837 self.Nf += self.rmswd(sessions = sg)['Nf'] 1838 1839 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1840 1841 avgD4x = { 1842 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1843 for sample in self.samples 1844 } 1845 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1846 rD4x = (chi2/self.Nf)**.5 1847 self.repeatability[f'sigma_{self._4x}'] = rD4x 1848 1849 if consolidate: 1850 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1851 1852 1853 def standardization_error(self, session, d4x, D4x, t = 0): 1854 ''' 1855 Compute standardization error for a given session and 1856 (δ47, Δ47) composition. 1857 ''' 1858 a = self.sessions[session]['a'] 1859 b = self.sessions[session]['b'] 1860 c = self.sessions[session]['c'] 1861 a2 = self.sessions[session]['a2'] 1862 b2 = self.sessions[session]['b2'] 1863 c2 = self.sessions[session]['c2'] 1864 CM = self.sessions[session]['CM'] 1865 1866 x, y = D4x, d4x 1867 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1868# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1869 dxdy = -(b+b2*t) / (a+a2*t) 1870 dxdz = 1. / (a+a2*t) 1871 dxda = -x / (a+a2*t) 1872 dxdb = -y / (a+a2*t) 1873 dxdc = -1. / (a+a2*t) 1874 dxda2 = -x * a2 / (a+a2*t) 1875 dxdb2 = -y * t / (a+a2*t) 1876 dxdc2 = -t / (a+a2*t) 1877 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1878 sx = (V @ CM @ V.T) ** .5 1879 return sx 1880 1881 1882 @make_verbal 1883 def summary(self, 1884 dir = 'output', 1885 filename = None, 1886 save_to_file = True, 1887 print_out = True, 1888 ): 1889 ''' 1890 Print out an/or save to disk a summary of the standardization results. 1891 1892 **Parameters** 1893 1894 + `dir`: the directory in which to save the table 1895 + `filename`: the name to the csv file to write to 1896 + `save_to_file`: whether to save the table to disk 1897 + `print_out`: whether to print out the table 1898 ''' 1899 1900 out = [] 1901 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1902 out += [['N analyses (anchors + unknowns)', f"{len(self)} ({len([r for r in self if r['Sample'] in self.anchors])} + {len([r for r in self if r['Sample'] in self.unknowns])})"]] 1903 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1904 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1905 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1906 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1907 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1908 out += [['Model degrees of freedom', f"{self.Nf}"]] 1909 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1910 out += [['Standardization method', self.standardization_method]] 1911 1912 if save_to_file: 1913 if not os.path.exists(dir): 1914 os.makedirs(dir) 1915 if filename is None: 1916 filename = f'D{self._4x}_summary.csv' 1917 with open(f'{dir}/{filename}', 'w') as fid: 1918 fid.write(make_csv(out)) 1919 if print_out: 1920 self.msg('\n' + pretty_table(out, header = 0)) 1921 1922 1923 @make_verbal 1924 def table_of_sessions(self, 1925 dir = 'output', 1926 filename = None, 1927 save_to_file = True, 1928 print_out = True, 1929 output = None, 1930 ): 1931 ''' 1932 Print out an/or save to disk a table of sessions. 1933 1934 **Parameters** 1935 1936 + `dir`: the directory in which to save the table 1937 + `filename`: the name to the csv file to write to 1938 + `save_to_file`: whether to save the table to disk 1939 + `print_out`: whether to print out the table 1940 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1941 if set to `'raw'`: return a list of list of strings 1942 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1943 ''' 1944 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1945 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1946 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1947 1948 out = [['Session','Na','Nu','d13Cwg_VPDB','d18Owg_VSMOW','r_d13C','r_d18O',f'r_D{self._4x}','a ± SE','1e3 x b ± SE','c ± SE']] 1949 if include_a2: 1950 out[-1] += ['a2 ± SE'] 1951 if include_b2: 1952 out[-1] += ['b2 ± SE'] 1953 if include_c2: 1954 out[-1] += ['c2 ± SE'] 1955 for session in self.sessions: 1956 out += [[ 1957 session, 1958 f"{self.sessions[session]['Na']}", 1959 f"{self.sessions[session]['Nu']}", 1960 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1961 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1962 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1963 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1964 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1965 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1966 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1967 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1968 ]] 1969 if include_a2: 1970 if self.sessions[session]['scrambling_drift']: 1971 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1972 else: 1973 out[-1] += [''] 1974 if include_b2: 1975 if self.sessions[session]['slope_drift']: 1976 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1977 else: 1978 out[-1] += [''] 1979 if include_c2: 1980 if self.sessions[session]['wg_drift']: 1981 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1982 else: 1983 out[-1] += [''] 1984 1985 if save_to_file: 1986 if not os.path.exists(dir): 1987 os.makedirs(dir) 1988 if filename is None: 1989 filename = f'D{self._4x}_sessions.csv' 1990 with open(f'{dir}/{filename}', 'w') as fid: 1991 fid.write(make_csv(out)) 1992 if print_out: 1993 self.msg('\n' + pretty_table(out)) 1994 if output == 'raw': 1995 return out 1996 elif output == 'pretty': 1997 return pretty_table(out) 1998 1999 2000 @make_verbal 2001 def table_of_analyses( 2002 self, 2003 dir = 'output', 2004 filename = None, 2005 save_to_file = True, 2006 print_out = True, 2007 output = None, 2008 ): 2009 ''' 2010 Print out an/or save to disk a table of analyses. 2011 2012 **Parameters** 2013 2014 + `dir`: the directory in which to save the table 2015 + `filename`: the name to the csv file to write to 2016 + `save_to_file`: whether to save the table to disk 2017 + `print_out`: whether to print out the table 2018 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2019 if set to `'raw'`: return a list of list of strings 2020 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2021 ''' 2022 2023 out = [['UID','Session','Sample']] 2024 extra_fields = [f for f in [('SampleMass','.2f'),('ColdFingerPressure','.1f'),('AcidReactionYield','.3f')] if f[0] in {k for r in self for k in r}] 2025 for f in extra_fields: 2026 out[-1] += [f[0]] 2027 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 2028 for r in self: 2029 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 2030 for f in extra_fields: 2031 out[-1] += [f"{r[f[0]]:{f[1]}}"] 2032 out[-1] += [ 2033 f"{r['d13Cwg_VPDB']:.3f}", 2034 f"{r['d18Owg_VSMOW']:.3f}", 2035 f"{r['d45']:.6f}", 2036 f"{r['d46']:.6f}", 2037 f"{r['d47']:.6f}", 2038 f"{r['d48']:.6f}", 2039 f"{r['d49']:.6f}", 2040 f"{r['d13C_VPDB']:.6f}", 2041 f"{r['d18O_VSMOW']:.6f}", 2042 f"{r['D47raw']:.6f}", 2043 f"{r['D48raw']:.6f}", 2044 f"{r['D49raw']:.6f}", 2045 f"{r[f'D{self._4x}']:.6f}" 2046 ] 2047 if save_to_file: 2048 if not os.path.exists(dir): 2049 os.makedirs(dir) 2050 if filename is None: 2051 filename = f'D{self._4x}_analyses.csv' 2052 with open(f'{dir}/{filename}', 'w') as fid: 2053 fid.write(make_csv(out)) 2054 if print_out: 2055 self.msg('\n' + pretty_table(out)) 2056 return out 2057 2058 @make_verbal 2059 def covar_table( 2060 self, 2061 correl = False, 2062 dir = 'output', 2063 filename = None, 2064 save_to_file = True, 2065 print_out = True, 2066 output = None, 2067 ): 2068 ''' 2069 Print out, save to disk and/or return the variance-covariance matrix of D4x 2070 for all unknown samples. 2071 2072 **Parameters** 2073 2074 + `dir`: the directory in which to save the csv 2075 + `filename`: the name of the csv file to write to 2076 + `save_to_file`: whether to save the csv 2077 + `print_out`: whether to print out the matrix 2078 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2079 if set to `'raw'`: return a list of list of strings 2080 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2081 ''' 2082 samples = sorted([u for u in self.unknowns]) 2083 out = [[''] + samples] 2084 for s1 in samples: 2085 out.append([s1]) 2086 for s2 in samples: 2087 if correl: 2088 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2089 else: 2090 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2091 2092 if save_to_file: 2093 if not os.path.exists(dir): 2094 os.makedirs(dir) 2095 if filename is None: 2096 if correl: 2097 filename = f'D{self._4x}_correl.csv' 2098 else: 2099 filename = f'D{self._4x}_covar.csv' 2100 with open(f'{dir}/{filename}', 'w') as fid: 2101 fid.write(make_csv(out)) 2102 if print_out: 2103 self.msg('\n'+pretty_table(out)) 2104 if output == 'raw': 2105 return out 2106 elif output == 'pretty': 2107 return pretty_table(out) 2108 2109 @make_verbal 2110 def table_of_samples( 2111 self, 2112 dir = 'output', 2113 filename = None, 2114 save_to_file = True, 2115 print_out = True, 2116 output = None, 2117 ): 2118 ''' 2119 Print out, save to disk and/or return a table of samples. 2120 2121 **Parameters** 2122 2123 + `dir`: the directory in which to save the csv 2124 + `filename`: the name of the csv file to write to 2125 + `save_to_file`: whether to save the csv 2126 + `print_out`: whether to print out the table 2127 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2128 if set to `'raw'`: return a list of list of strings 2129 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2130 ''' 2131 2132 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2133 for sample in self.anchors: 2134 out += [[ 2135 f"{sample}", 2136 f"{self.samples[sample]['N']}", 2137 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2138 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2139 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2140 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2141 ]] 2142 for sample in self.unknowns: 2143 out += [[ 2144 f"{sample}", 2145 f"{self.samples[sample]['N']}", 2146 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2147 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2148 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2149 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2150 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2151 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2152 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2153 ]] 2154 if save_to_file: 2155 if not os.path.exists(dir): 2156 os.makedirs(dir) 2157 if filename is None: 2158 filename = f'D{self._4x}_samples.csv' 2159 with open(f'{dir}/{filename}', 'w') as fid: 2160 fid.write(make_csv(out)) 2161 if print_out: 2162 self.msg('\n'+pretty_table(out)) 2163 if output == 'raw': 2164 return out 2165 elif output == 'pretty': 2166 return pretty_table(out) 2167 2168 2169 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2170 ''' 2171 Generate session plots and save them to disk. 2172 2173 **Parameters** 2174 2175 + `dir`: the directory in which to save the plots 2176 + `figsize`: the width and height (in inches) of each plot 2177 ''' 2178 if not os.path.exists(dir): 2179 os.makedirs(dir) 2180 2181 for session in self.sessions: 2182 sp = self.plot_single_session(session, xylimits = 'constant') 2183 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2184 ppl.close(sp.fig) 2185 2186 2187 @make_verbal 2188 def consolidate_samples(self): 2189 ''' 2190 Compile various statistics for each sample. 2191 2192 For each anchor sample: 2193 2194 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2195 + `SE_D47` or `SE_D48`: set to zero by definition 2196 2197 For each unknown sample: 2198 2199 + `D47` or `D48`: the standardized Δ4x value for this unknown 2200 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2201 2202 For each anchor and unknown: 2203 2204 + `N`: the total number of analyses of this sample 2205 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2206 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2207 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2208 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2209 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2210 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2211 ''' 2212 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2213 for sample in self.samples: 2214 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2215 if self.samples[sample]['N'] > 1: 2216 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2217 2218 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2219 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2220 2221 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2222 if len(D4x_pop) > 2: 2223 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2224 2225 if self.standardization_method == 'pooled': 2226 for sample in self.anchors: 2227 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2228 self.samples[sample][f'SE_D{self._4x}'] = 0. 2229 for sample in self.unknowns: 2230 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2231 try: 2232 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2233 except ValueError: 2234 # when `sample` is constrained by self.standardize(constraints = {...}), 2235 # it is no longer listed in self.standardization.var_names. 2236 # Temporary fix: define SE as zero for now 2237 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2238 2239 elif self.standardization_method == 'indep_sessions': 2240 for sample in self.anchors: 2241 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2242 self.samples[sample][f'SE_D{self._4x}'] = 0. 2243 for sample in self.unknowns: 2244 self.msg(f'Consolidating sample {sample}') 2245 self.unknowns[sample][f'session_D{self._4x}'] = {} 2246 session_avg = [] 2247 for session in self.sessions: 2248 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2249 if sdata: 2250 self.msg(f'{sample} found in session {session}') 2251 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2252 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2253 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2254 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2255 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2256 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2257 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2258 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2259 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2260 wsum = sum([weights[s] for s in weights]) 2261 for s in weights: 2262 self.unknowns[sample][f'session_D{self._4x}'][s] += [self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 / wsum] 2263 2264 2265 def consolidate_sessions(self): 2266 ''' 2267 Compute various statistics for each session. 2268 2269 + `Na`: Number of anchor analyses in the session 2270 + `Nu`: Number of unknown analyses in the session 2271 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2272 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2273 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2274 + `a`: scrambling factor 2275 + `b`: compositional slope 2276 + `c`: WG offset 2277 + `SE_a`: Model stadard erorr of `a` 2278 + `SE_b`: Model stadard erorr of `b` 2279 + `SE_c`: Model stadard erorr of `c` 2280 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2281 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2282 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2283 + `a2`: scrambling factor drift 2284 + `b2`: compositional slope drift 2285 + `c2`: WG offset drift 2286 + `Np`: Number of standardization parameters to fit 2287 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2288 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2289 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2290 ''' 2291 for session in self.sessions: 2292 if 'd13Cwg_VPDB' not in self.sessions[session]: 2293 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2294 if 'd18Owg_VSMOW' not in self.sessions[session]: 2295 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2296 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2297 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2298 2299 self.msg(f'Computing repeatabilities for session {session}') 2300 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2301 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2302 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2303 2304 if self.standardization_method == 'pooled': 2305 for session in self.sessions: 2306 2307 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2308 i = self.standardization.var_names.index(f'a_{pf(session)}') 2309 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2310 2311 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2312 i = self.standardization.var_names.index(f'b_{pf(session)}') 2313 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2314 2315 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2316 i = self.standardization.var_names.index(f'c_{pf(session)}') 2317 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2318 2319 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2320 if self.sessions[session]['scrambling_drift']: 2321 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2322 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2323 else: 2324 self.sessions[session]['SE_a2'] = 0. 2325 2326 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2327 if self.sessions[session]['slope_drift']: 2328 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2329 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2330 else: 2331 self.sessions[session]['SE_b2'] = 0. 2332 2333 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2334 if self.sessions[session]['wg_drift']: 2335 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2336 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2337 else: 2338 self.sessions[session]['SE_c2'] = 0. 2339 2340 i = self.standardization.var_names.index(f'a_{pf(session)}') 2341 j = self.standardization.var_names.index(f'b_{pf(session)}') 2342 k = self.standardization.var_names.index(f'c_{pf(session)}') 2343 CM = np.zeros((6,6)) 2344 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2345 try: 2346 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2347 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2348 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2349 try: 2350 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2351 CM[3,4] = self.standardization.covar[i2,j2] 2352 CM[4,3] = self.standardization.covar[j2,i2] 2353 except ValueError: 2354 pass 2355 try: 2356 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2357 CM[3,5] = self.standardization.covar[i2,k2] 2358 CM[5,3] = self.standardization.covar[k2,i2] 2359 except ValueError: 2360 pass 2361 except ValueError: 2362 pass 2363 try: 2364 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2365 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2366 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2367 try: 2368 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2369 CM[4,5] = self.standardization.covar[j2,k2] 2370 CM[5,4] = self.standardization.covar[k2,j2] 2371 except ValueError: 2372 pass 2373 except ValueError: 2374 pass 2375 try: 2376 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2377 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2378 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2379 except ValueError: 2380 pass 2381 2382 self.sessions[session]['CM'] = CM 2383 2384 elif self.standardization_method == 'indep_sessions': 2385 pass # Not implemented yet 2386 2387 2388 @make_verbal 2389 def repeatabilities(self): 2390 ''' 2391 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2392 (for all samples, for anchors, and for unknowns). 2393 ''' 2394 self.msg('Computing reproducibilities for all sessions') 2395 2396 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2397 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2398 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2399 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2400 self.repeatability[f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', samples = 'all samples') 2401 2402 2403 @make_verbal 2404 def consolidate(self, tables = True, plots = True): 2405 ''' 2406 Collect information about samples, sessions and repeatabilities. 2407 ''' 2408 self.consolidate_samples() 2409 self.consolidate_sessions() 2410 self.repeatabilities() 2411 2412 if tables: 2413 self.summary() 2414 self.table_of_sessions() 2415 self.table_of_analyses() 2416 self.table_of_samples() 2417 2418 if plots: 2419 self.plot_sessions() 2420 2421 2422 @make_verbal 2423 def rmswd(self, 2424 samples = 'all samples', 2425 sessions = 'all sessions', 2426 ): 2427 ''' 2428 Compute the χ2, root mean squared weighted deviation 2429 (i.e. reduced χ2), and corresponding degrees of freedom of the 2430 Δ4x values for samples in `samples` and sessions in `sessions`. 2431 2432 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2433 ''' 2434 if samples == 'all samples': 2435 mysamples = [k for k in self.samples] 2436 elif samples == 'anchors': 2437 mysamples = [k for k in self.anchors] 2438 elif samples == 'unknowns': 2439 mysamples = [k for k in self.unknowns] 2440 else: 2441 mysamples = samples 2442 2443 if sessions == 'all sessions': 2444 sessions = [k for k in self.sessions] 2445 2446 chisq, Nf = 0, 0 2447 for sample in mysamples : 2448 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2449 if len(G) > 1 : 2450 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2451 Nf += (len(G) - 1) 2452 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2453 r = (chisq / Nf)**.5 if Nf > 0 else 0 2454 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2455 return {'rmswd': r, 'chisq': chisq, 'Nf': Nf} 2456 2457 2458 @make_verbal 2459 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2460 ''' 2461 Compute the repeatability of `[r[key] for r in self]` 2462 ''' 2463 # NB: it's debatable whether rD47 should be computed 2464 # with Nf = len(self)-len(self.samples) instead of 2465 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2466 2467 if samples == 'all samples': 2468 mysamples = [k for k in self.samples] 2469 elif samples == 'anchors': 2470 mysamples = [k for k in self.anchors] 2471 elif samples == 'unknowns': 2472 mysamples = [k for k in self.unknowns] 2473 else: 2474 mysamples = samples 2475 2476 if sessions == 'all sessions': 2477 sessions = [k for k in self.sessions] 2478 2479 if key in ['D47', 'D48']: 2480 chisq, Nf = 0, 0 2481 for sample in mysamples : 2482 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2483 if len(X) > 1 : 2484 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2485 if sample in self.unknowns: 2486 Nf += len(X) - 1 2487 else: 2488 Nf += len(X) 2489 if samples in ['anchors', 'all samples']: 2490 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2491 r = (chisq / Nf)**.5 if Nf > 0 else 0 2492 2493 else: # if key not in ['D47', 'D48'] 2494 chisq, Nf = 0, 0 2495 for sample in mysamples : 2496 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2497 if len(X) > 1 : 2498 Nf += len(X) - 1 2499 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2500 r = (chisq / Nf)**.5 if Nf > 0 else 0 2501 2502 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2503 return r 2504 2505 def sample_average(self, samples, weights = 'equal', normalize = True): 2506 ''' 2507 Weighted average Δ4x value of a group of samples, accounting for covariance. 2508 2509 Returns the weighed average Δ4x value and associated SE 2510 of a group of samples. Weights are equal by default. If `normalize` is 2511 true, `weights` will be rescaled so that their sum equals 1. 2512 2513 **Examples** 2514 2515 ```python 2516 self.sample_average(['X','Y'], [1, 2]) 2517 ``` 2518 2519 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2520 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2521 values of samples X and Y, respectively. 2522 2523 ```python 2524 self.sample_average(['X','Y'], [1, -1], normalize = False) 2525 ``` 2526 2527 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2528 ''' 2529 if weights == 'equal': 2530 weights = [1/len(samples)] * len(samples) 2531 2532 if normalize: 2533 s = sum(weights) 2534 if s: 2535 weights = [w/s for w in weights] 2536 2537 try: 2538# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2539# C = self.standardization.covar[indices,:][:,indices] 2540 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2541 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2542 return correlated_sum(X, C, weights) 2543 except ValueError: 2544 return (0., 0.) 2545 2546 2547 def sample_D4x_covar(self, sample1, sample2 = None): 2548 ''' 2549 Covariance between Δ4x values of samples 2550 2551 Returns the error covariance between the average Δ4x values of two 2552 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2553 returns the Δ4x variance for that sample. 2554 ''' 2555 if sample2 is None: 2556 sample2 = sample1 2557 if self.standardization_method == 'pooled': 2558 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2559 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2560 return self.standardization.covar[i, j] 2561 elif self.standardization_method == 'indep_sessions': 2562 if sample1 == sample2: 2563 return self.samples[sample1][f'SE_D{self._4x}']**2 2564 else: 2565 c = 0 2566 for session in self.sessions: 2567 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2568 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2569 if sdata1 and sdata2: 2570 a = self.sessions[session]['a'] 2571 # !! TODO: CM below does not account for temporal changes in standardization parameters 2572 CM = self.sessions[session]['CM'][:3,:3] 2573 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2574 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2575 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2576 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2577 c += ( 2578 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2579 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2580 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2581 @ CM 2582 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2583 ) / a**2 2584 return float(c) 2585 2586 def sample_D4x_correl(self, sample1, sample2 = None): 2587 ''' 2588 Correlation between Δ4x errors of samples 2589 2590 Returns the error correlation between the average Δ4x values of two samples. 2591 ''' 2592 if sample2 is None or sample2 == sample1: 2593 return 1. 2594 return ( 2595 self.sample_D4x_covar(sample1, sample2) 2596 / self.unknowns[sample1][f'SE_D{self._4x}'] 2597 / self.unknowns[sample2][f'SE_D{self._4x}'] 2598 ) 2599 2600 def plot_single_session(self, 2601 session, 2602 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2603 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2604 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2605 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2606 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2607 xylimits = 'free', # | 'constant' 2608 x_label = None, 2609 y_label = None, 2610 error_contour_interval = 'auto', 2611 fig = 'new', 2612 ): 2613 ''' 2614 Generate plot for a single session 2615 ''' 2616 if x_label is None: 2617 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2618 if y_label is None: 2619 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2620 2621 out = _SessionPlot() 2622 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2623 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2624 2625 if fig == 'new': 2626 out.fig = ppl.figure(figsize = (6,6)) 2627 ppl.subplots_adjust(.1,.1,.9,.9) 2628 2629 out.anchor_analyses, = ppl.plot( 2630 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2631 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2632 **kw_plot_anchors) 2633 out.unknown_analyses, = ppl.plot( 2634 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2635 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2636 **kw_plot_unknowns) 2637 out.anchor_avg = ppl.plot( 2638 np.array([ np.array([ 2639 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2640 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2641 ]) for sample in anchors]).T, 2642 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2643 **kw_plot_anchor_avg) 2644 out.unknown_avg = ppl.plot( 2645 np.array([ np.array([ 2646 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2647 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2648 ]) for sample in unknowns]).T, 2649 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2650 **kw_plot_unknown_avg) 2651 if xylimits == 'constant': 2652 x = [r[f'd{self._4x}'] for r in self] 2653 y = [r[f'D{self._4x}'] for r in self] 2654 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2655 w, h = x2-x1, y2-y1 2656 x1 -= w/20 2657 x2 += w/20 2658 y1 -= h/20 2659 y2 += h/20 2660 ppl.axis([x1, x2, y1, y2]) 2661 elif xylimits == 'free': 2662 x1, x2, y1, y2 = ppl.axis() 2663 else: 2664 x1, x2, y1, y2 = ppl.axis(xylimits) 2665 2666 if error_contour_interval != 'none': 2667 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2668 XI,YI = np.meshgrid(xi, yi) 2669 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2670 if error_contour_interval == 'auto': 2671 rng = np.max(SI) - np.min(SI) 2672 if rng <= 0.01: 2673 cinterval = 0.001 2674 elif rng <= 0.03: 2675 cinterval = 0.004 2676 elif rng <= 0.1: 2677 cinterval = 0.01 2678 elif rng <= 0.3: 2679 cinterval = 0.03 2680 elif rng <= 1.: 2681 cinterval = 0.1 2682 else: 2683 cinterval = 0.5 2684 else: 2685 cinterval = error_contour_interval 2686 2687 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2688 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2689 out.clabel = ppl.clabel(out.contour) 2690 2691 ppl.xlabel(x_label) 2692 ppl.ylabel(y_label) 2693 ppl.title(session, weight = 'bold') 2694 ppl.grid(alpha = .2) 2695 out.ax = ppl.gca() 2696 2697 return out 2698 2699 def plot_residuals( 2700 self, 2701 hist = False, 2702 binwidth = 2/3, 2703 dir = 'output', 2704 filename = None, 2705 highlight = [], 2706 colors = None, 2707 figsize = None, 2708 ): 2709 ''' 2710 Plot residuals of each analysis as a function of time (actually, as a function of 2711 the order of analyses in the `D4xdata` object) 2712 2713 + `hist`: whether to add a histogram of residuals 2714 + `histbins`: specify bin edges for the histogram 2715 + `dir`: the directory in which to save the plot 2716 + `highlight`: a list of samples to highlight 2717 + `colors`: a dict of `{<sample>: <color>}` for all samples 2718 + `figsize`: (width, height) of figure 2719 ''' 2720 # Layout 2721 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2722 if hist: 2723 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2724 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2725 else: 2726 ppl.subplots_adjust(.08,.05,.78,.8) 2727 ax1 = ppl.subplot(111) 2728 2729 # Colors 2730 N = len(self.anchors) 2731 if colors is None: 2732 if len(highlight) > 0: 2733 Nh = len(highlight) 2734 if Nh == 1: 2735 colors = {highlight[0]: (0,0,0)} 2736 elif Nh == 3: 2737 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2738 elif Nh == 4: 2739 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2740 else: 2741 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2742 else: 2743 if N == 3: 2744 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2745 elif N == 4: 2746 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2747 else: 2748 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2749 2750 ppl.sca(ax1) 2751 2752 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2753 2754 session = self[0]['Session'] 2755 x1 = 0 2756# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2757 x_sessions = {} 2758 one_or_more_singlets = False 2759 one_or_more_multiplets = False 2760 multiplets = set() 2761 for k,r in enumerate(self): 2762 if r['Session'] != session: 2763 x2 = k-1 2764 x_sessions[session] = (x1+x2)/2 2765 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2766 session = r['Session'] 2767 x1 = k 2768 singlet = len(self.samples[r['Sample']]['data']) == 1 2769 if not singlet: 2770 multiplets.add(r['Sample']) 2771 if r['Sample'] in self.unknowns: 2772 if singlet: 2773 one_or_more_singlets = True 2774 else: 2775 one_or_more_multiplets = True 2776 kw = dict( 2777 marker = 'x' if singlet else '+', 2778 ms = 4 if singlet else 5, 2779 ls = 'None', 2780 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2781 mew = 1, 2782 alpha = 0.2 if singlet else 1, 2783 ) 2784 if highlight and r['Sample'] not in highlight: 2785 kw['alpha'] = 0.2 2786 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2787 x2 = k 2788 x_sessions[session] = (x1+x2)/2 2789 2790 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2791 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2792 if not hist: 2793 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2794 ppl.text(len(self), self.repeatability['r_D47']*1000*self.t95, f" 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", size = 9, alpha = 1, va = 'center') 2795 2796 xmin, xmax, ymin, ymax = ppl.axis() 2797 for s in x_sessions: 2798 ppl.text( 2799 x_sessions[s], 2800 ymax +1, 2801 s, 2802 va = 'bottom', 2803 **( 2804 dict(ha = 'center') 2805 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2806 else dict(ha = 'left', rotation = 45) 2807 ) 2808 ) 2809 2810 if hist: 2811 ppl.sca(ax2) 2812 2813 for s in colors: 2814 kw['marker'] = '+' 2815 kw['ms'] = 5 2816 kw['mec'] = colors[s] 2817 kw['label'] = s 2818 kw['alpha'] = 1 2819 ppl.plot([], [], **kw) 2820 2821 kw['mec'] = (0,0,0) 2822 2823 if one_or_more_singlets: 2824 kw['marker'] = 'x' 2825 kw['ms'] = 4 2826 kw['alpha'] = .2 2827 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2828 ppl.plot([], [], **kw) 2829 2830 if one_or_more_multiplets: 2831 kw['marker'] = '+' 2832 kw['ms'] = 4 2833 kw['alpha'] = 1 2834 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2835 ppl.plot([], [], **kw) 2836 2837 if hist: 2838 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2839 else: 2840 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2841 leg.set_zorder(-1000) 2842 2843 ppl.sca(ax1) 2844 2845 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2846 ppl.xticks([]) 2847 ppl.axis([-1, len(self), None, None]) 2848 2849 if hist: 2850 ppl.sca(ax2) 2851 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2852 ppl.hist( 2853 X, 2854 orientation = 'horizontal', 2855 histtype = 'stepfilled', 2856 ec = [.4]*3, 2857 fc = [.25]*3, 2858 alpha = .25, 2859 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2860 ) 2861 ppl.axis([None, None, ymin, ymax]) 2862 ppl.text(0, 0, 2863 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2864 size = 8, 2865 alpha = 1, 2866 va = 'center', 2867 ha = 'left', 2868 ) 2869 2870 ppl.xticks([]) 2871 ppl.yticks([]) 2872# ax2.spines['left'].set_visible(False) 2873 ax2.spines['right'].set_visible(False) 2874 ax2.spines['top'].set_visible(False) 2875 ax2.spines['bottom'].set_visible(False) 2876 2877 2878 if not os.path.exists(dir): 2879 os.makedirs(dir) 2880 if filename is None: 2881 return fig 2882 elif filename == '': 2883 filename = f'D{self._4x}_residuals.pdf' 2884 ppl.savefig(f'{dir}/{filename}') 2885 ppl.close(fig) 2886 2887 2888 def simulate(self, *args, **kwargs): 2889 ''' 2890 Legacy function with warning message pointing to `virtual_data()` 2891 ''' 2892 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()') 2893 2894 def plot_distribution_of_analyses( 2895 self, 2896 dir = 'output', 2897 filename = None, 2898 vs_time = False, 2899 figsize = (6,4), 2900 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2901 output = None, 2902 ): 2903 ''' 2904 Plot temporal distribution of all analyses in the data set. 2905 2906 **Parameters** 2907 2908 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2909 ''' 2910 2911 asamples = [s for s in self.anchors] 2912 usamples = [s for s in self.unknowns] 2913 if output is None or output == 'fig': 2914 fig = ppl.figure(figsize = figsize) 2915 ppl.subplots_adjust(*subplots_adjust) 2916 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2917 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2918 Xmax += (Xmax-Xmin)/40 2919 Xmin -= (Xmax-Xmin)/41 2920 for k, s in enumerate(asamples + usamples): 2921 if vs_time: 2922 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2923 else: 2924 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2925 Y = [-k for x in X] 2926 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2927 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2928 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2929 ppl.axis([Xmin, Xmax, -k-1, 1]) 2930 ppl.xlabel('\ntime') 2931 ppl.gca().annotate('', 2932 xy = (0.6, -0.02), 2933 xycoords = 'axes fraction', 2934 xytext = (.4, -0.02), 2935 arrowprops = dict(arrowstyle = "->", color = 'k'), 2936 ) 2937 2938 2939 x2 = -1 2940 for session in self.sessions: 2941 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2942 if vs_time: 2943 ppl.axvline(x1, color = 'k', lw = .75) 2944 if x2 > -1: 2945 if not vs_time: 2946 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2947 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2948# from xlrd import xldate_as_datetime 2949# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2950 if vs_time: 2951 ppl.axvline(x2, color = 'k', lw = .75) 2952 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2953 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2954 2955 ppl.xticks([]) 2956 ppl.yticks([]) 2957 2958 if output is None: 2959 if not os.path.exists(dir): 2960 os.makedirs(dir) 2961 if filename == None: 2962 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2963 ppl.savefig(f'{dir}/{filename}') 2964 ppl.close(fig) 2965 elif output == 'ax': 2966 return ppl.gca() 2967 elif output == 'fig': 2968 return fig 2969 2970 2971class D47data(D4xdata): 2972 ''' 2973 Store and process data for a large set of Δ47 analyses, 2974 usually comprising more than one analytical session. 2975 ''' 2976 2977 Nominal_D4x = { 2978 'ETH-1': 0.2052, 2979 'ETH-2': 0.2085, 2980 'ETH-3': 0.6132, 2981 'ETH-4': 0.4511, 2982 'IAEA-C1': 0.3018, 2983 'IAEA-C2': 0.6409, 2984 'MERCK': 0.5135, 2985 } # I-CDES (Bernasconi et al., 2021) 2986 ''' 2987 Nominal Δ47 values assigned to the Δ47 anchor samples, used by 2988 `D47data.standardize()` to normalize unknown samples to an absolute Δ47 2989 reference frame. 2990 2991 By default equal to (after [Bernasconi et al. (2021)](https://doi.org/10.1029/2020GC009588)): 2992 ```py 2993 { 2994 'ETH-1' : 0.2052, 2995 'ETH-2' : 0.2085, 2996 'ETH-3' : 0.6132, 2997 'ETH-4' : 0.4511, 2998 'IAEA-C1' : 0.3018, 2999 'IAEA-C2' : 0.6409, 3000 'MERCK' : 0.5135, 3001 } 3002 ``` 3003 ''' 3004 3005 3006 @property 3007 def Nominal_D47(self): 3008 return self.Nominal_D4x 3009 3010 3011 @Nominal_D47.setter 3012 def Nominal_D47(self, new): 3013 self.Nominal_D4x = dict(**new) 3014 self.refresh() 3015 3016 3017 def __init__(self, l = [], **kwargs): 3018 ''' 3019 **Parameters:** same as `D4xdata.__init__()` 3020 ''' 3021 D4xdata.__init__(self, l = l, mass = '47', **kwargs) 3022 3023 3024 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 3025 ''' 3026 Find all samples for which `Teq` is specified, compute equilibrium Δ47 3027 value for that temperature, and add treat these samples as additional anchors. 3028 3029 **Parameters** 3030 3031 + `fCo2eqD47`: Which CO2 equilibrium law to use 3032 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 3033 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 3034 + `priority`: if `replace`: forget old anchors and only use the new ones; 3035 if `new`: keep pre-existing anchors but update them in case of conflict 3036 between old and new Δ47 values; 3037 if `old`: keep pre-existing anchors but preserve their original Δ47 3038 values in case of conflict. 3039 ''' 3040 f = { 3041 'petersen': fCO2eqD47_Petersen, 3042 'wang': fCO2eqD47_Wang, 3043 }[fCo2eqD47] 3044 foo = {} 3045 for r in self: 3046 if 'Teq' in r: 3047 if r['Sample'] in foo: 3048 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 3049 else: 3050 foo[r['Sample']] = f(r['Teq']) 3051 else: 3052 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 3053 3054 if priority == 'replace': 3055 self.Nominal_D47 = {} 3056 for s in foo: 3057 if priority != 'old' or s not in self.Nominal_D47: 3058 self.Nominal_D47[s] = foo[s] 3059 3060 3061 3062 3063class D48data(D4xdata): 3064 ''' 3065 Store and process data for a large set of Δ48 analyses, 3066 usually comprising more than one analytical session. 3067 ''' 3068 3069 Nominal_D4x = { 3070 'ETH-1': 0.138, 3071 'ETH-2': 0.138, 3072 'ETH-3': 0.270, 3073 'ETH-4': 0.223, 3074 'GU-1': -0.419, 3075 } # (Fiebig et al., 2019, 2021) 3076 ''' 3077 Nominal Δ48 values assigned to the Δ48 anchor samples, used by 3078 `D48data.standardize()` to normalize unknown samples to an absolute Δ48 3079 reference frame. 3080 3081 By default equal to (after [Fiebig et al. (2019)](https://doi.org/10.1016/j.chemgeo.2019.05.019), 3082 Fiebig et al. (in press)): 3083 3084 ```py 3085 { 3086 'ETH-1' : 0.138, 3087 'ETH-2' : 0.138, 3088 'ETH-3' : 0.270, 3089 'ETH-4' : 0.223, 3090 'GU-1' : -0.419, 3091 } 3092 ``` 3093 ''' 3094 3095 3096 @property 3097 def Nominal_D48(self): 3098 return self.Nominal_D4x 3099 3100 3101 @Nominal_D48.setter 3102 def Nominal_D48(self, new): 3103 self.Nominal_D4x = dict(**new) 3104 self.refresh() 3105 3106 3107 def __init__(self, l = [], **kwargs): 3108 ''' 3109 **Parameters:** same as `D4xdata.__init__()` 3110 ''' 3111 D4xdata.__init__(self, l = l, mass = '48', **kwargs) 3112 3113 3114class _SessionPlot(): 3115 ''' 3116 Simple placeholder class 3117 ''' 3118 def __init__(self): 3119 pass
63def fCO2eqD47_Petersen(T): 64 ''' 65 CO2 equilibrium Δ47 value as a function of T (in degrees C) 66 according to [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127). 67 68 ''' 69 return float(_fCO2eqD47_Petersen(T))
CO2 equilibrium Δ47 value as a function of T (in degrees C) according to Petersen et al. (2019).
74def fCO2eqD47_Wang(T): 75 ''' 76 CO2 equilibrium Δ47 value as a function of `T` (in degrees C) 77 according to [Wang et al. (2004)](https://doi.org/10.1016/j.gca.2004.05.039) 78 (supplementary data of [Dennis et al., 2011](https://doi.org/10.1016/j.gca.2011.09.025)). 79 ''' 80 return float(_fCO2eqD47_Wang(T))
CO2 equilibrium Δ47 value as a function of T
(in degrees C)
according to Wang et al. (2004)
(supplementary data of Dennis et al., 2011).
102def make_csv(x, hsep = ',', vsep = '\n'): 103 ''' 104 Formats a list of lists of strings as a CSV 105 106 **Parameters** 107 108 + `x`: the list of lists of strings to format 109 + `hsep`: the field separator (`,` by default) 110 + `vsep`: the line-ending convention to use (`\\n` by default) 111 112 **Example** 113 114 ```py 115 print(make_csv([['a', 'b', 'c'], ['d', 'e', 'f']])) 116 ``` 117 118 outputs: 119 120 ```py 121 a,b,c 122 d,e,f 123 ``` 124 ''' 125 return vsep.join([hsep.join(l) for l in x])
Formats a list of lists of strings as a CSV
Parameters
x
: the list of lists of strings to formathsep
: the field separator (,
by default)vsep
: the line-ending convention to use (\n
by default)
Example
print(make_csv([['a', 'b', 'c'], ['d', 'e', 'f']]))
outputs:
a,b,c
d,e,f
128def pf(txt): 129 ''' 130 Modify string `txt` to follow `lmfit.Parameter()` naming rules. 131 ''' 132 return txt.replace('-','_').replace('.','_').replace(' ','_')
Modify string txt
to follow lmfit.Parameter()
naming rules.
135def smart_type(x): 136 ''' 137 Tries to convert string `x` to a float if it includes a decimal point, or 138 to an integer if it does not. If both attempts fail, return the original 139 string unchanged. 140 ''' 141 try: 142 y = float(x) 143 except ValueError: 144 return x 145 if '.' not in x: 146 return int(y) 147 return y
Tries to convert string x
to a float if it includes a decimal point, or
to an integer if it does not. If both attempts fail, return the original
string unchanged.
150def pretty_table(x, header = 1, hsep = ' ', vsep = '–', align = '<'): 151 ''' 152 Reads a list of lists of strings and outputs an ascii table 153 154 **Parameters** 155 156 + `x`: a list of lists of strings 157 + `header`: the number of lines to treat as header lines 158 + `hsep`: the horizontal separator between columns 159 + `vsep`: the character to use as vertical separator 160 + `align`: string of left (`<`) or right (`>`) alignment characters. 161 162 **Example** 163 164 ```py 165 x = [['A', 'B', 'C'], ['1', '1.9999', 'foo'], ['10', 'x', 'bar']] 166 print(pretty_table(x)) 167 ``` 168 yields: 169 ``` 170 -- ------ --- 171 A B C 172 -- ------ --- 173 1 1.9999 foo 174 10 x bar 175 -- ------ --- 176 ``` 177 178 ''' 179 txt = [] 180 widths = [np.max([len(e) for e in c]) for c in zip(*x)] 181 182 if len(widths) > len(align): 183 align += '>' * (len(widths)-len(align)) 184 sepline = hsep.join([vsep*w for w in widths]) 185 txt += [sepline] 186 for k,l in enumerate(x): 187 if k and k == header: 188 txt += [sepline] 189 txt += [hsep.join([f'{e:{a}{w}}' for e, w, a in zip(l, widths, align)])] 190 txt += [sepline] 191 txt += [''] 192 return '\n'.join(txt)
Reads a list of lists of strings and outputs an ascii table
Parameters
x
: a list of lists of stringsheader
: the number of lines to treat as header lineshsep
: the horizontal separator between columnsvsep
: the character to use as vertical separatoralign
: string of left (<
) or right (>
) alignment characters.
Example
x = [['A', 'B', 'C'], ['1', '1.9999', 'foo'], ['10', 'x', 'bar']]
print(pretty_table(x))
yields:
-- ------ ---
A B C
-- ------ ---
1 1.9999 foo
10 x bar
-- ------ ---
195def transpose_table(x): 196 ''' 197 Transpose a list if lists 198 199 **Parameters** 200 201 + `x`: a list of lists 202 203 **Example** 204 205 ```py 206 x = [[1, 2], [3, 4]] 207 print(transpose_table(x)) # yields: [[1, 3], [2, 4]] 208 ``` 209 ''' 210 return [[e for e in c] for c in zip(*x)]
Transpose a list if lists
Parameters
x
: a list of lists
Example
x = [[1, 2], [3, 4]]
print(transpose_table(x)) # yields: [[1, 3], [2, 4]]
213def w_avg(X, sX) : 214 ''' 215 Compute variance-weighted average 216 217 Returns the value and SE of the weighted average of the elements of `X`, 218 with relative weights equal to their inverse variances (`1/sX**2`). 219 220 **Parameters** 221 222 + `X`: array-like of elements to average 223 + `sX`: array-like of the corresponding SE values 224 225 **Tip** 226 227 If `X` and `sX` are initially arranged as a list of `(x, sx)` doublets, 228 they may be rearranged using `zip()`: 229 230 ```python 231 foo = [(0, 1), (1, 0.5), (2, 0.5)] 232 print(w_avg(*zip(*foo))) # yields: (1.3333333333333333, 0.3333333333333333) 233 ``` 234 ''' 235 X = [ x for x in X ] 236 sX = [ sx for sx in sX ] 237 W = [ sx**-2 for sx in sX ] 238 W = [ w/sum(W) for w in W ] 239 Xavg = sum([ w*x for w,x in zip(W,X) ]) 240 sXavg = sum([ w**2*sx**2 for w,sx in zip(W,sX) ])**.5 241 return Xavg, sXavg
Compute variance-weighted average
Returns the value and SE of the weighted average of the elements of X
,
with relative weights equal to their inverse variances (1/sX**2
).
Parameters
X
: array-like of elements to averagesX
: array-like of the corresponding SE values
Tip
If X
and sX
are initially arranged as a list of (x, sx)
doublets,
they may be rearranged using zip()
:
foo = [(0, 1), (1, 0.5), (2, 0.5)]
print(w_avg(*zip(*foo))) # yields: (1.3333333333333333, 0.3333333333333333)
244def read_csv(filename, sep = ''): 245 ''' 246 Read contents of `filename` in csv format and return a list of dictionaries. 247 248 In the csv string, spaces before and after field separators (`','` by default) 249 are optional. 250 251 **Parameters** 252 253 + `filename`: the csv file to read 254 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 255 whichever appers most often in the contents of `filename`. 256 ''' 257 with open(filename) as fid: 258 txt = fid.read() 259 260 if sep == '': 261 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 262 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 263 return [{k: smart_type(v) for k,v in zip(txt[0], l) if v} for l in txt[1:]]
Read contents of filename
in csv format and return a list of dictionaries.
In the csv string, spaces before and after field separators (','
by default)
are optional.
Parameters
filename
: the csv file to readsep
: csv separator delimiting the fields. By default, use,
,;
, or, whichever appers most often in the contents of
filename
.
266def simulate_single_analysis( 267 sample = 'MYSAMPLE', 268 d13Cwg_VPDB = -4., d18Owg_VSMOW = 26., 269 d13C_VPDB = None, d18O_VPDB = None, 270 D47 = None, D48 = None, D49 = 0., D17O = 0., 271 a47 = 1., b47 = 0., c47 = -0.9, 272 a48 = 1., b48 = 0., c48 = -0.45, 273 Nominal_D47 = None, 274 Nominal_D48 = None, 275 Nominal_d13C_VPDB = None, 276 Nominal_d18O_VPDB = None, 277 ALPHA_18O_ACID_REACTION = None, 278 R13_VPDB = None, 279 R17_VSMOW = None, 280 R18_VSMOW = None, 281 LAMBDA_17 = None, 282 R18_VPDB = None, 283 ): 284 ''' 285 Compute working-gas delta values for a single analysis, assuming a stochastic working 286 gas and a “perfect” measurement (i.e. raw Δ values are identical to absolute values). 287 288 **Parameters** 289 290 + `sample`: sample name 291 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 292 (respectively –4 and +26 ‰ by default) 293 + `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 294 + `D47`, `D48`, `D49`, `D17O`: clumped-isotope and oxygen-17 anomalies 295 of the carbonate sample 296 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and 297 Δ48 values if `D47` or `D48` are not specified 298 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 299 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 300 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 301 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 302 correction parameters (by default equal to the `D4xdata` default values) 303 304 Returns a dictionary with fields 305 `['Sample', 'D17O', 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'd45', 'd46', 'd47', 'd48', 'd49']`. 306 ''' 307 308 if Nominal_d13C_VPDB is None: 309 Nominal_d13C_VPDB = D4xdata().Nominal_d13C_VPDB 310 311 if Nominal_d18O_VPDB is None: 312 Nominal_d18O_VPDB = D4xdata().Nominal_d18O_VPDB 313 314 if ALPHA_18O_ACID_REACTION is None: 315 ALPHA_18O_ACID_REACTION = D4xdata().ALPHA_18O_ACID_REACTION 316 317 if R13_VPDB is None: 318 R13_VPDB = D4xdata().R13_VPDB 319 320 if R17_VSMOW is None: 321 R17_VSMOW = D4xdata().R17_VSMOW 322 323 if R18_VSMOW is None: 324 R18_VSMOW = D4xdata().R18_VSMOW 325 326 if LAMBDA_17 is None: 327 LAMBDA_17 = D4xdata().LAMBDA_17 328 329 if R18_VPDB is None: 330 R18_VPDB = D4xdata().R18_VPDB 331 332 R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW) ** LAMBDA_17 333 334 if Nominal_D47 is None: 335 Nominal_D47 = D47data().Nominal_D47 336 337 if Nominal_D48 is None: 338 Nominal_D48 = D48data().Nominal_D48 339 340 if d13C_VPDB is None: 341 if sample in Nominal_d13C_VPDB: 342 d13C_VPDB = Nominal_d13C_VPDB[sample] 343 else: 344 raise KeyError(f"Sample {sample} is missing d13C_VDP value, and it is not defined in Nominal_d13C_VDP.") 345 346 if d18O_VPDB is None: 347 if sample in Nominal_d18O_VPDB: 348 d18O_VPDB = Nominal_d18O_VPDB[sample] 349 else: 350 raise KeyError(f"Sample {sample} is missing d18O_VPDB value, and it is not defined in Nominal_d18O_VPDB.") 351 352 if D47 is None: 353 if sample in Nominal_D47: 354 D47 = Nominal_D47[sample] 355 else: 356 raise KeyError(f"Sample {sample} is missing D47 value, and it is not defined in Nominal_D47.") 357 358 if D48 is None: 359 if sample in Nominal_D48: 360 D48 = Nominal_D48[sample] 361 else: 362 raise KeyError(f"Sample {sample} is missing D48 value, and it is not defined in Nominal_D48.") 363 364 X = D4xdata() 365 X.R13_VPDB = R13_VPDB 366 X.R17_VSMOW = R17_VSMOW 367 X.R18_VSMOW = R18_VSMOW 368 X.LAMBDA_17 = LAMBDA_17 369 X.R18_VPDB = R18_VPDB 370 X.R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW)**LAMBDA_17 371 372 R45wg, R46wg, R47wg, R48wg, R49wg = X.compute_isobar_ratios( 373 R13 = R13_VPDB * (1 + d13Cwg_VPDB/1000), 374 R18 = R18_VSMOW * (1 + d18Owg_VSMOW/1000), 375 ) 376 R45, R46, R47, R48, R49 = X.compute_isobar_ratios( 377 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 378 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 379 D17O=D17O, D47=D47, D48=D48, D49=D49, 380 ) 381 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = X.compute_isobar_ratios( 382 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 383 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 384 D17O=D17O, 385 ) 386 387 d45 = 1000 * (R45/R45wg - 1) 388 d46 = 1000 * (R46/R46wg - 1) 389 d47 = 1000 * (R47/R47wg - 1) 390 d48 = 1000 * (R48/R48wg - 1) 391 d49 = 1000 * (R49/R49wg - 1) 392 393 for k in range(3): # dumb iteration to adjust for small changes in d47 394 R47raw = (1 + (a47 * D47 + b47 * d47 + c47)/1000) * R47stoch 395 R48raw = (1 + (a48 * D48 + b48 * d48 + c48)/1000) * R48stoch 396 d47 = 1000 * (R47raw/R47wg - 1) 397 d48 = 1000 * (R48raw/R48wg - 1) 398 399 return dict( 400 Sample = sample, 401 D17O = D17O, 402 d13Cwg_VPDB = d13Cwg_VPDB, 403 d18Owg_VSMOW = d18Owg_VSMOW, 404 d45 = d45, 405 d46 = d46, 406 d47 = d47, 407 d48 = d48, 408 d49 = d49, 409 )
Compute working-gas delta values for a single analysis, assuming a stochastic working gas and a “perfect” measurement (i.e. raw Δ values are identical to absolute values).
Parameters
sample
: sample named13Cwg_VPDB
,d18Owg_VSMOW
: bulk composition of the working gas (respectively –4 and +26 ‰ by default)d13C_VPDB
,d18O_VPDB
: bulk composition of the carbonate sampleD47
,D48
,D49
,D17O
: clumped-isotope and oxygen-17 anomalies of the carbonate sampleNominal_D47
,Nominal_D48
: where to lookup Δ47 and Δ48 values ifD47
orD48
are not specifiedNominal_d13C_VPDB
,Nominal_d18O_VPDB
: where to lookup δ13C and δ18O values ifd13C_VPDB
ord18O_VPDB
are not specifiedALPHA_18O_ACID_REACTION
: 18O/16O acid fractionation factorR13_VPDB
,R17_VSMOW
,R18_VSMOW
,LAMBDA_17
,R18_VPDB
: oxygen-17 correction parameters (by default equal to theD4xdata
default values)
Returns a dictionary with fields
['Sample', 'D17O', 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'd45', 'd46', 'd47', 'd48', 'd49']
.
412def virtual_data( 413 samples = [], 414 a47 = 1., b47 = 0., c47 = -0.9, 415 a48 = 1., b48 = 0., c48 = -0.45, 416 rD47 = 0.015, rD48 = 0.045, 417 d13Cwg_VPDB = None, d18Owg_VSMOW = None, 418 session = None, 419 Nominal_D47 = None, Nominal_D48 = None, 420 Nominal_d13C_VPDB = None, Nominal_d18O_VPDB = None, 421 ALPHA_18O_ACID_REACTION = None, 422 R13_VPDB = None, 423 R17_VSMOW = None, 424 R18_VSMOW = None, 425 LAMBDA_17 = None, 426 R18_VPDB = None, 427 seed = 0, 428 ): 429 ''' 430 Return list with simulated analyses from a single session. 431 432 **Parameters** 433 434 + `samples`: a list of entries; each entry is a dictionary with the following fields: 435 * `Sample`: the name of the sample 436 * `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 437 * `D47`, `D48`, `D49`, `D17O` (all optional): clumped-isotope and oxygen-17 anomalies of the carbonate sample 438 * `N`: how many analyses to generate for this sample 439 + `a47`: scrambling factor for Δ47 440 + `b47`: compositional nonlinearity for Δ47 441 + `c47`: working gas offset for Δ47 442 + `a48`: scrambling factor for Δ48 443 + `b48`: compositional nonlinearity for Δ48 444 + `c48`: working gas offset for Δ48 445 + `rD47`: analytical repeatability of Δ47 446 + `rD48`: analytical repeatability of Δ48 447 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 448 (by default equal to the `simulate_single_analysis` default values) 449 + `session`: name of the session (no name by default) 450 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and Δ48 values 451 if `D47` or `D48` are not specified (by default equal to the `simulate_single_analysis` defaults) 452 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 453 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 454 (by default equal to the `simulate_single_analysis` defaults) 455 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 456 (by default equal to the `simulate_single_analysis` defaults) 457 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 458 correction parameters (by default equal to the `simulate_single_analysis` default) 459 + `seed`: explicitly set to a non-zero value to achieve random but repeatable simulations 460 461 462 Here is an example of using this method to generate an arbitrary combination of 463 anchors and unknowns for a bunch of sessions: 464 465 ```py 466 args = dict( 467 samples = [ 468 dict(Sample = 'ETH-1', N = 4), 469 dict(Sample = 'ETH-2', N = 5), 470 dict(Sample = 'ETH-3', N = 6), 471 dict(Sample = 'FOO', N = 2, 472 d13C_VPDB = -5., d18O_VPDB = -10., 473 D47 = 0.3, D48 = 0.15), 474 ], rD47 = 0.010, rD48 = 0.030) 475 476 session1 = virtual_data(session = 'Session_01', **args, seed = 123) 477 session2 = virtual_data(session = 'Session_02', **args, seed = 1234) 478 session3 = virtual_data(session = 'Session_03', **args, seed = 12345) 479 session4 = virtual_data(session = 'Session_04', **args, seed = 123456) 480 481 D = D47data(session1 + session2 + session3 + session4) 482 483 D.crunch() 484 D.standardize() 485 486 D.table_of_sessions(verbose = True, save_to_file = False) 487 D.table_of_samples(verbose = True, save_to_file = False) 488 D.table_of_analyses(verbose = True, save_to_file = False) 489 ``` 490 491 This should output something like: 492 493 ``` 494 [table_of_sessions] 495 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 496 Session Na Nu d13Cwg_VPDB d18Owg_VSMOW r_d13C r_d18O r_D47 a ± SE 1e3 x b ± SE c ± SE 497 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 498 Session_01 15 2 -4.000 26.000 0.0000 0.0000 0.0110 0.997 ± 0.017 -0.097 ± 0.244 -0.896 ± 0.006 499 Session_02 15 2 -4.000 26.000 0.0000 0.0000 0.0109 1.002 ± 0.017 -0.110 ± 0.244 -0.901 ± 0.006 500 Session_03 15 2 -4.000 26.000 0.0000 0.0000 0.0107 1.010 ± 0.017 -0.037 ± 0.244 -0.904 ± 0.006 501 Session_04 15 2 -4.000 26.000 0.0000 0.0000 0.0106 1.001 ± 0.017 -0.181 ± 0.244 -0.894 ± 0.006 502 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 503 504 [table_of_samples] 505 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 506 Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene 507 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 508 ETH-1 16 2.02 37.02 0.2052 0.0079 509 ETH-2 20 -10.17 19.88 0.2085 0.0100 510 ETH-3 24 1.71 37.45 0.6132 0.0105 511 FOO 8 -5.00 28.91 0.2989 0.0040 ± 0.0080 0.0101 0.638 512 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 513 514 [table_of_analyses] 515 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 516 UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47 517 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 518 1 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.122986 21.273526 27.780042 2.020000 37.024281 -0.706013 -0.328878 -0.000013 0.192554 519 2 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.130144 21.282615 27.780042 2.020000 37.024281 -0.698974 -0.319981 -0.000013 0.199615 520 3 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.149219 21.299572 27.780042 2.020000 37.024281 -0.680215 -0.303383 -0.000013 0.218429 521 4 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.136616 21.233128 27.780042 2.020000 37.024281 -0.692609 -0.368421 -0.000013 0.205998 522 5 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.697171 -12.203054 -18.023381 -10.170000 19.875825 -0.680771 -0.290128 -0.000002 0.215054 523 6 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701124 -12.184422 -18.023381 -10.170000 19.875825 -0.684772 -0.271272 -0.000002 0.211041 524 7 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715105 -12.195251 -18.023381 -10.170000 19.875825 -0.698923 -0.282232 -0.000002 0.196848 525 8 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701529 -12.204963 -18.023381 -10.170000 19.875825 -0.685182 -0.292061 -0.000002 0.210630 526 9 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.711420 -12.228478 -18.023381 -10.170000 19.875825 -0.695193 -0.315859 -0.000002 0.200589 527 10 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.666719 22.296486 28.306614 1.710000 37.450394 -0.290459 -0.147284 -0.000014 0.609363 528 11 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.671553 22.291060 28.306614 1.710000 37.450394 -0.285706 -0.152592 -0.000014 0.614130 529 12 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.652854 22.273271 28.306614 1.710000 37.450394 -0.304093 -0.169990 -0.000014 0.595689 530 13 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.684168 22.263156 28.306614 1.710000 37.450394 -0.273302 -0.179883 -0.000014 0.626572 531 14 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.662702 22.253578 28.306614 1.710000 37.450394 -0.294409 -0.189251 -0.000014 0.605401 532 15 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.681957 22.230907 28.306614 1.710000 37.450394 -0.275476 -0.211424 -0.000014 0.624391 533 16 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.312044 5.395798 4.665655 -5.000000 28.907344 -0.598436 -0.268176 -0.000006 0.298996 534 17 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.328123 5.307086 4.665655 -5.000000 28.907344 -0.582387 -0.356389 -0.000006 0.315092 535 18 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.122201 21.340606 27.780042 2.020000 37.024281 -0.706785 -0.263217 -0.000013 0.195135 536 19 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.134868 21.305714 27.780042 2.020000 37.024281 -0.694328 -0.297370 -0.000013 0.207564 537 20 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.140008 21.261931 27.780042 2.020000 37.024281 -0.689273 -0.340227 -0.000013 0.212607 538 21 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.135540 21.298472 27.780042 2.020000 37.024281 -0.693667 -0.304459 -0.000013 0.208224 539 22 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701213 -12.202602 -18.023381 -10.170000 19.875825 -0.684862 -0.289671 -0.000002 0.213842 540 23 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.685649 -12.190405 -18.023381 -10.170000 19.875825 -0.669108 -0.277327 -0.000002 0.229559 541 24 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.719003 -12.257955 -18.023381 -10.170000 19.875825 -0.702869 -0.345692 -0.000002 0.195876 542 25 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700592 -12.204641 -18.023381 -10.170000 19.875825 -0.684233 -0.291735 -0.000002 0.214469 543 26 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720426 -12.214561 -18.023381 -10.170000 19.875825 -0.704308 -0.301774 -0.000002 0.194439 544 27 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.673044 22.262090 28.306614 1.710000 37.450394 -0.284240 -0.180926 -0.000014 0.616730 545 28 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.666542 22.263401 28.306614 1.710000 37.450394 -0.290634 -0.179643 -0.000014 0.610350 546 29 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.680487 22.243486 28.306614 1.710000 37.450394 -0.276921 -0.199121 -0.000014 0.624031 547 30 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.663900 22.245175 28.306614 1.710000 37.450394 -0.293231 -0.197469 -0.000014 0.607759 548 31 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.674379 22.301309 28.306614 1.710000 37.450394 -0.282927 -0.142568 -0.000014 0.618039 549 32 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.660825 22.270466 28.306614 1.710000 37.450394 -0.296255 -0.172733 -0.000014 0.604742 550 33 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.294076 5.349940 4.665655 -5.000000 28.907344 -0.616369 -0.313776 -0.000006 0.283707 551 34 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.313775 5.292121 4.665655 -5.000000 28.907344 -0.596708 -0.371269 -0.000006 0.303323 552 35 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.121613 21.259909 27.780042 2.020000 37.024281 -0.707364 -0.342207 -0.000013 0.194934 553 36 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.145714 21.304889 27.780042 2.020000 37.024281 -0.683661 -0.298178 -0.000013 0.218401 554 37 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.126573 21.325093 27.780042 2.020000 37.024281 -0.702485 -0.278401 -0.000013 0.199764 555 38 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.132057 21.323211 27.780042 2.020000 37.024281 -0.697092 -0.280244 -0.000013 0.205104 556 39 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.708448 -12.232023 -18.023381 -10.170000 19.875825 -0.692185 -0.319447 -0.000002 0.208915 557 40 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.714417 -12.202504 -18.023381 -10.170000 19.875825 -0.698226 -0.289572 -0.000002 0.202934 558 41 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720039 -12.264469 -18.023381 -10.170000 19.875825 -0.703917 -0.352285 -0.000002 0.197300 559 42 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701953 -12.228550 -18.023381 -10.170000 19.875825 -0.685611 -0.315932 -0.000002 0.215423 560 43 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.704535 -12.213634 -18.023381 -10.170000 19.875825 -0.688224 -0.300836 -0.000002 0.212837 561 44 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.652920 22.230043 28.306614 1.710000 37.450394 -0.304028 -0.212269 -0.000014 0.594265 562 45 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.691485 22.261017 28.306614 1.710000 37.450394 -0.266106 -0.181975 -0.000014 0.631810 563 46 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.679119 22.305357 28.306614 1.710000 37.450394 -0.278266 -0.138609 -0.000014 0.619771 564 47 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.663623 22.327286 28.306614 1.710000 37.450394 -0.293503 -0.117161 -0.000014 0.604685 565 48 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.678524 22.282103 28.306614 1.710000 37.450394 -0.278851 -0.161352 -0.000014 0.619192 566 49 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.666246 22.283361 28.306614 1.710000 37.450394 -0.290925 -0.160121 -0.000014 0.607238 567 50 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.309929 5.340249 4.665655 -5.000000 28.907344 -0.600546 -0.323413 -0.000006 0.300148 568 51 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.317548 5.334102 4.665655 -5.000000 28.907344 -0.592942 -0.329524 -0.000006 0.307676 569 52 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.136865 21.300298 27.780042 2.020000 37.024281 -0.692364 -0.302672 -0.000013 0.204033 570 53 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.133538 21.291260 27.780042 2.020000 37.024281 -0.695637 -0.311519 -0.000013 0.200762 571 54 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.139991 21.319865 27.780042 2.020000 37.024281 -0.689290 -0.283519 -0.000013 0.207107 572 55 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.145748 21.330075 27.780042 2.020000 37.024281 -0.683629 -0.273524 -0.000013 0.212766 573 56 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702989 -12.202762 -18.023381 -10.170000 19.875825 -0.686660 -0.289833 -0.000002 0.204507 574 57 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.692830 -12.240287 -18.023381 -10.170000 19.875825 -0.676377 -0.327811 -0.000002 0.214786 575 58 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702899 -12.180291 -18.023381 -10.170000 19.875825 -0.686568 -0.267091 -0.000002 0.204598 576 59 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.709282 -12.282257 -18.023381 -10.170000 19.875825 -0.693029 -0.370287 -0.000002 0.198140 577 60 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.679330 -12.235994 -18.023381 -10.170000 19.875825 -0.662712 -0.323466 -0.000002 0.228446 578 61 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.695594 22.238663 28.306614 1.710000 37.450394 -0.262066 -0.203838 -0.000014 0.634200 579 62 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.663504 22.286354 28.306614 1.710000 37.450394 -0.293620 -0.157194 -0.000014 0.602656 580 63 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666457 22.254290 28.306614 1.710000 37.450394 -0.290717 -0.188555 -0.000014 0.605558 581 64 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666910 22.223232 28.306614 1.710000 37.450394 -0.290271 -0.218930 -0.000014 0.606004 582 65 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.679662 22.257256 28.306614 1.710000 37.450394 -0.277732 -0.185653 -0.000014 0.618539 583 66 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.676768 22.267680 28.306614 1.710000 37.450394 -0.280578 -0.175459 -0.000014 0.615693 584 67 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.307663 5.317330 4.665655 -5.000000 28.907344 -0.602808 -0.346202 -0.000006 0.290853 585 68 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.308562 5.331400 4.665655 -5.000000 28.907344 -0.601911 -0.332212 -0.000006 0.291749 586 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 587 ``` 588 ''' 589 590 kwargs = locals().copy() 591 592 from numpy import random as nprandom 593 if seed: 594 rng = nprandom.default_rng(seed) 595 else: 596 rng = nprandom.default_rng() 597 598 N = sum([s['N'] for s in samples]) 599 errors47 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 600 errors47 *= rD47 / stdev(errors47) # scale errors to rD47 601 errors48 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 602 errors48 *= rD48 / stdev(errors48) # scale errors to rD48 603 604 k = 0 605 out = [] 606 for s in samples: 607 kw = {} 608 kw['sample'] = s['Sample'] 609 kw = { 610 **kw, 611 **{var: kwargs[var] 612 for var in [ 613 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'ALPHA_18O_ACID_REACTION', 614 'Nominal_D47', 'Nominal_D48', 'Nominal_d13C_VPDB', 'Nominal_d18O_VPDB', 615 'R13_VPDB', 'R17_VSMOW', 'R18_VSMOW', 'LAMBDA_17', 'R18_VPDB', 616 'a47', 'b47', 'c47', 'a48', 'b48', 'c48', 617 ] 618 if kwargs[var] is not None}, 619 **{var: s[var] 620 for var in ['d13C_VPDB', 'd18O_VPDB', 'D47', 'D48', 'D49', 'D17O'] 621 if var in s}, 622 } 623 624 sN = s['N'] 625 while sN: 626 out.append(simulate_single_analysis(**kw)) 627 out[-1]['d47'] += errors47[k] * a47 628 out[-1]['d48'] += errors48[k] * a48 629 sN -= 1 630 k += 1 631 632 if session is not None: 633 for r in out: 634 r['Session'] = session 635 return out
Return list with simulated analyses from a single session.
Parameters
samples
: a list of entries; each entry is a dictionary with the following fields:Sample
: the name of the sampled13C_VPDB
,d18O_VPDB
: bulk composition of the carbonate sampleD47
,D48
,D49
,D17O
(all optional): clumped-isotope and oxygen-17 anomalies of the carbonate sampleN
: how many analyses to generate for this sample
a47
: scrambling factor for Δ47b47
: compositional nonlinearity for Δ47c47
: working gas offset for Δ47a48
: scrambling factor for Δ48b48
: compositional nonlinearity for Δ48c48
: working gas offset for Δ48rD47
: analytical repeatability of Δ47rD48
: analytical repeatability of Δ48d13Cwg_VPDB
,d18Owg_VSMOW
: bulk composition of the working gas (by default equal to thesimulate_single_analysis
default values)session
: name of the session (no name by default)Nominal_D47
,Nominal_D48
: where to lookup Δ47 and Δ48 values ifD47
orD48
are not specified (by default equal to thesimulate_single_analysis
defaults)Nominal_d13C_VPDB
,Nominal_d18O_VPDB
: where to lookup δ13C and δ18O values ifd13C_VPDB
ord18O_VPDB
are not specified (by default equal to thesimulate_single_analysis
defaults)ALPHA_18O_ACID_REACTION
: 18O/16O acid fractionation factor (by default equal to thesimulate_single_analysis
defaults)R13_VPDB
,R17_VSMOW
,R18_VSMOW
,LAMBDA_17
,R18_VPDB
: oxygen-17 correction parameters (by default equal to thesimulate_single_analysis
default)seed
: explicitly set to a non-zero value to achieve random but repeatable simulations
Here is an example of using this method to generate an arbitrary combination of anchors and unknowns for a bunch of sessions:
args = dict(
samples = [
dict(Sample = 'ETH-1', N = 4),
dict(Sample = 'ETH-2', N = 5),
dict(Sample = 'ETH-3', N = 6),
dict(Sample = 'FOO', N = 2,
d13C_VPDB = -5., d18O_VPDB = -10.,
D47 = 0.3, D48 = 0.15),
], rD47 = 0.010, rD48 = 0.030)
session1 = virtual_data(session = 'Session_01', **args, seed = 123)
session2 = virtual_data(session = 'Session_02', **args, seed = 1234)
session3 = virtual_data(session = 'Session_03', **args, seed = 12345)
session4 = virtual_data(session = 'Session_04', **args, seed = 123456)
D = D47data(session1 + session2 + session3 + session4)
D.crunch()
D.standardize()
D.table_of_sessions(verbose = True, save_to_file = False)
D.table_of_samples(verbose = True, save_to_file = False)
D.table_of_analyses(verbose = True, save_to_file = False)
This should output something like:
[table_of_sessions]
–––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– ––––––––––––––
Session Na Nu d13Cwg_VPDB d18Owg_VSMOW r_d13C r_d18O r_D47 a ± SE 1e3 x b ± SE c ± SE
–––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– ––––––––––––––
Session_01 15 2 -4.000 26.000 0.0000 0.0000 0.0110 0.997 ± 0.017 -0.097 ± 0.244 -0.896 ± 0.006
Session_02 15 2 -4.000 26.000 0.0000 0.0000 0.0109 1.002 ± 0.017 -0.110 ± 0.244 -0.901 ± 0.006
Session_03 15 2 -4.000 26.000 0.0000 0.0000 0.0107 1.010 ± 0.017 -0.037 ± 0.244 -0.904 ± 0.006
Session_04 15 2 -4.000 26.000 0.0000 0.0000 0.0106 1.001 ± 0.017 -0.181 ± 0.244 -0.894 ± 0.006
–––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– ––––––––––––––
[table_of_samples]
–––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– ––––––––
Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene
–––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– ––––––––
ETH-1 16 2.02 37.02 0.2052 0.0079
ETH-2 20 -10.17 19.88 0.2085 0.0100
ETH-3 24 1.71 37.45 0.6132 0.0105
FOO 8 -5.00 28.91 0.2989 0.0040 ± 0.0080 0.0101 0.638
–––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– ––––––––
[table_of_analyses]
––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– ––––––––
UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47
––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– ––––––––
1 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.122986 21.273526 27.780042 2.020000 37.024281 -0.706013 -0.328878 -0.000013 0.192554
2 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.130144 21.282615 27.780042 2.020000 37.024281 -0.698974 -0.319981 -0.000013 0.199615
3 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.149219 21.299572 27.780042 2.020000 37.024281 -0.680215 -0.303383 -0.000013 0.218429
4 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.136616 21.233128 27.780042 2.020000 37.024281 -0.692609 -0.368421 -0.000013 0.205998
5 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.697171 -12.203054 -18.023381 -10.170000 19.875825 -0.680771 -0.290128 -0.000002 0.215054
6 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701124 -12.184422 -18.023381 -10.170000 19.875825 -0.684772 -0.271272 -0.000002 0.211041
7 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715105 -12.195251 -18.023381 -10.170000 19.875825 -0.698923 -0.282232 -0.000002 0.196848
8 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701529 -12.204963 -18.023381 -10.170000 19.875825 -0.685182 -0.292061 -0.000002 0.210630
9 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.711420 -12.228478 -18.023381 -10.170000 19.875825 -0.695193 -0.315859 -0.000002 0.200589
10 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.666719 22.296486 28.306614 1.710000 37.450394 -0.290459 -0.147284 -0.000014 0.609363
11 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.671553 22.291060 28.306614 1.710000 37.450394 -0.285706 -0.152592 -0.000014 0.614130
12 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.652854 22.273271 28.306614 1.710000 37.450394 -0.304093 -0.169990 -0.000014 0.595689
13 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.684168 22.263156 28.306614 1.710000 37.450394 -0.273302 -0.179883 -0.000014 0.626572
14 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.662702 22.253578 28.306614 1.710000 37.450394 -0.294409 -0.189251 -0.000014 0.605401
15 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.681957 22.230907 28.306614 1.710000 37.450394 -0.275476 -0.211424 -0.000014 0.624391
16 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.312044 5.395798 4.665655 -5.000000 28.907344 -0.598436 -0.268176 -0.000006 0.298996
17 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.328123 5.307086 4.665655 -5.000000 28.907344 -0.582387 -0.356389 -0.000006 0.315092
18 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.122201 21.340606 27.780042 2.020000 37.024281 -0.706785 -0.263217 -0.000013 0.195135
19 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.134868 21.305714 27.780042 2.020000 37.024281 -0.694328 -0.297370 -0.000013 0.207564
20 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.140008 21.261931 27.780042 2.020000 37.024281 -0.689273 -0.340227 -0.000013 0.212607
21 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.135540 21.298472 27.780042 2.020000 37.024281 -0.693667 -0.304459 -0.000013 0.208224
22 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701213 -12.202602 -18.023381 -10.170000 19.875825 -0.684862 -0.289671 -0.000002 0.213842
23 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.685649 -12.190405 -18.023381 -10.170000 19.875825 -0.669108 -0.277327 -0.000002 0.229559
24 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.719003 -12.257955 -18.023381 -10.170000 19.875825 -0.702869 -0.345692 -0.000002 0.195876
25 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700592 -12.204641 -18.023381 -10.170000 19.875825 -0.684233 -0.291735 -0.000002 0.214469
26 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720426 -12.214561 -18.023381 -10.170000 19.875825 -0.704308 -0.301774 -0.000002 0.194439
27 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.673044 22.262090 28.306614 1.710000 37.450394 -0.284240 -0.180926 -0.000014 0.616730
28 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.666542 22.263401 28.306614 1.710000 37.450394 -0.290634 -0.179643 -0.000014 0.610350
29 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.680487 22.243486 28.306614 1.710000 37.450394 -0.276921 -0.199121 -0.000014 0.624031
30 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.663900 22.245175 28.306614 1.710000 37.450394 -0.293231 -0.197469 -0.000014 0.607759
31 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.674379 22.301309 28.306614 1.710000 37.450394 -0.282927 -0.142568 -0.000014 0.618039
32 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.660825 22.270466 28.306614 1.710000 37.450394 -0.296255 -0.172733 -0.000014 0.604742
33 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.294076 5.349940 4.665655 -5.000000 28.907344 -0.616369 -0.313776 -0.000006 0.283707
34 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.313775 5.292121 4.665655 -5.000000 28.907344 -0.596708 -0.371269 -0.000006 0.303323
35 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.121613 21.259909 27.780042 2.020000 37.024281 -0.707364 -0.342207 -0.000013 0.194934
36 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.145714 21.304889 27.780042 2.020000 37.024281 -0.683661 -0.298178 -0.000013 0.218401
37 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.126573 21.325093 27.780042 2.020000 37.024281 -0.702485 -0.278401 -0.000013 0.199764
38 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.132057 21.323211 27.780042 2.020000 37.024281 -0.697092 -0.280244 -0.000013 0.205104
39 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.708448 -12.232023 -18.023381 -10.170000 19.875825 -0.692185 -0.319447 -0.000002 0.208915
40 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.714417 -12.202504 -18.023381 -10.170000 19.875825 -0.698226 -0.289572 -0.000002 0.202934
41 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720039 -12.264469 -18.023381 -10.170000 19.875825 -0.703917 -0.352285 -0.000002 0.197300
42 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701953 -12.228550 -18.023381 -10.170000 19.875825 -0.685611 -0.315932 -0.000002 0.215423
43 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.704535 -12.213634 -18.023381 -10.170000 19.875825 -0.688224 -0.300836 -0.000002 0.212837
44 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.652920 22.230043 28.306614 1.710000 37.450394 -0.304028 -0.212269 -0.000014 0.594265
45 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.691485 22.261017 28.306614 1.710000 37.450394 -0.266106 -0.181975 -0.000014 0.631810
46 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.679119 22.305357 28.306614 1.710000 37.450394 -0.278266 -0.138609 -0.000014 0.619771
47 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.663623 22.327286 28.306614 1.710000 37.450394 -0.293503 -0.117161 -0.000014 0.604685
48 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.678524 22.282103 28.306614 1.710000 37.450394 -0.278851 -0.161352 -0.000014 0.619192
49 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.666246 22.283361 28.306614 1.710000 37.450394 -0.290925 -0.160121 -0.000014 0.607238
50 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.309929 5.340249 4.665655 -5.000000 28.907344 -0.600546 -0.323413 -0.000006 0.300148
51 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.317548 5.334102 4.665655 -5.000000 28.907344 -0.592942 -0.329524 -0.000006 0.307676
52 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.136865 21.300298 27.780042 2.020000 37.024281 -0.692364 -0.302672 -0.000013 0.204033
53 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.133538 21.291260 27.780042 2.020000 37.024281 -0.695637 -0.311519 -0.000013 0.200762
54 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.139991 21.319865 27.780042 2.020000 37.024281 -0.689290 -0.283519 -0.000013 0.207107
55 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.145748 21.330075 27.780042 2.020000 37.024281 -0.683629 -0.273524 -0.000013 0.212766
56 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702989 -12.202762 -18.023381 -10.170000 19.875825 -0.686660 -0.289833 -0.000002 0.204507
57 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.692830 -12.240287 -18.023381 -10.170000 19.875825 -0.676377 -0.327811 -0.000002 0.214786
58 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702899 -12.180291 -18.023381 -10.170000 19.875825 -0.686568 -0.267091 -0.000002 0.204598
59 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.709282 -12.282257 -18.023381 -10.170000 19.875825 -0.693029 -0.370287 -0.000002 0.198140
60 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.679330 -12.235994 -18.023381 -10.170000 19.875825 -0.662712 -0.323466 -0.000002 0.228446
61 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.695594 22.238663 28.306614 1.710000 37.450394 -0.262066 -0.203838 -0.000014 0.634200
62 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.663504 22.286354 28.306614 1.710000 37.450394 -0.293620 -0.157194 -0.000014 0.602656
63 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666457 22.254290 28.306614 1.710000 37.450394 -0.290717 -0.188555 -0.000014 0.605558
64 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666910 22.223232 28.306614 1.710000 37.450394 -0.290271 -0.218930 -0.000014 0.606004
65 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.679662 22.257256 28.306614 1.710000 37.450394 -0.277732 -0.185653 -0.000014 0.618539
66 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.676768 22.267680 28.306614 1.710000 37.450394 -0.280578 -0.175459 -0.000014 0.615693
67 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.307663 5.317330 4.665655 -5.000000 28.907344 -0.602808 -0.346202 -0.000006 0.290853
68 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.308562 5.331400 4.665655 -5.000000 28.907344 -0.601911 -0.332212 -0.000006 0.291749
––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– ––––––––
637def table_of_samples( 638 data47 = None, 639 data48 = None, 640 dir = 'output', 641 filename = None, 642 save_to_file = True, 643 print_out = True, 644 output = None, 645 ): 646 ''' 647 Print out, save to disk and/or return a combined table of samples 648 for a pair of `D47data` and `D48data` objects. 649 650 **Parameters** 651 652 + `data47`: `D47data` instance 653 + `data48`: `D48data` instance 654 + `dir`: the directory in which to save the table 655 + `filename`: the name to the csv file to write to 656 + `save_to_file`: whether to save the table to disk 657 + `print_out`: whether to print out the table 658 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 659 if set to `'raw'`: return a list of list of strings 660 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 661 ''' 662 if data47 is None: 663 if data48 is None: 664 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 665 else: 666 return data48.table_of_samples( 667 dir = dir, 668 filename = filename, 669 save_to_file = save_to_file, 670 print_out = print_out, 671 output = output 672 ) 673 else: 674 if data48 is None: 675 return data47.table_of_samples( 676 dir = dir, 677 filename = filename, 678 save_to_file = save_to_file, 679 print_out = print_out, 680 output = output 681 ) 682 else: 683 out47 = data47.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 684 out48 = data48.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 685 out = transpose_table(transpose_table(out47) + transpose_table(out48)[4:]) 686 687 if save_to_file: 688 if not os.path.exists(dir): 689 os.makedirs(dir) 690 if filename is None: 691 filename = f'D47D48_samples.csv' 692 with open(f'{dir}/{filename}', 'w') as fid: 693 fid.write(make_csv(out)) 694 if print_out: 695 print('\n'+pretty_table(out)) 696 if output == 'raw': 697 return out 698 elif output == 'pretty': 699 return pretty_table(out)
Print out, save to disk and/or return a combined table of samples
for a pair of D47data
and D48data
objects.
Parameters
data47
:D47data
instancedata48
:D48data
instancedir
: the directory in which to save the tablefilename
: the name to the csv file to write tosave_to_file
: whether to save the table to diskprint_out
: whether to print out the tableoutput
: if set to'pretty'
: return a pretty text table (seepretty_table()
); if set to'raw'
: return a list of list of strings (e.g.,[['header1', 'header2'], ['0.1', '0.2']]
)
702def table_of_sessions( 703 data47 = None, 704 data48 = None, 705 dir = 'output', 706 filename = None, 707 save_to_file = True, 708 print_out = True, 709 output = None, 710 ): 711 ''' 712 Print out, save to disk and/or return a combined table of sessions 713 for a pair of `D47data` and `D48data` objects. 714 ***Only applicable if the sessions in `data47` and those in `data48` 715 consist of the exact same sets of analyses.*** 716 717 **Parameters** 718 719 + `data47`: `D47data` instance 720 + `data48`: `D48data` instance 721 + `dir`: the directory in which to save the table 722 + `filename`: the name to the csv file to write to 723 + `save_to_file`: whether to save the table to disk 724 + `print_out`: whether to print out the table 725 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 726 if set to `'raw'`: return a list of list of strings 727 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 728 ''' 729 if data47 is None: 730 if data48 is None: 731 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 732 else: 733 return data48.table_of_sessions( 734 dir = dir, 735 filename = filename, 736 save_to_file = save_to_file, 737 print_out = print_out, 738 output = output 739 ) 740 else: 741 if data48 is None: 742 return data47.table_of_sessions( 743 dir = dir, 744 filename = filename, 745 save_to_file = save_to_file, 746 print_out = print_out, 747 output = output 748 ) 749 else: 750 out47 = data47.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 751 out48 = data48.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 752 for k,x in enumerate(out47[0]): 753 if k>7: 754 out47[0][k] = out47[0][k].replace('a', 'a_47').replace('b', 'b_47').replace('c', 'c_47') 755 out48[0][k] = out48[0][k].replace('a', 'a_48').replace('b', 'b_48').replace('c', 'c_48') 756 out = transpose_table(transpose_table(out47) + transpose_table(out48)[7:]) 757 758 if save_to_file: 759 if not os.path.exists(dir): 760 os.makedirs(dir) 761 if filename is None: 762 filename = f'D47D48_sessions.csv' 763 with open(f'{dir}/{filename}', 'w') as fid: 764 fid.write(make_csv(out)) 765 if print_out: 766 print('\n'+pretty_table(out)) 767 if output == 'raw': 768 return out 769 elif output == 'pretty': 770 return pretty_table(out)
Print out, save to disk and/or return a combined table of sessions
for a pair of D47data
and D48data
objects.
Only applicable if the sessions in data47
and those in data48
consist of the exact same sets of analyses.
Parameters
data47
:D47data
instancedata48
:D48data
instancedir
: the directory in which to save the tablefilename
: the name to the csv file to write tosave_to_file
: whether to save the table to diskprint_out
: whether to print out the tableoutput
: if set to'pretty'
: return a pretty text table (seepretty_table()
); if set to'raw'
: return a list of list of strings (e.g.,[['header1', 'header2'], ['0.1', '0.2']]
)
773def table_of_analyses( 774 data47 = None, 775 data48 = None, 776 dir = 'output', 777 filename = None, 778 save_to_file = True, 779 print_out = True, 780 output = None, 781 ): 782 ''' 783 Print out, save to disk and/or return a combined table of analyses 784 for a pair of `D47data` and `D48data` objects. 785 786 If the sessions in `data47` and those in `data48` do not consist of 787 the exact same sets of analyses, the table will have two columns 788 `Session_47` and `Session_48` instead of a single `Session` column. 789 790 **Parameters** 791 792 + `data47`: `D47data` instance 793 + `data48`: `D48data` instance 794 + `dir`: the directory in which to save the table 795 + `filename`: the name to the csv file to write to 796 + `save_to_file`: whether to save the table to disk 797 + `print_out`: whether to print out the table 798 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 799 if set to `'raw'`: return a list of list of strings 800 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 801 ''' 802 if data47 is None: 803 if data48 is None: 804 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 805 else: 806 return data48.table_of_analyses( 807 dir = dir, 808 filename = filename, 809 save_to_file = save_to_file, 810 print_out = print_out, 811 output = output 812 ) 813 else: 814 if data48 is None: 815 return data47.table_of_analyses( 816 dir = dir, 817 filename = filename, 818 save_to_file = save_to_file, 819 print_out = print_out, 820 output = output 821 ) 822 else: 823 out47 = data47.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 824 out48 = data48.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 825 826 if [l[1] for l in out47[1:]] == [l[1] for l in out48[1:]]: # if sessions are identical 827 out = transpose_table(transpose_table(out47) + transpose_table(out48)[-1:]) 828 else: 829 out47[0][1] = 'Session_47' 830 out48[0][1] = 'Session_48' 831 out47 = transpose_table(out47) 832 out48 = transpose_table(out48) 833 out = transpose_table(out47[:2] + out48[1:2] + out47[2:] + out48[-1:]) 834 835 if save_to_file: 836 if not os.path.exists(dir): 837 os.makedirs(dir) 838 if filename is None: 839 filename = f'D47D48_sessions.csv' 840 with open(f'{dir}/{filename}', 'w') as fid: 841 fid.write(make_csv(out)) 842 if print_out: 843 print('\n'+pretty_table(out)) 844 if output == 'raw': 845 return out 846 elif output == 'pretty': 847 return pretty_table(out)
Print out, save to disk and/or return a combined table of analyses
for a pair of D47data
and D48data
objects.
If the sessions in data47
and those in data48
do not consist of
the exact same sets of analyses, the table will have two columns
Session_47
and Session_48
instead of a single Session
column.
Parameters
data47
:D47data
instancedata48
:D48data
instancedir
: the directory in which to save the tablefilename
: the name to the csv file to write tosave_to_file
: whether to save the table to diskprint_out
: whether to print out the tableoutput
: if set to'pretty'
: return a pretty text table (seepretty_table()
); if set to'raw'
: return a list of list of strings (e.g.,[['header1', 'header2'], ['0.1', '0.2']]
)
895class D4xdata(list): 896 ''' 897 Store and process data for a large set of Δ47 and/or Δ48 898 analyses, usually comprising more than one analytical session. 899 ''' 900 901 ### 17O CORRECTION PARAMETERS 902 R13_VPDB = 0.01118 # (Chang & Li, 1990) 903 ''' 904 Absolute (13C/12C) ratio of VPDB. 905 By default equal to 0.01118 ([Chang & Li, 1990](http://www.cnki.com.cn/Article/CJFDTotal-JXTW199004006.htm)) 906 ''' 907 908 R18_VSMOW = 0.0020052 # (Baertschi, 1976) 909 ''' 910 Absolute (18O/16C) ratio of VSMOW. 911 By default equal to 0.0020052 ([Baertschi, 1976](https://doi.org/10.1016/0012-821X(76)90115-1)) 912 ''' 913 914 LAMBDA_17 = 0.528 # (Barkan & Luz, 2005) 915 ''' 916 Mass-dependent exponent for triple oxygen isotopes. 917 By default equal to 0.528 ([Barkan & Luz, 2005](https://doi.org/10.1002/rcm.2250)) 918 ''' 919 920 R17_VSMOW = 0.00038475 # (Assonov & Brenninkmeijer, 2003, rescaled to R13_VPDB) 921 ''' 922 Absolute (17O/16C) ratio of VSMOW. 923 By default equal to 0.00038475 924 ([Assonov & Brenninkmeijer, 2003](https://dx.doi.org/10.1002/rcm.1011), 925 rescaled to `R13_VPDB`) 926 ''' 927 928 R18_VPDB = R18_VSMOW * 1.03092 929 ''' 930 Absolute (18O/16C) ratio of VPDB. 931 By definition equal to `R18_VSMOW * 1.03092`. 932 ''' 933 934 R17_VPDB = R17_VSMOW * 1.03092 ** LAMBDA_17 935 ''' 936 Absolute (17O/16C) ratio of VPDB. 937 By definition equal to `R17_VSMOW * 1.03092 ** LAMBDA_17`. 938 ''' 939 940 LEVENE_REF_SAMPLE = 'ETH-3' 941 ''' 942 After the Δ4x standardization step, each sample is tested to 943 assess whether the Δ4x variance within all analyses for that 944 sample differs significantly from that observed for a given reference 945 sample (using [Levene's test](https://en.wikipedia.org/wiki/Levene%27s_test), 946 which yields a p-value corresponding to the null hypothesis that the 947 underlying variances are equal). 948 949 `LEVENE_REF_SAMPLE` (by default equal to `'ETH-3'`) specifies which 950 sample should be used as a reference for this test. 951 ''' 952 953 ALPHA_18O_ACID_REACTION = round(np.exp(3.59 / (90 + 273.15) - 1.79e-3), 6) # (Kim et al., 2007, calcite) 954 ''' 955 Specifies the 18O/16O fractionation factor generally applicable 956 to acid reactions in the dataset. Currently used by `D4xdata.wg()`, 957 `D4xdata.standardize_d13C`, and `D4xdata.standardize_d18O`. 958 959 By default equal to 1.008129 (calcite reacted at 90 °C, 960 [Kim et al., 2007](https://dx.doi.org/10.1016/j.chemgeo.2007.08.005)). 961 ''' 962 963 Nominal_d13C_VPDB = { 964 'ETH-1': 2.02, 965 'ETH-2': -10.17, 966 'ETH-3': 1.71, 967 } # (Bernasconi et al., 2018) 968 ''' 969 Nominal δ13C_VPDB values assigned to carbonate standards, used by 970 `D4xdata.standardize_d13C()`. 971 972 By default equal to `{'ETH-1': 2.02, 'ETH-2': -10.17, 'ETH-3': 1.71}` after 973 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 974 ''' 975 976 Nominal_d18O_VPDB = { 977 'ETH-1': -2.19, 978 'ETH-2': -18.69, 979 'ETH-3': -1.78, 980 } # (Bernasconi et al., 2018) 981 ''' 982 Nominal δ18O_VPDB values assigned to carbonate standards, used by 983 `D4xdata.standardize_d18O()`. 984 985 By default equal to `{'ETH-1': -2.19, 'ETH-2': -18.69, 'ETH-3': -1.78}` after 986 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 987 ''' 988 989 d13C_STANDARDIZATION_METHOD = '2pt' 990 ''' 991 Method by which to standardize δ13C values: 992 993 + `none`: do not apply any δ13C standardization. 994 + `'1pt'`: within each session, offset all initial δ13C values so as to 995 minimize the difference between final δ13C_VPDB values and 996 `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` is defined). 997 + `'2pt'`: within each session, apply a affine trasformation to all δ13C 998 values so as to minimize the difference between final δ13C_VPDB 999 values and `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` 1000 is defined). 1001 ''' 1002 1003 d18O_STANDARDIZATION_METHOD = '2pt' 1004 ''' 1005 Method by which to standardize δ18O values: 1006 1007 + `none`: do not apply any δ18O standardization. 1008 + `'1pt'`: within each session, offset all initial δ18O values so as to 1009 minimize the difference between final δ18O_VPDB values and 1010 `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` is defined). 1011 + `'2pt'`: within each session, apply a affine trasformation to all δ18O 1012 values so as to minimize the difference between final δ18O_VPDB 1013 values and `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` 1014 is defined). 1015 ''' 1016 1017 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 1018 ''' 1019 **Parameters** 1020 1021 + `l`: a list of dictionaries, with each dictionary including at least the keys 1022 `Sample`, `d45`, `d46`, and `d47` or `d48`. 1023 + `mass`: `'47'` or `'48'` 1024 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 1025 + `session`: define session name for analyses without a `Session` key 1026 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 1027 1028 Returns a `D4xdata` object derived from `list`. 1029 ''' 1030 self._4x = mass 1031 self.verbose = verbose 1032 self.prefix = 'D4xdata' 1033 self.logfile = logfile 1034 list.__init__(self, l) 1035 self.Nf = None 1036 self.repeatability = {} 1037 self.refresh(session = session) 1038 1039 1040 def make_verbal(oldfun): 1041 ''' 1042 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 1043 ''' 1044 @wraps(oldfun) 1045 def newfun(*args, verbose = '', **kwargs): 1046 myself = args[0] 1047 oldprefix = myself.prefix 1048 myself.prefix = oldfun.__name__ 1049 if verbose != '': 1050 oldverbose = myself.verbose 1051 myself.verbose = verbose 1052 out = oldfun(*args, **kwargs) 1053 myself.prefix = oldprefix 1054 if verbose != '': 1055 myself.verbose = oldverbose 1056 return out 1057 return newfun 1058 1059 1060 def msg(self, txt): 1061 ''' 1062 Log a message to `self.logfile`, and print it out if `verbose = True` 1063 ''' 1064 self.log(txt) 1065 if self.verbose: 1066 print(f'{f"[{self.prefix}]":<16} {txt}') 1067 1068 1069 def vmsg(self, txt): 1070 ''' 1071 Log a message to `self.logfile` and print it out 1072 ''' 1073 self.log(txt) 1074 print(txt) 1075 1076 1077 def log(self, *txts): 1078 ''' 1079 Log a message to `self.logfile` 1080 ''' 1081 if self.logfile: 1082 with open(self.logfile, 'a') as fid: 1083 for txt in txts: 1084 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}') 1085 1086 1087 def refresh(self, session = 'mySession'): 1088 ''' 1089 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1090 ''' 1091 self.fill_in_missing_info(session = session) 1092 self.refresh_sessions() 1093 self.refresh_samples() 1094 1095 1096 def refresh_sessions(self): 1097 ''' 1098 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1099 to `False` for all sessions. 1100 ''' 1101 self.sessions = { 1102 s: {'data': [r for r in self if r['Session'] == s]} 1103 for s in sorted({r['Session'] for r in self}) 1104 } 1105 for s in self.sessions: 1106 self.sessions[s]['scrambling_drift'] = False 1107 self.sessions[s]['slope_drift'] = False 1108 self.sessions[s]['wg_drift'] = False 1109 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1110 self.sessions[s]['d18O_standardization_method'] = self.d18O_STANDARDIZATION_METHOD 1111 1112 1113 def refresh_samples(self): 1114 ''' 1115 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1116 ''' 1117 self.samples = { 1118 s: {'data': [r for r in self if r['Sample'] == s]} 1119 for s in sorted({r['Sample'] for r in self}) 1120 } 1121 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1122 self.unknowns = {s: self.samples[s] for s in self.samples if s not in self.Nominal_D4x} 1123 1124 1125 def read(self, filename, sep = '', session = ''): 1126 ''' 1127 Read file in csv format to load data into a `D47data` object. 1128 1129 In the csv file, spaces before and after field separators (`','` by default) 1130 are optional. Each line corresponds to a single analysis. 1131 1132 The required fields are: 1133 1134 + `UID`: a unique identifier 1135 + `Session`: an identifier for the analytical session 1136 + `Sample`: a sample identifier 1137 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1138 1139 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1140 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1141 and `d49` are optional, and set to NaN by default. 1142 1143 **Parameters** 1144 1145 + `fileneme`: the path of the file to read 1146 + `sep`: csv separator delimiting the fields 1147 + `session`: set `Session` field to this string for all analyses 1148 ''' 1149 with open(filename) as fid: 1150 self.input(fid.read(), sep = sep, session = session) 1151 1152 1153 def input(self, txt, sep = '', session = ''): 1154 ''' 1155 Read `txt` string in csv format to load analysis data into a `D47data` object. 1156 1157 In the csv string, spaces before and after field separators (`','` by default) 1158 are optional. Each line corresponds to a single analysis. 1159 1160 The required fields are: 1161 1162 + `UID`: a unique identifier 1163 + `Session`: an identifier for the analytical session 1164 + `Sample`: a sample identifier 1165 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1166 1167 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1168 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1169 and `d49` are optional, and set to NaN by default. 1170 1171 **Parameters** 1172 1173 + `txt`: the csv string to read 1174 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1175 whichever appers most often in `txt`. 1176 + `session`: set `Session` field to this string for all analyses 1177 ''' 1178 if sep == '': 1179 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1180 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1181 data = [{k: v if k in ['UID', 'Session', 'Sample'] else smart_type(v) for k,v in zip(txt[0], l) if v != ''} for l in txt[1:]] 1182 1183 if session != '': 1184 for r in data: 1185 r['Session'] = session 1186 1187 self += data 1188 self.refresh() 1189 1190 1191 @make_verbal 1192 def wg(self, samples = None, a18_acid = None): 1193 ''' 1194 Compute bulk composition of the working gas for each session based on 1195 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1196 `self.Nominal_d18O_VPDB`. 1197 ''' 1198 1199 self.msg('Computing WG composition:') 1200 1201 if a18_acid is None: 1202 a18_acid = self.ALPHA_18O_ACID_REACTION 1203 if samples is None: 1204 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1205 1206 assert a18_acid, f'Acid fractionation factor should not be zero.' 1207 1208 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1209 R45R46_standards = {} 1210 for sample in samples: 1211 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1212 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1213 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1214 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1215 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1216 1217 C12_s = 1 / (1 + R13_s) 1218 C13_s = R13_s / (1 + R13_s) 1219 C16_s = 1 / (1 + R17_s + R18_s) 1220 C17_s = R17_s / (1 + R17_s + R18_s) 1221 C18_s = R18_s / (1 + R17_s + R18_s) 1222 1223 C626_s = C12_s * C16_s ** 2 1224 C627_s = 2 * C12_s * C16_s * C17_s 1225 C628_s = 2 * C12_s * C16_s * C18_s 1226 C636_s = C13_s * C16_s ** 2 1227 C637_s = 2 * C13_s * C16_s * C17_s 1228 C727_s = C12_s * C17_s ** 2 1229 1230 R45_s = (C627_s + C636_s) / C626_s 1231 R46_s = (C628_s + C637_s + C727_s) / C626_s 1232 R45R46_standards[sample] = (R45_s, R46_s) 1233 1234 for s in self.sessions: 1235 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1236 assert db, f'No sample from {samples} found in session "{s}".' 1237# dbsamples = sorted({r['Sample'] for r in db}) 1238 1239 X = [r['d45'] for r in db] 1240 Y = [R45R46_standards[r['Sample']][0] for r in db] 1241 x1, x2 = np.min(X), np.max(X) 1242 1243 if x1 < x2: 1244 wgcoord = x1/(x1-x2) 1245 else: 1246 wgcoord = 999 1247 1248 if wgcoord < -.5 or wgcoord > 1.5: 1249 # unreasonable to extrapolate to d45 = 0 1250 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1251 else : 1252 # d45 = 0 is reasonably well bracketed 1253 R45_wg = np.polyfit(X, Y, 1)[1] 1254 1255 X = [r['d46'] for r in db] 1256 Y = [R45R46_standards[r['Sample']][1] for r in db] 1257 x1, x2 = np.min(X), np.max(X) 1258 1259 if x1 < x2: 1260 wgcoord = x1/(x1-x2) 1261 else: 1262 wgcoord = 999 1263 1264 if wgcoord < -.5 or wgcoord > 1.5: 1265 # unreasonable to extrapolate to d46 = 0 1266 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1267 else : 1268 # d46 = 0 is reasonably well bracketed 1269 R46_wg = np.polyfit(X, Y, 1)[1] 1270 1271 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1272 1273 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1274 1275 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1276 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1277 for r in self.sessions[s]['data']: 1278 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1279 r['d18Owg_VSMOW'] = d18Owg_VSMOW 1280 1281 1282 def compute_bulk_delta(self, R45, R46, D17O = 0): 1283 ''' 1284 Compute δ13C_VPDB and δ18O_VSMOW, 1285 by solving the generalized form of equation (17) from 1286 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1287 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1288 solving the corresponding second-order Taylor polynomial. 1289 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1290 ''' 1291 1292 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1293 1294 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1295 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1296 C = 2 * self.R18_VSMOW 1297 D = -R46 1298 1299 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1300 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1301 cc = A + B + C + D 1302 1303 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1304 1305 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1306 R17 = K * R18 ** self.LAMBDA_17 1307 R13 = R45 - 2 * R17 1308 1309 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1310 1311 return d13C_VPDB, d18O_VSMOW 1312 1313 1314 @make_verbal 1315 def crunch(self, verbose = ''): 1316 ''' 1317 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1318 ''' 1319 for r in self: 1320 self.compute_bulk_and_clumping_deltas(r) 1321 self.standardize_d13C() 1322 self.standardize_d18O() 1323 self.msg(f"Crunched {len(self)} analyses.") 1324 1325 1326 def fill_in_missing_info(self, session = 'mySession'): 1327 ''' 1328 Fill in optional fields with default values 1329 ''' 1330 for i,r in enumerate(self): 1331 if 'D17O' not in r: 1332 r['D17O'] = 0. 1333 if 'UID' not in r: 1334 r['UID'] = f'{i+1}' 1335 if 'Session' not in r: 1336 r['Session'] = session 1337 for k in ['d47', 'd48', 'd49']: 1338 if k not in r: 1339 r[k] = np.nan 1340 1341 1342 def standardize_d13C(self): 1343 ''' 1344 Perform δ13C standadization within each session `s` according to 1345 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1346 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1347 may be redefined abitrarily at a later stage. 1348 ''' 1349 for s in self.sessions: 1350 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1351 XY = [(r['d13C_VPDB'], self.Nominal_d13C_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d13C_VPDB] 1352 X,Y = zip(*XY) 1353 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1354 offset = np.mean(Y) - np.mean(X) 1355 for r in self.sessions[s]['data']: 1356 r['d13C_VPDB'] += offset 1357 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1358 a,b = np.polyfit(X,Y,1) 1359 for r in self.sessions[s]['data']: 1360 r['d13C_VPDB'] = a * r['d13C_VPDB'] + b 1361 1362 def standardize_d18O(self): 1363 ''' 1364 Perform δ18O standadization within each session `s` according to 1365 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1366 which is defined by default by `D47data.refresh_sessions()`as equal to 1367 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1368 ''' 1369 for s in self.sessions: 1370 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1371 XY = [(r['d18O_VSMOW'], self.Nominal_d18O_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d18O_VPDB] 1372 X,Y = zip(*XY) 1373 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1374 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1375 offset = np.mean(Y) - np.mean(X) 1376 for r in self.sessions[s]['data']: 1377 r['d18O_VSMOW'] += offset 1378 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1379 a,b = np.polyfit(X,Y,1) 1380 for r in self.sessions[s]['data']: 1381 r['d18O_VSMOW'] = a * r['d18O_VSMOW'] + b 1382 1383 1384 def compute_bulk_and_clumping_deltas(self, r): 1385 ''' 1386 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1387 ''' 1388 1389 # Compute working gas R13, R18, and isobar ratios 1390 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1391 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1392 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1393 1394 # Compute analyte isobar ratios 1395 R45 = (1 + r['d45'] / 1000) * R45_wg 1396 R46 = (1 + r['d46'] / 1000) * R46_wg 1397 R47 = (1 + r['d47'] / 1000) * R47_wg 1398 R48 = (1 + r['d48'] / 1000) * R48_wg 1399 R49 = (1 + r['d49'] / 1000) * R49_wg 1400 1401 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1402 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1403 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1404 1405 # Compute stochastic isobar ratios of the analyte 1406 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1407 R13, R18, D17O = r['D17O'] 1408 ) 1409 1410 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1411 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1412 if (R45 / R45stoch - 1) > 5e-8: 1413 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1414 if (R46 / R46stoch - 1) > 5e-8: 1415 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1416 1417 # Compute raw clumped isotope anomalies 1418 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1419 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1420 r['D49raw'] = 1000 * (R49 / R49stoch - 1) 1421 1422 1423 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1424 ''' 1425 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1426 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1427 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1428 ''' 1429 1430 # Compute R17 1431 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1432 1433 # Compute isotope concentrations 1434 C12 = (1 + R13) ** -1 1435 C13 = C12 * R13 1436 C16 = (1 + R17 + R18) ** -1 1437 C17 = C16 * R17 1438 C18 = C16 * R18 1439 1440 # Compute stochastic isotopologue concentrations 1441 C626 = C16 * C12 * C16 1442 C627 = C16 * C12 * C17 * 2 1443 C628 = C16 * C12 * C18 * 2 1444 C636 = C16 * C13 * C16 1445 C637 = C16 * C13 * C17 * 2 1446 C638 = C16 * C13 * C18 * 2 1447 C727 = C17 * C12 * C17 1448 C728 = C17 * C12 * C18 * 2 1449 C737 = C17 * C13 * C17 1450 C738 = C17 * C13 * C18 * 2 1451 C828 = C18 * C12 * C18 1452 C838 = C18 * C13 * C18 1453 1454 # Compute stochastic isobar ratios 1455 R45 = (C636 + C627) / C626 1456 R46 = (C628 + C637 + C727) / C626 1457 R47 = (C638 + C728 + C737) / C626 1458 R48 = (C738 + C828) / C626 1459 R49 = C838 / C626 1460 1461 # Account for stochastic anomalies 1462 R47 *= 1 + D47 / 1000 1463 R48 *= 1 + D48 / 1000 1464 R49 *= 1 + D49 / 1000 1465 1466 # Return isobar ratios 1467 return R45, R46, R47, R48, R49 1468 1469 1470 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1471 ''' 1472 Split unknown samples by UID (treat all analyses as different samples) 1473 or by session (treat analyses of a given sample in different sessions as 1474 different samples). 1475 1476 **Parameters** 1477 1478 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1479 + `grouping`: `by_uid` | `by_session` 1480 ''' 1481 if samples_to_split == 'all': 1482 samples_to_split = [s for s in self.unknowns] 1483 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1484 self.grouping = grouping.lower() 1485 if self.grouping in gkeys: 1486 gkey = gkeys[self.grouping] 1487 for r in self: 1488 if r['Sample'] in samples_to_split: 1489 r['Sample_original'] = r['Sample'] 1490 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1491 elif r['Sample'] in self.unknowns: 1492 r['Sample_original'] = r['Sample'] 1493 self.refresh_samples() 1494 1495 1496 def unsplit_samples(self, tables = False): 1497 ''' 1498 Reverse the effects of `D47data.split_samples()`. 1499 1500 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1501 1502 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1503 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1504 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1505 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1506 that case session-averaged Δ4x values are statistically independent). 1507 ''' 1508 unknowns_old = sorted({s for s in self.unknowns}) 1509 CM_old = self.standardization.covar[:,:] 1510 VD_old = self.standardization.params.valuesdict().copy() 1511 vars_old = self.standardization.var_names 1512 1513 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1514 1515 Ns = len(vars_old) - len(unknowns_old) 1516 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1517 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1518 1519 W = np.zeros((len(vars_new), len(vars_old))) 1520 W[:Ns,:Ns] = np.eye(Ns) 1521 for u in unknowns_new: 1522 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1523 if self.grouping == 'by_session': 1524 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1525 elif self.grouping == 'by_uid': 1526 weights = [1 for s in splits] 1527 sw = sum(weights) 1528 weights = [w/sw for w in weights] 1529 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1530 1531 CM_new = W @ CM_old @ W.T 1532 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1533 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1534 1535 self.standardization.covar = CM_new 1536 self.standardization.params.valuesdict = lambda : VD_new 1537 self.standardization.var_names = vars_new 1538 1539 for r in self: 1540 if r['Sample'] in self.unknowns: 1541 r['Sample_split'] = r['Sample'] 1542 r['Sample'] = r['Sample_original'] 1543 1544 self.refresh_samples() 1545 self.consolidate_samples() 1546 self.repeatabilities() 1547 1548 if tables: 1549 self.table_of_analyses() 1550 self.table_of_samples() 1551 1552 def assign_timestamps(self): 1553 ''' 1554 Assign a time field `t` of type `float` to each analysis. 1555 1556 If `TimeTag` is one of the data fields, `t` is equal within a given session 1557 to `TimeTag` minus the mean value of `TimeTag` for that session. 1558 Otherwise, `TimeTag` is by default equal to the index of each analysis 1559 in the dataset and `t` is defined as above. 1560 ''' 1561 for session in self.sessions: 1562 sdata = self.sessions[session]['data'] 1563 try: 1564 t0 = np.mean([r['TimeTag'] for r in sdata]) 1565 for r in sdata: 1566 r['t'] = r['TimeTag'] - t0 1567 except KeyError: 1568 t0 = (len(sdata)-1)/2 1569 for t,r in enumerate(sdata): 1570 r['t'] = t - t0 1571 1572 1573 def report(self): 1574 ''' 1575 Prints a report on the standardization fit. 1576 Only applicable after `D4xdata.standardize(method='pooled')`. 1577 ''' 1578 report_fit(self.standardization) 1579 1580 1581 def combine_samples(self, sample_groups): 1582 ''' 1583 Combine analyses of different samples to compute weighted average Δ4x 1584 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1585 dictionary. 1586 1587 Caution: samples are weighted by number of replicate analyses, which is a 1588 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1589 correlated analytical errors for one or more samples). 1590 1591 Returns a tuplet of: 1592 1593 + the list of group names 1594 + an array of the corresponding Δ4x values 1595 + the corresponding (co)variance matrix 1596 1597 **Parameters** 1598 1599 + `sample_groups`: a dictionary of the form: 1600 ```py 1601 {'group1': ['sample_1', 'sample_2'], 1602 'group2': ['sample_3', 'sample_4', 'sample_5']} 1603 ``` 1604 ''' 1605 1606 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1607 groups = sorted(sample_groups.keys()) 1608 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1609 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1610 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1611 W = np.array([ 1612 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1613 for j in groups]) 1614 D4x_new = W @ D4x_old 1615 CM_new = W @ CM_old @ W.T 1616 1617 return groups, D4x_new[:,0], CM_new 1618 1619 1620 @make_verbal 1621 def standardize(self, 1622 method = 'pooled', 1623 weighted_sessions = [], 1624 consolidate = True, 1625 consolidate_tables = False, 1626 consolidate_plots = False, 1627 constraints = {}, 1628 ): 1629 ''' 1630 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1631 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1632 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1633 i.e. that their true Δ4x value does not change between sessions, 1634 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1635 `'indep_sessions'`, the standardization processes each session independently, based only 1636 on anchors analyses. 1637 ''' 1638 1639 self.standardization_method = method 1640 self.assign_timestamps() 1641 1642 if method == 'pooled': 1643 if weighted_sessions: 1644 for session_group in weighted_sessions: 1645 if self._4x == '47': 1646 X = D47data([r for r in self if r['Session'] in session_group]) 1647 elif self._4x == '48': 1648 X = D48data([r for r in self if r['Session'] in session_group]) 1649 X.Nominal_D4x = self.Nominal_D4x.copy() 1650 X.refresh() 1651 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1652 w = np.sqrt(result.redchi) 1653 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1654 for r in X: 1655 r[f'wD{self._4x}raw'] *= w 1656 else: 1657 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1658 for r in self: 1659 r[f'wD{self._4x}raw'] = 1. 1660 1661 params = Parameters() 1662 for k,session in enumerate(self.sessions): 1663 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1664 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1665 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1666 s = pf(session) 1667 params.add(f'a_{s}', value = 0.9) 1668 params.add(f'b_{s}', value = 0.) 1669 params.add(f'c_{s}', value = -0.9) 1670 params.add(f'a2_{s}', value = 0., 1671# vary = self.sessions[session]['scrambling_drift'], 1672 ) 1673 params.add(f'b2_{s}', value = 0., 1674# vary = self.sessions[session]['slope_drift'], 1675 ) 1676 params.add(f'c2_{s}', value = 0., 1677# vary = self.sessions[session]['wg_drift'], 1678 ) 1679 if not self.sessions[session]['scrambling_drift']: 1680 params[f'a2_{s}'].expr = '0' 1681 if not self.sessions[session]['slope_drift']: 1682 params[f'b2_{s}'].expr = '0' 1683 if not self.sessions[session]['wg_drift']: 1684 params[f'c2_{s}'].expr = '0' 1685 1686 for sample in self.unknowns: 1687 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1688 1689 for k in constraints: 1690 params[k].expr = constraints[k] 1691 1692 def residuals(p): 1693 R = [] 1694 for r in self: 1695 session = pf(r['Session']) 1696 sample = pf(r['Sample']) 1697 if r['Sample'] in self.Nominal_D4x: 1698 R += [ ( 1699 r[f'D{self._4x}raw'] - ( 1700 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1701 + p[f'b_{session}'] * r[f'd{self._4x}'] 1702 + p[f'c_{session}'] 1703 + r['t'] * ( 1704 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1705 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1706 + p[f'c2_{session}'] 1707 ) 1708 ) 1709 ) / r[f'wD{self._4x}raw'] ] 1710 else: 1711 R += [ ( 1712 r[f'D{self._4x}raw'] - ( 1713 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1714 + p[f'b_{session}'] * r[f'd{self._4x}'] 1715 + p[f'c_{session}'] 1716 + r['t'] * ( 1717 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1718 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1719 + p[f'c2_{session}'] 1720 ) 1721 ) 1722 ) / r[f'wD{self._4x}raw'] ] 1723 return R 1724 1725 M = Minimizer(residuals, params) 1726 result = M.least_squares() 1727 self.Nf = result.nfree 1728 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1729 new_names, new_covar, new_se = _fullcovar(result)[:3] 1730 result.var_names = new_names 1731 result.covar = new_covar 1732 1733 for r in self: 1734 s = pf(r["Session"]) 1735 a = result.params.valuesdict()[f'a_{s}'] 1736 b = result.params.valuesdict()[f'b_{s}'] 1737 c = result.params.valuesdict()[f'c_{s}'] 1738 a2 = result.params.valuesdict()[f'a2_{s}'] 1739 b2 = result.params.valuesdict()[f'b2_{s}'] 1740 c2 = result.params.valuesdict()[f'c2_{s}'] 1741 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1742 1743 self.standardization = result 1744 1745 for session in self.sessions: 1746 self.sessions[session]['Np'] = 3 1747 for k in ['scrambling', 'slope', 'wg']: 1748 if self.sessions[session][f'{k}_drift']: 1749 self.sessions[session]['Np'] += 1 1750 1751 if consolidate: 1752 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1753 return result 1754 1755 1756 elif method == 'indep_sessions': 1757 1758 if weighted_sessions: 1759 for session_group in weighted_sessions: 1760 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1761 X.Nominal_D4x = self.Nominal_D4x.copy() 1762 X.refresh() 1763 # This is only done to assign r['wD47raw'] for r in X: 1764 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1765 self.msg(f'D{self._4x}raw weights set to {1000*X[0][f"wD{self._4x}raw"]:.1f} ppm for sessions in {session_group}') 1766 else: 1767 self.msg('All weights set to 1 ‰') 1768 for r in self: 1769 r[f'wD{self._4x}raw'] = 1 1770 1771 for session in self.sessions: 1772 s = self.sessions[session] 1773 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1774 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1775 s['Np'] = sum(p_active) 1776 sdata = s['data'] 1777 1778 A = np.array([ 1779 [ 1780 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1781 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1782 1 / r[f'wD{self._4x}raw'], 1783 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1784 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1785 r['t'] / r[f'wD{self._4x}raw'] 1786 ] 1787 for r in sdata if r['Sample'] in self.anchors 1788 ])[:,p_active] # only keep columns for the active parameters 1789 Y = np.array([[r[f'D{self._4x}raw'] / r[f'wD{self._4x}raw']] for r in sdata if r['Sample'] in self.anchors]) 1790 s['Na'] = Y.size 1791 CM = linalg.inv(A.T @ A) 1792 bf = (CM @ A.T @ Y).T[0,:] 1793 k = 0 1794 for n,a in zip(p_names, p_active): 1795 if a: 1796 s[n] = bf[k] 1797# self.msg(f'{n} = {bf[k]}') 1798 k += 1 1799 else: 1800 s[n] = 0. 1801# self.msg(f'{n} = 0.0') 1802 1803 for r in sdata : 1804 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1805 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1806 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1807 1808 s['CM'] = np.zeros((6,6)) 1809 i = 0 1810 k_active = [j for j,a in enumerate(p_active) if a] 1811 for j,a in enumerate(p_active): 1812 if a: 1813 s['CM'][j,k_active] = CM[i,:] 1814 i += 1 1815 1816 if not weighted_sessions: 1817 w = self.rmswd()['rmswd'] 1818 for r in self: 1819 r[f'wD{self._4x}'] *= w 1820 r[f'wD{self._4x}raw'] *= w 1821 for session in self.sessions: 1822 self.sessions[session]['CM'] *= w**2 1823 1824 for session in self.sessions: 1825 s = self.sessions[session] 1826 s['SE_a'] = s['CM'][0,0]**.5 1827 s['SE_b'] = s['CM'][1,1]**.5 1828 s['SE_c'] = s['CM'][2,2]**.5 1829 s['SE_a2'] = s['CM'][3,3]**.5 1830 s['SE_b2'] = s['CM'][4,4]**.5 1831 s['SE_c2'] = s['CM'][5,5]**.5 1832 1833 if not weighted_sessions: 1834 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1835 else: 1836 self.Nf = 0 1837 for sg in weighted_sessions: 1838 self.Nf += self.rmswd(sessions = sg)['Nf'] 1839 1840 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1841 1842 avgD4x = { 1843 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1844 for sample in self.samples 1845 } 1846 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1847 rD4x = (chi2/self.Nf)**.5 1848 self.repeatability[f'sigma_{self._4x}'] = rD4x 1849 1850 if consolidate: 1851 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1852 1853 1854 def standardization_error(self, session, d4x, D4x, t = 0): 1855 ''' 1856 Compute standardization error for a given session and 1857 (δ47, Δ47) composition. 1858 ''' 1859 a = self.sessions[session]['a'] 1860 b = self.sessions[session]['b'] 1861 c = self.sessions[session]['c'] 1862 a2 = self.sessions[session]['a2'] 1863 b2 = self.sessions[session]['b2'] 1864 c2 = self.sessions[session]['c2'] 1865 CM = self.sessions[session]['CM'] 1866 1867 x, y = D4x, d4x 1868 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1869# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1870 dxdy = -(b+b2*t) / (a+a2*t) 1871 dxdz = 1. / (a+a2*t) 1872 dxda = -x / (a+a2*t) 1873 dxdb = -y / (a+a2*t) 1874 dxdc = -1. / (a+a2*t) 1875 dxda2 = -x * a2 / (a+a2*t) 1876 dxdb2 = -y * t / (a+a2*t) 1877 dxdc2 = -t / (a+a2*t) 1878 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1879 sx = (V @ CM @ V.T) ** .5 1880 return sx 1881 1882 1883 @make_verbal 1884 def summary(self, 1885 dir = 'output', 1886 filename = None, 1887 save_to_file = True, 1888 print_out = True, 1889 ): 1890 ''' 1891 Print out an/or save to disk a summary of the standardization results. 1892 1893 **Parameters** 1894 1895 + `dir`: the directory in which to save the table 1896 + `filename`: the name to the csv file to write to 1897 + `save_to_file`: whether to save the table to disk 1898 + `print_out`: whether to print out the table 1899 ''' 1900 1901 out = [] 1902 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1903 out += [['N analyses (anchors + unknowns)', f"{len(self)} ({len([r for r in self if r['Sample'] in self.anchors])} + {len([r for r in self if r['Sample'] in self.unknowns])})"]] 1904 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1905 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1906 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1907 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1908 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1909 out += [['Model degrees of freedom', f"{self.Nf}"]] 1910 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1911 out += [['Standardization method', self.standardization_method]] 1912 1913 if save_to_file: 1914 if not os.path.exists(dir): 1915 os.makedirs(dir) 1916 if filename is None: 1917 filename = f'D{self._4x}_summary.csv' 1918 with open(f'{dir}/{filename}', 'w') as fid: 1919 fid.write(make_csv(out)) 1920 if print_out: 1921 self.msg('\n' + pretty_table(out, header = 0)) 1922 1923 1924 @make_verbal 1925 def table_of_sessions(self, 1926 dir = 'output', 1927 filename = None, 1928 save_to_file = True, 1929 print_out = True, 1930 output = None, 1931 ): 1932 ''' 1933 Print out an/or save to disk a table of sessions. 1934 1935 **Parameters** 1936 1937 + `dir`: the directory in which to save the table 1938 + `filename`: the name to the csv file to write to 1939 + `save_to_file`: whether to save the table to disk 1940 + `print_out`: whether to print out the table 1941 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1942 if set to `'raw'`: return a list of list of strings 1943 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1944 ''' 1945 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1946 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1947 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1948 1949 out = [['Session','Na','Nu','d13Cwg_VPDB','d18Owg_VSMOW','r_d13C','r_d18O',f'r_D{self._4x}','a ± SE','1e3 x b ± SE','c ± SE']] 1950 if include_a2: 1951 out[-1] += ['a2 ± SE'] 1952 if include_b2: 1953 out[-1] += ['b2 ± SE'] 1954 if include_c2: 1955 out[-1] += ['c2 ± SE'] 1956 for session in self.sessions: 1957 out += [[ 1958 session, 1959 f"{self.sessions[session]['Na']}", 1960 f"{self.sessions[session]['Nu']}", 1961 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1962 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1963 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1964 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1965 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1966 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1967 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1968 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1969 ]] 1970 if include_a2: 1971 if self.sessions[session]['scrambling_drift']: 1972 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1973 else: 1974 out[-1] += [''] 1975 if include_b2: 1976 if self.sessions[session]['slope_drift']: 1977 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1978 else: 1979 out[-1] += [''] 1980 if include_c2: 1981 if self.sessions[session]['wg_drift']: 1982 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1983 else: 1984 out[-1] += [''] 1985 1986 if save_to_file: 1987 if not os.path.exists(dir): 1988 os.makedirs(dir) 1989 if filename is None: 1990 filename = f'D{self._4x}_sessions.csv' 1991 with open(f'{dir}/{filename}', 'w') as fid: 1992 fid.write(make_csv(out)) 1993 if print_out: 1994 self.msg('\n' + pretty_table(out)) 1995 if output == 'raw': 1996 return out 1997 elif output == 'pretty': 1998 return pretty_table(out) 1999 2000 2001 @make_verbal 2002 def table_of_analyses( 2003 self, 2004 dir = 'output', 2005 filename = None, 2006 save_to_file = True, 2007 print_out = True, 2008 output = None, 2009 ): 2010 ''' 2011 Print out an/or save to disk a table of analyses. 2012 2013 **Parameters** 2014 2015 + `dir`: the directory in which to save the table 2016 + `filename`: the name to the csv file to write to 2017 + `save_to_file`: whether to save the table to disk 2018 + `print_out`: whether to print out the table 2019 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2020 if set to `'raw'`: return a list of list of strings 2021 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2022 ''' 2023 2024 out = [['UID','Session','Sample']] 2025 extra_fields = [f for f in [('SampleMass','.2f'),('ColdFingerPressure','.1f'),('AcidReactionYield','.3f')] if f[0] in {k for r in self for k in r}] 2026 for f in extra_fields: 2027 out[-1] += [f[0]] 2028 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 2029 for r in self: 2030 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 2031 for f in extra_fields: 2032 out[-1] += [f"{r[f[0]]:{f[1]}}"] 2033 out[-1] += [ 2034 f"{r['d13Cwg_VPDB']:.3f}", 2035 f"{r['d18Owg_VSMOW']:.3f}", 2036 f"{r['d45']:.6f}", 2037 f"{r['d46']:.6f}", 2038 f"{r['d47']:.6f}", 2039 f"{r['d48']:.6f}", 2040 f"{r['d49']:.6f}", 2041 f"{r['d13C_VPDB']:.6f}", 2042 f"{r['d18O_VSMOW']:.6f}", 2043 f"{r['D47raw']:.6f}", 2044 f"{r['D48raw']:.6f}", 2045 f"{r['D49raw']:.6f}", 2046 f"{r[f'D{self._4x}']:.6f}" 2047 ] 2048 if save_to_file: 2049 if not os.path.exists(dir): 2050 os.makedirs(dir) 2051 if filename is None: 2052 filename = f'D{self._4x}_analyses.csv' 2053 with open(f'{dir}/{filename}', 'w') as fid: 2054 fid.write(make_csv(out)) 2055 if print_out: 2056 self.msg('\n' + pretty_table(out)) 2057 return out 2058 2059 @make_verbal 2060 def covar_table( 2061 self, 2062 correl = False, 2063 dir = 'output', 2064 filename = None, 2065 save_to_file = True, 2066 print_out = True, 2067 output = None, 2068 ): 2069 ''' 2070 Print out, save to disk and/or return the variance-covariance matrix of D4x 2071 for all unknown samples. 2072 2073 **Parameters** 2074 2075 + `dir`: the directory in which to save the csv 2076 + `filename`: the name of the csv file to write to 2077 + `save_to_file`: whether to save the csv 2078 + `print_out`: whether to print out the matrix 2079 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2080 if set to `'raw'`: return a list of list of strings 2081 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2082 ''' 2083 samples = sorted([u for u in self.unknowns]) 2084 out = [[''] + samples] 2085 for s1 in samples: 2086 out.append([s1]) 2087 for s2 in samples: 2088 if correl: 2089 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2090 else: 2091 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2092 2093 if save_to_file: 2094 if not os.path.exists(dir): 2095 os.makedirs(dir) 2096 if filename is None: 2097 if correl: 2098 filename = f'D{self._4x}_correl.csv' 2099 else: 2100 filename = f'D{self._4x}_covar.csv' 2101 with open(f'{dir}/{filename}', 'w') as fid: 2102 fid.write(make_csv(out)) 2103 if print_out: 2104 self.msg('\n'+pretty_table(out)) 2105 if output == 'raw': 2106 return out 2107 elif output == 'pretty': 2108 return pretty_table(out) 2109 2110 @make_verbal 2111 def table_of_samples( 2112 self, 2113 dir = 'output', 2114 filename = None, 2115 save_to_file = True, 2116 print_out = True, 2117 output = None, 2118 ): 2119 ''' 2120 Print out, save to disk and/or return a table of samples. 2121 2122 **Parameters** 2123 2124 + `dir`: the directory in which to save the csv 2125 + `filename`: the name of the csv file to write to 2126 + `save_to_file`: whether to save the csv 2127 + `print_out`: whether to print out the table 2128 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2129 if set to `'raw'`: return a list of list of strings 2130 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2131 ''' 2132 2133 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2134 for sample in self.anchors: 2135 out += [[ 2136 f"{sample}", 2137 f"{self.samples[sample]['N']}", 2138 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2139 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2140 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2141 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2142 ]] 2143 for sample in self.unknowns: 2144 out += [[ 2145 f"{sample}", 2146 f"{self.samples[sample]['N']}", 2147 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2148 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2149 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2150 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2151 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2152 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2153 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2154 ]] 2155 if save_to_file: 2156 if not os.path.exists(dir): 2157 os.makedirs(dir) 2158 if filename is None: 2159 filename = f'D{self._4x}_samples.csv' 2160 with open(f'{dir}/{filename}', 'w') as fid: 2161 fid.write(make_csv(out)) 2162 if print_out: 2163 self.msg('\n'+pretty_table(out)) 2164 if output == 'raw': 2165 return out 2166 elif output == 'pretty': 2167 return pretty_table(out) 2168 2169 2170 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2171 ''' 2172 Generate session plots and save them to disk. 2173 2174 **Parameters** 2175 2176 + `dir`: the directory in which to save the plots 2177 + `figsize`: the width and height (in inches) of each plot 2178 ''' 2179 if not os.path.exists(dir): 2180 os.makedirs(dir) 2181 2182 for session in self.sessions: 2183 sp = self.plot_single_session(session, xylimits = 'constant') 2184 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2185 ppl.close(sp.fig) 2186 2187 2188 @make_verbal 2189 def consolidate_samples(self): 2190 ''' 2191 Compile various statistics for each sample. 2192 2193 For each anchor sample: 2194 2195 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2196 + `SE_D47` or `SE_D48`: set to zero by definition 2197 2198 For each unknown sample: 2199 2200 + `D47` or `D48`: the standardized Δ4x value for this unknown 2201 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2202 2203 For each anchor and unknown: 2204 2205 + `N`: the total number of analyses of this sample 2206 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2207 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2208 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2209 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2210 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2211 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2212 ''' 2213 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2214 for sample in self.samples: 2215 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2216 if self.samples[sample]['N'] > 1: 2217 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2218 2219 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2220 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2221 2222 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2223 if len(D4x_pop) > 2: 2224 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2225 2226 if self.standardization_method == 'pooled': 2227 for sample in self.anchors: 2228 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2229 self.samples[sample][f'SE_D{self._4x}'] = 0. 2230 for sample in self.unknowns: 2231 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2232 try: 2233 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2234 except ValueError: 2235 # when `sample` is constrained by self.standardize(constraints = {...}), 2236 # it is no longer listed in self.standardization.var_names. 2237 # Temporary fix: define SE as zero for now 2238 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2239 2240 elif self.standardization_method == 'indep_sessions': 2241 for sample in self.anchors: 2242 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2243 self.samples[sample][f'SE_D{self._4x}'] = 0. 2244 for sample in self.unknowns: 2245 self.msg(f'Consolidating sample {sample}') 2246 self.unknowns[sample][f'session_D{self._4x}'] = {} 2247 session_avg = [] 2248 for session in self.sessions: 2249 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2250 if sdata: 2251 self.msg(f'{sample} found in session {session}') 2252 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2253 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2254 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2255 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2256 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2257 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2258 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2259 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2260 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2261 wsum = sum([weights[s] for s in weights]) 2262 for s in weights: 2263 self.unknowns[sample][f'session_D{self._4x}'][s] += [self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 / wsum] 2264 2265 2266 def consolidate_sessions(self): 2267 ''' 2268 Compute various statistics for each session. 2269 2270 + `Na`: Number of anchor analyses in the session 2271 + `Nu`: Number of unknown analyses in the session 2272 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2273 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2274 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2275 + `a`: scrambling factor 2276 + `b`: compositional slope 2277 + `c`: WG offset 2278 + `SE_a`: Model stadard erorr of `a` 2279 + `SE_b`: Model stadard erorr of `b` 2280 + `SE_c`: Model stadard erorr of `c` 2281 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2282 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2283 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2284 + `a2`: scrambling factor drift 2285 + `b2`: compositional slope drift 2286 + `c2`: WG offset drift 2287 + `Np`: Number of standardization parameters to fit 2288 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2289 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2290 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2291 ''' 2292 for session in self.sessions: 2293 if 'd13Cwg_VPDB' not in self.sessions[session]: 2294 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2295 if 'd18Owg_VSMOW' not in self.sessions[session]: 2296 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2297 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2298 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2299 2300 self.msg(f'Computing repeatabilities for session {session}') 2301 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2302 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2303 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2304 2305 if self.standardization_method == 'pooled': 2306 for session in self.sessions: 2307 2308 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2309 i = self.standardization.var_names.index(f'a_{pf(session)}') 2310 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2311 2312 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2313 i = self.standardization.var_names.index(f'b_{pf(session)}') 2314 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2315 2316 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2317 i = self.standardization.var_names.index(f'c_{pf(session)}') 2318 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2319 2320 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2321 if self.sessions[session]['scrambling_drift']: 2322 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2323 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2324 else: 2325 self.sessions[session]['SE_a2'] = 0. 2326 2327 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2328 if self.sessions[session]['slope_drift']: 2329 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2330 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2331 else: 2332 self.sessions[session]['SE_b2'] = 0. 2333 2334 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2335 if self.sessions[session]['wg_drift']: 2336 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2337 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2338 else: 2339 self.sessions[session]['SE_c2'] = 0. 2340 2341 i = self.standardization.var_names.index(f'a_{pf(session)}') 2342 j = self.standardization.var_names.index(f'b_{pf(session)}') 2343 k = self.standardization.var_names.index(f'c_{pf(session)}') 2344 CM = np.zeros((6,6)) 2345 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2346 try: 2347 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2348 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2349 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2350 try: 2351 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2352 CM[3,4] = self.standardization.covar[i2,j2] 2353 CM[4,3] = self.standardization.covar[j2,i2] 2354 except ValueError: 2355 pass 2356 try: 2357 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2358 CM[3,5] = self.standardization.covar[i2,k2] 2359 CM[5,3] = self.standardization.covar[k2,i2] 2360 except ValueError: 2361 pass 2362 except ValueError: 2363 pass 2364 try: 2365 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2366 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2367 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2368 try: 2369 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2370 CM[4,5] = self.standardization.covar[j2,k2] 2371 CM[5,4] = self.standardization.covar[k2,j2] 2372 except ValueError: 2373 pass 2374 except ValueError: 2375 pass 2376 try: 2377 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2378 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2379 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2380 except ValueError: 2381 pass 2382 2383 self.sessions[session]['CM'] = CM 2384 2385 elif self.standardization_method == 'indep_sessions': 2386 pass # Not implemented yet 2387 2388 2389 @make_verbal 2390 def repeatabilities(self): 2391 ''' 2392 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2393 (for all samples, for anchors, and for unknowns). 2394 ''' 2395 self.msg('Computing reproducibilities for all sessions') 2396 2397 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2398 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2399 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2400 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2401 self.repeatability[f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', samples = 'all samples') 2402 2403 2404 @make_verbal 2405 def consolidate(self, tables = True, plots = True): 2406 ''' 2407 Collect information about samples, sessions and repeatabilities. 2408 ''' 2409 self.consolidate_samples() 2410 self.consolidate_sessions() 2411 self.repeatabilities() 2412 2413 if tables: 2414 self.summary() 2415 self.table_of_sessions() 2416 self.table_of_analyses() 2417 self.table_of_samples() 2418 2419 if plots: 2420 self.plot_sessions() 2421 2422 2423 @make_verbal 2424 def rmswd(self, 2425 samples = 'all samples', 2426 sessions = 'all sessions', 2427 ): 2428 ''' 2429 Compute the χ2, root mean squared weighted deviation 2430 (i.e. reduced χ2), and corresponding degrees of freedom of the 2431 Δ4x values for samples in `samples` and sessions in `sessions`. 2432 2433 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2434 ''' 2435 if samples == 'all samples': 2436 mysamples = [k for k in self.samples] 2437 elif samples == 'anchors': 2438 mysamples = [k for k in self.anchors] 2439 elif samples == 'unknowns': 2440 mysamples = [k for k in self.unknowns] 2441 else: 2442 mysamples = samples 2443 2444 if sessions == 'all sessions': 2445 sessions = [k for k in self.sessions] 2446 2447 chisq, Nf = 0, 0 2448 for sample in mysamples : 2449 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2450 if len(G) > 1 : 2451 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2452 Nf += (len(G) - 1) 2453 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2454 r = (chisq / Nf)**.5 if Nf > 0 else 0 2455 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2456 return {'rmswd': r, 'chisq': chisq, 'Nf': Nf} 2457 2458 2459 @make_verbal 2460 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2461 ''' 2462 Compute the repeatability of `[r[key] for r in self]` 2463 ''' 2464 # NB: it's debatable whether rD47 should be computed 2465 # with Nf = len(self)-len(self.samples) instead of 2466 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2467 2468 if samples == 'all samples': 2469 mysamples = [k for k in self.samples] 2470 elif samples == 'anchors': 2471 mysamples = [k for k in self.anchors] 2472 elif samples == 'unknowns': 2473 mysamples = [k for k in self.unknowns] 2474 else: 2475 mysamples = samples 2476 2477 if sessions == 'all sessions': 2478 sessions = [k for k in self.sessions] 2479 2480 if key in ['D47', 'D48']: 2481 chisq, Nf = 0, 0 2482 for sample in mysamples : 2483 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2484 if len(X) > 1 : 2485 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2486 if sample in self.unknowns: 2487 Nf += len(X) - 1 2488 else: 2489 Nf += len(X) 2490 if samples in ['anchors', 'all samples']: 2491 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2492 r = (chisq / Nf)**.5 if Nf > 0 else 0 2493 2494 else: # if key not in ['D47', 'D48'] 2495 chisq, Nf = 0, 0 2496 for sample in mysamples : 2497 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2498 if len(X) > 1 : 2499 Nf += len(X) - 1 2500 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2501 r = (chisq / Nf)**.5 if Nf > 0 else 0 2502 2503 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2504 return r 2505 2506 def sample_average(self, samples, weights = 'equal', normalize = True): 2507 ''' 2508 Weighted average Δ4x value of a group of samples, accounting for covariance. 2509 2510 Returns the weighed average Δ4x value and associated SE 2511 of a group of samples. Weights are equal by default. If `normalize` is 2512 true, `weights` will be rescaled so that their sum equals 1. 2513 2514 **Examples** 2515 2516 ```python 2517 self.sample_average(['X','Y'], [1, 2]) 2518 ``` 2519 2520 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2521 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2522 values of samples X and Y, respectively. 2523 2524 ```python 2525 self.sample_average(['X','Y'], [1, -1], normalize = False) 2526 ``` 2527 2528 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2529 ''' 2530 if weights == 'equal': 2531 weights = [1/len(samples)] * len(samples) 2532 2533 if normalize: 2534 s = sum(weights) 2535 if s: 2536 weights = [w/s for w in weights] 2537 2538 try: 2539# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2540# C = self.standardization.covar[indices,:][:,indices] 2541 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2542 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2543 return correlated_sum(X, C, weights) 2544 except ValueError: 2545 return (0., 0.) 2546 2547 2548 def sample_D4x_covar(self, sample1, sample2 = None): 2549 ''' 2550 Covariance between Δ4x values of samples 2551 2552 Returns the error covariance between the average Δ4x values of two 2553 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2554 returns the Δ4x variance for that sample. 2555 ''' 2556 if sample2 is None: 2557 sample2 = sample1 2558 if self.standardization_method == 'pooled': 2559 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2560 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2561 return self.standardization.covar[i, j] 2562 elif self.standardization_method == 'indep_sessions': 2563 if sample1 == sample2: 2564 return self.samples[sample1][f'SE_D{self._4x}']**2 2565 else: 2566 c = 0 2567 for session in self.sessions: 2568 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2569 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2570 if sdata1 and sdata2: 2571 a = self.sessions[session]['a'] 2572 # !! TODO: CM below does not account for temporal changes in standardization parameters 2573 CM = self.sessions[session]['CM'][:3,:3] 2574 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2575 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2576 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2577 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2578 c += ( 2579 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2580 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2581 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2582 @ CM 2583 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2584 ) / a**2 2585 return float(c) 2586 2587 def sample_D4x_correl(self, sample1, sample2 = None): 2588 ''' 2589 Correlation between Δ4x errors of samples 2590 2591 Returns the error correlation between the average Δ4x values of two samples. 2592 ''' 2593 if sample2 is None or sample2 == sample1: 2594 return 1. 2595 return ( 2596 self.sample_D4x_covar(sample1, sample2) 2597 / self.unknowns[sample1][f'SE_D{self._4x}'] 2598 / self.unknowns[sample2][f'SE_D{self._4x}'] 2599 ) 2600 2601 def plot_single_session(self, 2602 session, 2603 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2604 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2605 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2606 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2607 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2608 xylimits = 'free', # | 'constant' 2609 x_label = None, 2610 y_label = None, 2611 error_contour_interval = 'auto', 2612 fig = 'new', 2613 ): 2614 ''' 2615 Generate plot for a single session 2616 ''' 2617 if x_label is None: 2618 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2619 if y_label is None: 2620 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2621 2622 out = _SessionPlot() 2623 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2624 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2625 2626 if fig == 'new': 2627 out.fig = ppl.figure(figsize = (6,6)) 2628 ppl.subplots_adjust(.1,.1,.9,.9) 2629 2630 out.anchor_analyses, = ppl.plot( 2631 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2632 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2633 **kw_plot_anchors) 2634 out.unknown_analyses, = ppl.plot( 2635 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2636 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2637 **kw_plot_unknowns) 2638 out.anchor_avg = ppl.plot( 2639 np.array([ np.array([ 2640 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2641 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2642 ]) for sample in anchors]).T, 2643 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2644 **kw_plot_anchor_avg) 2645 out.unknown_avg = ppl.plot( 2646 np.array([ np.array([ 2647 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2648 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2649 ]) for sample in unknowns]).T, 2650 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2651 **kw_plot_unknown_avg) 2652 if xylimits == 'constant': 2653 x = [r[f'd{self._4x}'] for r in self] 2654 y = [r[f'D{self._4x}'] for r in self] 2655 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2656 w, h = x2-x1, y2-y1 2657 x1 -= w/20 2658 x2 += w/20 2659 y1 -= h/20 2660 y2 += h/20 2661 ppl.axis([x1, x2, y1, y2]) 2662 elif xylimits == 'free': 2663 x1, x2, y1, y2 = ppl.axis() 2664 else: 2665 x1, x2, y1, y2 = ppl.axis(xylimits) 2666 2667 if error_contour_interval != 'none': 2668 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2669 XI,YI = np.meshgrid(xi, yi) 2670 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2671 if error_contour_interval == 'auto': 2672 rng = np.max(SI) - np.min(SI) 2673 if rng <= 0.01: 2674 cinterval = 0.001 2675 elif rng <= 0.03: 2676 cinterval = 0.004 2677 elif rng <= 0.1: 2678 cinterval = 0.01 2679 elif rng <= 0.3: 2680 cinterval = 0.03 2681 elif rng <= 1.: 2682 cinterval = 0.1 2683 else: 2684 cinterval = 0.5 2685 else: 2686 cinterval = error_contour_interval 2687 2688 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2689 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2690 out.clabel = ppl.clabel(out.contour) 2691 2692 ppl.xlabel(x_label) 2693 ppl.ylabel(y_label) 2694 ppl.title(session, weight = 'bold') 2695 ppl.grid(alpha = .2) 2696 out.ax = ppl.gca() 2697 2698 return out 2699 2700 def plot_residuals( 2701 self, 2702 hist = False, 2703 binwidth = 2/3, 2704 dir = 'output', 2705 filename = None, 2706 highlight = [], 2707 colors = None, 2708 figsize = None, 2709 ): 2710 ''' 2711 Plot residuals of each analysis as a function of time (actually, as a function of 2712 the order of analyses in the `D4xdata` object) 2713 2714 + `hist`: whether to add a histogram of residuals 2715 + `histbins`: specify bin edges for the histogram 2716 + `dir`: the directory in which to save the plot 2717 + `highlight`: a list of samples to highlight 2718 + `colors`: a dict of `{<sample>: <color>}` for all samples 2719 + `figsize`: (width, height) of figure 2720 ''' 2721 # Layout 2722 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2723 if hist: 2724 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2725 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2726 else: 2727 ppl.subplots_adjust(.08,.05,.78,.8) 2728 ax1 = ppl.subplot(111) 2729 2730 # Colors 2731 N = len(self.anchors) 2732 if colors is None: 2733 if len(highlight) > 0: 2734 Nh = len(highlight) 2735 if Nh == 1: 2736 colors = {highlight[0]: (0,0,0)} 2737 elif Nh == 3: 2738 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2739 elif Nh == 4: 2740 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2741 else: 2742 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2743 else: 2744 if N == 3: 2745 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2746 elif N == 4: 2747 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2748 else: 2749 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2750 2751 ppl.sca(ax1) 2752 2753 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2754 2755 session = self[0]['Session'] 2756 x1 = 0 2757# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2758 x_sessions = {} 2759 one_or_more_singlets = False 2760 one_or_more_multiplets = False 2761 multiplets = set() 2762 for k,r in enumerate(self): 2763 if r['Session'] != session: 2764 x2 = k-1 2765 x_sessions[session] = (x1+x2)/2 2766 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2767 session = r['Session'] 2768 x1 = k 2769 singlet = len(self.samples[r['Sample']]['data']) == 1 2770 if not singlet: 2771 multiplets.add(r['Sample']) 2772 if r['Sample'] in self.unknowns: 2773 if singlet: 2774 one_or_more_singlets = True 2775 else: 2776 one_or_more_multiplets = True 2777 kw = dict( 2778 marker = 'x' if singlet else '+', 2779 ms = 4 if singlet else 5, 2780 ls = 'None', 2781 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2782 mew = 1, 2783 alpha = 0.2 if singlet else 1, 2784 ) 2785 if highlight and r['Sample'] not in highlight: 2786 kw['alpha'] = 0.2 2787 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2788 x2 = k 2789 x_sessions[session] = (x1+x2)/2 2790 2791 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2792 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2793 if not hist: 2794 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2795 ppl.text(len(self), self.repeatability['r_D47']*1000*self.t95, f" 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", size = 9, alpha = 1, va = 'center') 2796 2797 xmin, xmax, ymin, ymax = ppl.axis() 2798 for s in x_sessions: 2799 ppl.text( 2800 x_sessions[s], 2801 ymax +1, 2802 s, 2803 va = 'bottom', 2804 **( 2805 dict(ha = 'center') 2806 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2807 else dict(ha = 'left', rotation = 45) 2808 ) 2809 ) 2810 2811 if hist: 2812 ppl.sca(ax2) 2813 2814 for s in colors: 2815 kw['marker'] = '+' 2816 kw['ms'] = 5 2817 kw['mec'] = colors[s] 2818 kw['label'] = s 2819 kw['alpha'] = 1 2820 ppl.plot([], [], **kw) 2821 2822 kw['mec'] = (0,0,0) 2823 2824 if one_or_more_singlets: 2825 kw['marker'] = 'x' 2826 kw['ms'] = 4 2827 kw['alpha'] = .2 2828 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2829 ppl.plot([], [], **kw) 2830 2831 if one_or_more_multiplets: 2832 kw['marker'] = '+' 2833 kw['ms'] = 4 2834 kw['alpha'] = 1 2835 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2836 ppl.plot([], [], **kw) 2837 2838 if hist: 2839 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2840 else: 2841 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2842 leg.set_zorder(-1000) 2843 2844 ppl.sca(ax1) 2845 2846 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2847 ppl.xticks([]) 2848 ppl.axis([-1, len(self), None, None]) 2849 2850 if hist: 2851 ppl.sca(ax2) 2852 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2853 ppl.hist( 2854 X, 2855 orientation = 'horizontal', 2856 histtype = 'stepfilled', 2857 ec = [.4]*3, 2858 fc = [.25]*3, 2859 alpha = .25, 2860 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2861 ) 2862 ppl.axis([None, None, ymin, ymax]) 2863 ppl.text(0, 0, 2864 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2865 size = 8, 2866 alpha = 1, 2867 va = 'center', 2868 ha = 'left', 2869 ) 2870 2871 ppl.xticks([]) 2872 ppl.yticks([]) 2873# ax2.spines['left'].set_visible(False) 2874 ax2.spines['right'].set_visible(False) 2875 ax2.spines['top'].set_visible(False) 2876 ax2.spines['bottom'].set_visible(False) 2877 2878 2879 if not os.path.exists(dir): 2880 os.makedirs(dir) 2881 if filename is None: 2882 return fig 2883 elif filename == '': 2884 filename = f'D{self._4x}_residuals.pdf' 2885 ppl.savefig(f'{dir}/{filename}') 2886 ppl.close(fig) 2887 2888 2889 def simulate(self, *args, **kwargs): 2890 ''' 2891 Legacy function with warning message pointing to `virtual_data()` 2892 ''' 2893 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()') 2894 2895 def plot_distribution_of_analyses( 2896 self, 2897 dir = 'output', 2898 filename = None, 2899 vs_time = False, 2900 figsize = (6,4), 2901 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2902 output = None, 2903 ): 2904 ''' 2905 Plot temporal distribution of all analyses in the data set. 2906 2907 **Parameters** 2908 2909 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2910 ''' 2911 2912 asamples = [s for s in self.anchors] 2913 usamples = [s for s in self.unknowns] 2914 if output is None or output == 'fig': 2915 fig = ppl.figure(figsize = figsize) 2916 ppl.subplots_adjust(*subplots_adjust) 2917 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2918 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2919 Xmax += (Xmax-Xmin)/40 2920 Xmin -= (Xmax-Xmin)/41 2921 for k, s in enumerate(asamples + usamples): 2922 if vs_time: 2923 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2924 else: 2925 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2926 Y = [-k for x in X] 2927 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2928 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2929 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2930 ppl.axis([Xmin, Xmax, -k-1, 1]) 2931 ppl.xlabel('\ntime') 2932 ppl.gca().annotate('', 2933 xy = (0.6, -0.02), 2934 xycoords = 'axes fraction', 2935 xytext = (.4, -0.02), 2936 arrowprops = dict(arrowstyle = "->", color = 'k'), 2937 ) 2938 2939 2940 x2 = -1 2941 for session in self.sessions: 2942 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2943 if vs_time: 2944 ppl.axvline(x1, color = 'k', lw = .75) 2945 if x2 > -1: 2946 if not vs_time: 2947 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2948 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2949# from xlrd import xldate_as_datetime 2950# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2951 if vs_time: 2952 ppl.axvline(x2, color = 'k', lw = .75) 2953 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2954 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2955 2956 ppl.xticks([]) 2957 ppl.yticks([]) 2958 2959 if output is None: 2960 if not os.path.exists(dir): 2961 os.makedirs(dir) 2962 if filename == None: 2963 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2964 ppl.savefig(f'{dir}/{filename}') 2965 ppl.close(fig) 2966 elif output == 'ax': 2967 return ppl.gca() 2968 elif output == 'fig': 2969 return fig
Store and process data for a large set of Δ47 and/or Δ48 analyses, usually comprising more than one analytical session.
1017 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 1018 ''' 1019 **Parameters** 1020 1021 + `l`: a list of dictionaries, with each dictionary including at least the keys 1022 `Sample`, `d45`, `d46`, and `d47` or `d48`. 1023 + `mass`: `'47'` or `'48'` 1024 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 1025 + `session`: define session name for analyses without a `Session` key 1026 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 1027 1028 Returns a `D4xdata` object derived from `list`. 1029 ''' 1030 self._4x = mass 1031 self.verbose = verbose 1032 self.prefix = 'D4xdata' 1033 self.logfile = logfile 1034 list.__init__(self, l) 1035 self.Nf = None 1036 self.repeatability = {} 1037 self.refresh(session = session)
Parameters
l
: a list of dictionaries, with each dictionary including at least the keysSample
,d45
,d46
, andd47
ord48
.mass
:'47'
or'48'
logfile
: if specified, write detailed logs to this file path when callingD4xdata
methods.session
: define session name for analyses without aSession
keyverbose
: ifTrue
, print out detailed logs when callingD4xdata
methods.
Returns a D4xdata
object derived from list
.
Absolute (18O/16C) ratio of VSMOW. By default equal to 0.0020052 (Baertschi, 1976)
Mass-dependent exponent for triple oxygen isotopes. By default equal to 0.528 (Barkan & Luz, 2005)
Absolute (17O/16C) ratio of VSMOW.
By default equal to 0.00038475
(Assonov & Brenninkmeijer, 2003,
rescaled to R13_VPDB
)
Absolute (18O/16C) ratio of VPDB.
By definition equal to R18_VSMOW * 1.03092
.
Absolute (17O/16C) ratio of VPDB.
By definition equal to R17_VSMOW * 1.03092 ** LAMBDA_17
.
After the Δ4x standardization step, each sample is tested to assess whether the Δ4x variance within all analyses for that sample differs significantly from that observed for a given reference sample (using Levene's test, which yields a p-value corresponding to the null hypothesis that the underlying variances are equal).
LEVENE_REF_SAMPLE
(by default equal to 'ETH-3'
) specifies which
sample should be used as a reference for this test.
Specifies the 18O/16O fractionation factor generally applicable
to acid reactions in the dataset. Currently used by D4xdata.wg()
,
D4xdata.standardize_d13C
, and D4xdata.standardize_d18O
.
By default equal to 1.008129 (calcite reacted at 90 °C, Kim et al., 2007).
Nominal δ13CVPDB values assigned to carbonate standards, used by
D4xdata.standardize_d13C()
.
By default equal to {'ETH-1': 2.02, 'ETH-2': -10.17, 'ETH-3': 1.71}
after
Bernasconi et al. (2018).
Nominal δ18OVPDB values assigned to carbonate standards, used by
D4xdata.standardize_d18O()
.
By default equal to {'ETH-1': -2.19, 'ETH-2': -18.69, 'ETH-3': -1.78}
after
Bernasconi et al. (2018).
Method by which to standardize δ13C values:
none
: do not apply any δ13C standardization.'1pt'
: within each session, offset all initial δ13C values so as to minimize the difference between final δ13CVPDB values andNominal_d13C_VPDB
(averaged over all analyses for whichNominal_d13C_VPDB
is defined).'2pt'
: within each session, apply a affine trasformation to all δ13C values so as to minimize the difference between final δ13CVPDB values andNominal_d13C_VPDB
(averaged over all analyses for whichNominal_d13C_VPDB
is defined).
Method by which to standardize δ18O values:
none
: do not apply any δ18O standardization.'1pt'
: within each session, offset all initial δ18O values so as to minimize the difference between final δ18OVPDB values andNominal_d18O_VPDB
(averaged over all analyses for whichNominal_d18O_VPDB
is defined).'2pt'
: within each session, apply a affine trasformation to all δ18O values so as to minimize the difference between final δ18OVPDB values andNominal_d18O_VPDB
(averaged over all analyses for whichNominal_d18O_VPDB
is defined).
1040 def make_verbal(oldfun): 1041 ''' 1042 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 1043 ''' 1044 @wraps(oldfun) 1045 def newfun(*args, verbose = '', **kwargs): 1046 myself = args[0] 1047 oldprefix = myself.prefix 1048 myself.prefix = oldfun.__name__ 1049 if verbose != '': 1050 oldverbose = myself.verbose 1051 myself.verbose = verbose 1052 out = oldfun(*args, **kwargs) 1053 myself.prefix = oldprefix 1054 if verbose != '': 1055 myself.verbose = oldverbose 1056 return out 1057 return newfun
Decorator: allow temporarily changing self.prefix
and overriding self.verbose
.
1060 def msg(self, txt): 1061 ''' 1062 Log a message to `self.logfile`, and print it out if `verbose = True` 1063 ''' 1064 self.log(txt) 1065 if self.verbose: 1066 print(f'{f"[{self.prefix}]":<16} {txt}')
Log a message to self.logfile
, and print it out if verbose = True
1069 def vmsg(self, txt): 1070 ''' 1071 Log a message to `self.logfile` and print it out 1072 ''' 1073 self.log(txt) 1074 print(txt)
Log a message to self.logfile
and print it out
1077 def log(self, *txts): 1078 ''' 1079 Log a message to `self.logfile` 1080 ''' 1081 if self.logfile: 1082 with open(self.logfile, 'a') as fid: 1083 for txt in txts: 1084 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}')
Log a message to self.logfile
1087 def refresh(self, session = 'mySession'): 1088 ''' 1089 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1090 ''' 1091 self.fill_in_missing_info(session = session) 1092 self.refresh_sessions() 1093 self.refresh_samples()
Update self.sessions
, self.samples
, self.anchors
, and self.unknowns
.
1096 def refresh_sessions(self): 1097 ''' 1098 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1099 to `False` for all sessions. 1100 ''' 1101 self.sessions = { 1102 s: {'data': [r for r in self if r['Session'] == s]} 1103 for s in sorted({r['Session'] for r in self}) 1104 } 1105 for s in self.sessions: 1106 self.sessions[s]['scrambling_drift'] = False 1107 self.sessions[s]['slope_drift'] = False 1108 self.sessions[s]['wg_drift'] = False 1109 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1110 self.sessions[s]['d18O_standardization_method'] = self.d18O_STANDARDIZATION_METHOD
Update self.sessions
and set scrambling_drift
, slope_drift
, and wg_drift
to False
for all sessions.
1113 def refresh_samples(self): 1114 ''' 1115 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1116 ''' 1117 self.samples = { 1118 s: {'data': [r for r in self if r['Sample'] == s]} 1119 for s in sorted({r['Sample'] for r in self}) 1120 } 1121 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1122 self.unknowns = {s: self.samples[s] for s in self.samples if s not in self.Nominal_D4x}
Define self.samples
, self.anchors
, and self.unknowns
.
1125 def read(self, filename, sep = '', session = ''): 1126 ''' 1127 Read file in csv format to load data into a `D47data` object. 1128 1129 In the csv file, spaces before and after field separators (`','` by default) 1130 are optional. Each line corresponds to a single analysis. 1131 1132 The required fields are: 1133 1134 + `UID`: a unique identifier 1135 + `Session`: an identifier for the analytical session 1136 + `Sample`: a sample identifier 1137 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1138 1139 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1140 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1141 and `d49` are optional, and set to NaN by default. 1142 1143 **Parameters** 1144 1145 + `fileneme`: the path of the file to read 1146 + `sep`: csv separator delimiting the fields 1147 + `session`: set `Session` field to this string for all analyses 1148 ''' 1149 with open(filename) as fid: 1150 self.input(fid.read(), sep = sep, session = session)
Read file in csv format to load data into a D47data
object.
In the csv file, spaces before and after field separators (','
by default)
are optional. Each line corresponds to a single analysis.
The required fields are:
UID
: a unique identifierSession
: an identifier for the analytical sessionSample
: a sample identifierd45
,d46
, and at least one ofd47
ord48
: the working-gas delta values
Independently known oxygen-17 anomalies may be provided as D17O
(in ‰ relative to
VSMOW, λ = self.LAMBDA_17
), and are otherwise assumed to be zero. Working-gas deltas d47
, d48
and d49
are optional, and set to NaN by default.
Parameters
fileneme
: the path of the file to readsep
: csv separator delimiting the fieldssession
: setSession
field to this string for all analyses
1153 def input(self, txt, sep = '', session = ''): 1154 ''' 1155 Read `txt` string in csv format to load analysis data into a `D47data` object. 1156 1157 In the csv string, spaces before and after field separators (`','` by default) 1158 are optional. Each line corresponds to a single analysis. 1159 1160 The required fields are: 1161 1162 + `UID`: a unique identifier 1163 + `Session`: an identifier for the analytical session 1164 + `Sample`: a sample identifier 1165 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1166 1167 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1168 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1169 and `d49` are optional, and set to NaN by default. 1170 1171 **Parameters** 1172 1173 + `txt`: the csv string to read 1174 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1175 whichever appers most often in `txt`. 1176 + `session`: set `Session` field to this string for all analyses 1177 ''' 1178 if sep == '': 1179 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1180 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1181 data = [{k: v if k in ['UID', 'Session', 'Sample'] else smart_type(v) for k,v in zip(txt[0], l) if v != ''} for l in txt[1:]] 1182 1183 if session != '': 1184 for r in data: 1185 r['Session'] = session 1186 1187 self += data 1188 self.refresh()
Read txt
string in csv format to load analysis data into a D47data
object.
In the csv string, spaces before and after field separators (','
by default)
are optional. Each line corresponds to a single analysis.
The required fields are:
UID
: a unique identifierSession
: an identifier for the analytical sessionSample
: a sample identifierd45
,d46
, and at least one ofd47
ord48
: the working-gas delta values
Independently known oxygen-17 anomalies may be provided as D17O
(in ‰ relative to
VSMOW, λ = self.LAMBDA_17
), and are otherwise assumed to be zero. Working-gas deltas d47
, d48
and d49
are optional, and set to NaN by default.
Parameters
txt
: the csv string to readsep
: csv separator delimiting the fields. By default, use,
,;
, or, whichever appers most often in
txt
.session
: setSession
field to this string for all analyses
1191 @make_verbal 1192 def wg(self, samples = None, a18_acid = None): 1193 ''' 1194 Compute bulk composition of the working gas for each session based on 1195 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1196 `self.Nominal_d18O_VPDB`. 1197 ''' 1198 1199 self.msg('Computing WG composition:') 1200 1201 if a18_acid is None: 1202 a18_acid = self.ALPHA_18O_ACID_REACTION 1203 if samples is None: 1204 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1205 1206 assert a18_acid, f'Acid fractionation factor should not be zero.' 1207 1208 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1209 R45R46_standards = {} 1210 for sample in samples: 1211 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1212 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1213 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1214 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1215 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1216 1217 C12_s = 1 / (1 + R13_s) 1218 C13_s = R13_s / (1 + R13_s) 1219 C16_s = 1 / (1 + R17_s + R18_s) 1220 C17_s = R17_s / (1 + R17_s + R18_s) 1221 C18_s = R18_s / (1 + R17_s + R18_s) 1222 1223 C626_s = C12_s * C16_s ** 2 1224 C627_s = 2 * C12_s * C16_s * C17_s 1225 C628_s = 2 * C12_s * C16_s * C18_s 1226 C636_s = C13_s * C16_s ** 2 1227 C637_s = 2 * C13_s * C16_s * C17_s 1228 C727_s = C12_s * C17_s ** 2 1229 1230 R45_s = (C627_s + C636_s) / C626_s 1231 R46_s = (C628_s + C637_s + C727_s) / C626_s 1232 R45R46_standards[sample] = (R45_s, R46_s) 1233 1234 for s in self.sessions: 1235 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1236 assert db, f'No sample from {samples} found in session "{s}".' 1237# dbsamples = sorted({r['Sample'] for r in db}) 1238 1239 X = [r['d45'] for r in db] 1240 Y = [R45R46_standards[r['Sample']][0] for r in db] 1241 x1, x2 = np.min(X), np.max(X) 1242 1243 if x1 < x2: 1244 wgcoord = x1/(x1-x2) 1245 else: 1246 wgcoord = 999 1247 1248 if wgcoord < -.5 or wgcoord > 1.5: 1249 # unreasonable to extrapolate to d45 = 0 1250 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1251 else : 1252 # d45 = 0 is reasonably well bracketed 1253 R45_wg = np.polyfit(X, Y, 1)[1] 1254 1255 X = [r['d46'] for r in db] 1256 Y = [R45R46_standards[r['Sample']][1] for r in db] 1257 x1, x2 = np.min(X), np.max(X) 1258 1259 if x1 < x2: 1260 wgcoord = x1/(x1-x2) 1261 else: 1262 wgcoord = 999 1263 1264 if wgcoord < -.5 or wgcoord > 1.5: 1265 # unreasonable to extrapolate to d46 = 0 1266 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1267 else : 1268 # d46 = 0 is reasonably well bracketed 1269 R46_wg = np.polyfit(X, Y, 1)[1] 1270 1271 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1272 1273 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1274 1275 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1276 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1277 for r in self.sessions[s]['data']: 1278 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1279 r['d18Owg_VSMOW'] = d18Owg_VSMOW
Compute bulk composition of the working gas for each session based on
the carbonate standards defined in both self.Nominal_d13C_VPDB
and
self.Nominal_d18O_VPDB
.
1282 def compute_bulk_delta(self, R45, R46, D17O = 0): 1283 ''' 1284 Compute δ13C_VPDB and δ18O_VSMOW, 1285 by solving the generalized form of equation (17) from 1286 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1287 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1288 solving the corresponding second-order Taylor polynomial. 1289 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1290 ''' 1291 1292 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1293 1294 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1295 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1296 C = 2 * self.R18_VSMOW 1297 D = -R46 1298 1299 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1300 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1301 cc = A + B + C + D 1302 1303 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1304 1305 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1306 R17 = K * R18 ** self.LAMBDA_17 1307 R13 = R45 - 2 * R17 1308 1309 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1310 1311 return d13C_VPDB, d18O_VSMOW
Compute δ13CVPDB and δ18OVSMOW, by solving the generalized form of equation (17) from Brand et al. (2010), assuming that δ18OVSMOW is not too big (0 ± 50 ‰) and solving the corresponding second-order Taylor polynomial. (Appendix A of Daëron et al., 2016)
1314 @make_verbal 1315 def crunch(self, verbose = ''): 1316 ''' 1317 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1318 ''' 1319 for r in self: 1320 self.compute_bulk_and_clumping_deltas(r) 1321 self.standardize_d13C() 1322 self.standardize_d18O() 1323 self.msg(f"Crunched {len(self)} analyses.")
Compute bulk composition and raw clumped isotope anomalies for all analyses.
1326 def fill_in_missing_info(self, session = 'mySession'): 1327 ''' 1328 Fill in optional fields with default values 1329 ''' 1330 for i,r in enumerate(self): 1331 if 'D17O' not in r: 1332 r['D17O'] = 0. 1333 if 'UID' not in r: 1334 r['UID'] = f'{i+1}' 1335 if 'Session' not in r: 1336 r['Session'] = session 1337 for k in ['d47', 'd48', 'd49']: 1338 if k not in r: 1339 r[k] = np.nan
Fill in optional fields with default values
1342 def standardize_d13C(self): 1343 ''' 1344 Perform δ13C standadization within each session `s` according to 1345 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1346 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1347 may be redefined abitrarily at a later stage. 1348 ''' 1349 for s in self.sessions: 1350 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1351 XY = [(r['d13C_VPDB'], self.Nominal_d13C_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d13C_VPDB] 1352 X,Y = zip(*XY) 1353 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1354 offset = np.mean(Y) - np.mean(X) 1355 for r in self.sessions[s]['data']: 1356 r['d13C_VPDB'] += offset 1357 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1358 a,b = np.polyfit(X,Y,1) 1359 for r in self.sessions[s]['data']: 1360 r['d13C_VPDB'] = a * r['d13C_VPDB'] + b
Perform δ13C standadization within each session s
according to
self.sessions[s]['d13C_standardization_method']
, which is defined by default
by D47data.refresh_sessions()
as equal to self.d13C_STANDARDIZATION_METHOD
, but
may be redefined abitrarily at a later stage.
1362 def standardize_d18O(self): 1363 ''' 1364 Perform δ18O standadization within each session `s` according to 1365 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1366 which is defined by default by `D47data.refresh_sessions()`as equal to 1367 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1368 ''' 1369 for s in self.sessions: 1370 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1371 XY = [(r['d18O_VSMOW'], self.Nominal_d18O_VPDB[r['Sample']]) for r in self.sessions[s]['data'] if r['Sample'] in self.Nominal_d18O_VPDB] 1372 X,Y = zip(*XY) 1373 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1374 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1375 offset = np.mean(Y) - np.mean(X) 1376 for r in self.sessions[s]['data']: 1377 r['d18O_VSMOW'] += offset 1378 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1379 a,b = np.polyfit(X,Y,1) 1380 for r in self.sessions[s]['data']: 1381 r['d18O_VSMOW'] = a * r['d18O_VSMOW'] + b
Perform δ18O standadization within each session s
according to
self.ALPHA_18O_ACID_REACTION
and self.sessions[s]['d18O_standardization_method']
,
which is defined by default by D47data.refresh_sessions()
as equal to
self.d18O_STANDARDIZATION_METHOD
, but may be redefined abitrarily at a later stage.
1384 def compute_bulk_and_clumping_deltas(self, r): 1385 ''' 1386 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1387 ''' 1388 1389 # Compute working gas R13, R18, and isobar ratios 1390 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1391 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1392 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1393 1394 # Compute analyte isobar ratios 1395 R45 = (1 + r['d45'] / 1000) * R45_wg 1396 R46 = (1 + r['d46'] / 1000) * R46_wg 1397 R47 = (1 + r['d47'] / 1000) * R47_wg 1398 R48 = (1 + r['d48'] / 1000) * R48_wg 1399 R49 = (1 + r['d49'] / 1000) * R49_wg 1400 1401 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1402 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1403 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1404 1405 # Compute stochastic isobar ratios of the analyte 1406 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1407 R13, R18, D17O = r['D17O'] 1408 ) 1409 1410 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1411 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1412 if (R45 / R45stoch - 1) > 5e-8: 1413 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1414 if (R46 / R46stoch - 1) > 5e-8: 1415 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1416 1417 # Compute raw clumped isotope anomalies 1418 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1419 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1420 r['D49raw'] = 1000 * (R49 / R49stoch - 1)
Compute δ13CVPDB, δ18OVSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis r
.
1423 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1424 ''' 1425 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1426 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1427 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1428 ''' 1429 1430 # Compute R17 1431 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1432 1433 # Compute isotope concentrations 1434 C12 = (1 + R13) ** -1 1435 C13 = C12 * R13 1436 C16 = (1 + R17 + R18) ** -1 1437 C17 = C16 * R17 1438 C18 = C16 * R18 1439 1440 # Compute stochastic isotopologue concentrations 1441 C626 = C16 * C12 * C16 1442 C627 = C16 * C12 * C17 * 2 1443 C628 = C16 * C12 * C18 * 2 1444 C636 = C16 * C13 * C16 1445 C637 = C16 * C13 * C17 * 2 1446 C638 = C16 * C13 * C18 * 2 1447 C727 = C17 * C12 * C17 1448 C728 = C17 * C12 * C18 * 2 1449 C737 = C17 * C13 * C17 1450 C738 = C17 * C13 * C18 * 2 1451 C828 = C18 * C12 * C18 1452 C838 = C18 * C13 * C18 1453 1454 # Compute stochastic isobar ratios 1455 R45 = (C636 + C627) / C626 1456 R46 = (C628 + C637 + C727) / C626 1457 R47 = (C638 + C728 + C737) / C626 1458 R48 = (C738 + C828) / C626 1459 R49 = C838 / C626 1460 1461 # Account for stochastic anomalies 1462 R47 *= 1 + D47 / 1000 1463 R48 *= 1 + D48 / 1000 1464 R49 *= 1 + D49 / 1000 1465 1466 # Return isobar ratios 1467 return R45, R46, R47, R48, R49
Compute isobar ratios for a sample with isotopic ratios R13
and R18
,
optionally accounting for non-zero values of Δ17O (D17O
) and clumped isotope
anomalies (D47
, D48
, D49
), all expressed in permil.
1470 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1471 ''' 1472 Split unknown samples by UID (treat all analyses as different samples) 1473 or by session (treat analyses of a given sample in different sessions as 1474 different samples). 1475 1476 **Parameters** 1477 1478 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1479 + `grouping`: `by_uid` | `by_session` 1480 ''' 1481 if samples_to_split == 'all': 1482 samples_to_split = [s for s in self.unknowns] 1483 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1484 self.grouping = grouping.lower() 1485 if self.grouping in gkeys: 1486 gkey = gkeys[self.grouping] 1487 for r in self: 1488 if r['Sample'] in samples_to_split: 1489 r['Sample_original'] = r['Sample'] 1490 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1491 elif r['Sample'] in self.unknowns: 1492 r['Sample_original'] = r['Sample'] 1493 self.refresh_samples()
Split unknown samples by UID (treat all analyses as different samples) or by session (treat analyses of a given sample in different sessions as different samples).
Parameters
samples_to_split
: a list of samples to split, e.g.,['IAEA-C1', 'IAEA-C2']
grouping
:by_uid
|by_session
1496 def unsplit_samples(self, tables = False): 1497 ''' 1498 Reverse the effects of `D47data.split_samples()`. 1499 1500 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1501 1502 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1503 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1504 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1505 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1506 that case session-averaged Δ4x values are statistically independent). 1507 ''' 1508 unknowns_old = sorted({s for s in self.unknowns}) 1509 CM_old = self.standardization.covar[:,:] 1510 VD_old = self.standardization.params.valuesdict().copy() 1511 vars_old = self.standardization.var_names 1512 1513 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1514 1515 Ns = len(vars_old) - len(unknowns_old) 1516 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1517 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1518 1519 W = np.zeros((len(vars_new), len(vars_old))) 1520 W[:Ns,:Ns] = np.eye(Ns) 1521 for u in unknowns_new: 1522 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1523 if self.grouping == 'by_session': 1524 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1525 elif self.grouping == 'by_uid': 1526 weights = [1 for s in splits] 1527 sw = sum(weights) 1528 weights = [w/sw for w in weights] 1529 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1530 1531 CM_new = W @ CM_old @ W.T 1532 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1533 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1534 1535 self.standardization.covar = CM_new 1536 self.standardization.params.valuesdict = lambda : VD_new 1537 self.standardization.var_names = vars_new 1538 1539 for r in self: 1540 if r['Sample'] in self.unknowns: 1541 r['Sample_split'] = r['Sample'] 1542 r['Sample'] = r['Sample_original'] 1543 1544 self.refresh_samples() 1545 self.consolidate_samples() 1546 self.repeatabilities() 1547 1548 if tables: 1549 self.table_of_analyses() 1550 self.table_of_samples()
Reverse the effects of D47data.split_samples()
.
This should only be used after D4xdata.standardize()
with method='pooled'
.
After D4xdata.standardize()
with method='indep_sessions'
, one should
probably use D4xdata.combine_samples()
instead to reverse the effects of
D47data.split_samples()
with grouping='by_uid'
, or w_avg()
to reverse the
effects of D47data.split_samples()
with grouping='by_sessions'
(because in
that case session-averaged Δ4x values are statistically independent).
1552 def assign_timestamps(self): 1553 ''' 1554 Assign a time field `t` of type `float` to each analysis. 1555 1556 If `TimeTag` is one of the data fields, `t` is equal within a given session 1557 to `TimeTag` minus the mean value of `TimeTag` for that session. 1558 Otherwise, `TimeTag` is by default equal to the index of each analysis 1559 in the dataset and `t` is defined as above. 1560 ''' 1561 for session in self.sessions: 1562 sdata = self.sessions[session]['data'] 1563 try: 1564 t0 = np.mean([r['TimeTag'] for r in sdata]) 1565 for r in sdata: 1566 r['t'] = r['TimeTag'] - t0 1567 except KeyError: 1568 t0 = (len(sdata)-1)/2 1569 for t,r in enumerate(sdata): 1570 r['t'] = t - t0
Assign a time field t
of type float
to each analysis.
If TimeTag
is one of the data fields, t
is equal within a given session
to TimeTag
minus the mean value of TimeTag
for that session.
Otherwise, TimeTag
is by default equal to the index of each analysis
in the dataset and t
is defined as above.
1573 def report(self): 1574 ''' 1575 Prints a report on the standardization fit. 1576 Only applicable after `D4xdata.standardize(method='pooled')`. 1577 ''' 1578 report_fit(self.standardization)
Prints a report on the standardization fit.
Only applicable after D4xdata.standardize(method='pooled')
.
1581 def combine_samples(self, sample_groups): 1582 ''' 1583 Combine analyses of different samples to compute weighted average Δ4x 1584 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1585 dictionary. 1586 1587 Caution: samples are weighted by number of replicate analyses, which is a 1588 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1589 correlated analytical errors for one or more samples). 1590 1591 Returns a tuplet of: 1592 1593 + the list of group names 1594 + an array of the corresponding Δ4x values 1595 + the corresponding (co)variance matrix 1596 1597 **Parameters** 1598 1599 + `sample_groups`: a dictionary of the form: 1600 ```py 1601 {'group1': ['sample_1', 'sample_2'], 1602 'group2': ['sample_3', 'sample_4', 'sample_5']} 1603 ``` 1604 ''' 1605 1606 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1607 groups = sorted(sample_groups.keys()) 1608 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1609 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1610 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1611 W = np.array([ 1612 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1613 for j in groups]) 1614 D4x_new = W @ D4x_old 1615 CM_new = W @ CM_old @ W.T 1616 1617 return groups, D4x_new[:,0], CM_new
Combine analyses of different samples to compute weighted average Δ4x
and new error (co)variances corresponding to the groups defined by the sample_groups
dictionary.
Caution: samples are weighted by number of replicate analyses, which is a reasonable default behavior but is not always optimal (e.g., in the case of strongly correlated analytical errors for one or more samples).
Returns a tuplet of:
- the list of group names
- an array of the corresponding Δ4x values
- the corresponding (co)variance matrix
Parameters
sample_groups
: a dictionary of the form:
{'group1': ['sample_1', 'sample_2'],
'group2': ['sample_3', 'sample_4', 'sample_5']}
1620 @make_verbal 1621 def standardize(self, 1622 method = 'pooled', 1623 weighted_sessions = [], 1624 consolidate = True, 1625 consolidate_tables = False, 1626 consolidate_plots = False, 1627 constraints = {}, 1628 ): 1629 ''' 1630 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1631 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1632 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1633 i.e. that their true Δ4x value does not change between sessions, 1634 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1635 `'indep_sessions'`, the standardization processes each session independently, based only 1636 on anchors analyses. 1637 ''' 1638 1639 self.standardization_method = method 1640 self.assign_timestamps() 1641 1642 if method == 'pooled': 1643 if weighted_sessions: 1644 for session_group in weighted_sessions: 1645 if self._4x == '47': 1646 X = D47data([r for r in self if r['Session'] in session_group]) 1647 elif self._4x == '48': 1648 X = D48data([r for r in self if r['Session'] in session_group]) 1649 X.Nominal_D4x = self.Nominal_D4x.copy() 1650 X.refresh() 1651 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1652 w = np.sqrt(result.redchi) 1653 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1654 for r in X: 1655 r[f'wD{self._4x}raw'] *= w 1656 else: 1657 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1658 for r in self: 1659 r[f'wD{self._4x}raw'] = 1. 1660 1661 params = Parameters() 1662 for k,session in enumerate(self.sessions): 1663 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1664 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1665 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1666 s = pf(session) 1667 params.add(f'a_{s}', value = 0.9) 1668 params.add(f'b_{s}', value = 0.) 1669 params.add(f'c_{s}', value = -0.9) 1670 params.add(f'a2_{s}', value = 0., 1671# vary = self.sessions[session]['scrambling_drift'], 1672 ) 1673 params.add(f'b2_{s}', value = 0., 1674# vary = self.sessions[session]['slope_drift'], 1675 ) 1676 params.add(f'c2_{s}', value = 0., 1677# vary = self.sessions[session]['wg_drift'], 1678 ) 1679 if not self.sessions[session]['scrambling_drift']: 1680 params[f'a2_{s}'].expr = '0' 1681 if not self.sessions[session]['slope_drift']: 1682 params[f'b2_{s}'].expr = '0' 1683 if not self.sessions[session]['wg_drift']: 1684 params[f'c2_{s}'].expr = '0' 1685 1686 for sample in self.unknowns: 1687 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1688 1689 for k in constraints: 1690 params[k].expr = constraints[k] 1691 1692 def residuals(p): 1693 R = [] 1694 for r in self: 1695 session = pf(r['Session']) 1696 sample = pf(r['Sample']) 1697 if r['Sample'] in self.Nominal_D4x: 1698 R += [ ( 1699 r[f'D{self._4x}raw'] - ( 1700 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1701 + p[f'b_{session}'] * r[f'd{self._4x}'] 1702 + p[f'c_{session}'] 1703 + r['t'] * ( 1704 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1705 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1706 + p[f'c2_{session}'] 1707 ) 1708 ) 1709 ) / r[f'wD{self._4x}raw'] ] 1710 else: 1711 R += [ ( 1712 r[f'D{self._4x}raw'] - ( 1713 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1714 + p[f'b_{session}'] * r[f'd{self._4x}'] 1715 + p[f'c_{session}'] 1716 + r['t'] * ( 1717 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1718 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1719 + p[f'c2_{session}'] 1720 ) 1721 ) 1722 ) / r[f'wD{self._4x}raw'] ] 1723 return R 1724 1725 M = Minimizer(residuals, params) 1726 result = M.least_squares() 1727 self.Nf = result.nfree 1728 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1729 new_names, new_covar, new_se = _fullcovar(result)[:3] 1730 result.var_names = new_names 1731 result.covar = new_covar 1732 1733 for r in self: 1734 s = pf(r["Session"]) 1735 a = result.params.valuesdict()[f'a_{s}'] 1736 b = result.params.valuesdict()[f'b_{s}'] 1737 c = result.params.valuesdict()[f'c_{s}'] 1738 a2 = result.params.valuesdict()[f'a2_{s}'] 1739 b2 = result.params.valuesdict()[f'b2_{s}'] 1740 c2 = result.params.valuesdict()[f'c2_{s}'] 1741 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1742 1743 self.standardization = result 1744 1745 for session in self.sessions: 1746 self.sessions[session]['Np'] = 3 1747 for k in ['scrambling', 'slope', 'wg']: 1748 if self.sessions[session][f'{k}_drift']: 1749 self.sessions[session]['Np'] += 1 1750 1751 if consolidate: 1752 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1753 return result 1754 1755 1756 elif method == 'indep_sessions': 1757 1758 if weighted_sessions: 1759 for session_group in weighted_sessions: 1760 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1761 X.Nominal_D4x = self.Nominal_D4x.copy() 1762 X.refresh() 1763 # This is only done to assign r['wD47raw'] for r in X: 1764 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1765 self.msg(f'D{self._4x}raw weights set to {1000*X[0][f"wD{self._4x}raw"]:.1f} ppm for sessions in {session_group}') 1766 else: 1767 self.msg('All weights set to 1 ‰') 1768 for r in self: 1769 r[f'wD{self._4x}raw'] = 1 1770 1771 for session in self.sessions: 1772 s = self.sessions[session] 1773 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1774 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1775 s['Np'] = sum(p_active) 1776 sdata = s['data'] 1777 1778 A = np.array([ 1779 [ 1780 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1781 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1782 1 / r[f'wD{self._4x}raw'], 1783 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1784 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1785 r['t'] / r[f'wD{self._4x}raw'] 1786 ] 1787 for r in sdata if r['Sample'] in self.anchors 1788 ])[:,p_active] # only keep columns for the active parameters 1789 Y = np.array([[r[f'D{self._4x}raw'] / r[f'wD{self._4x}raw']] for r in sdata if r['Sample'] in self.anchors]) 1790 s['Na'] = Y.size 1791 CM = linalg.inv(A.T @ A) 1792 bf = (CM @ A.T @ Y).T[0,:] 1793 k = 0 1794 for n,a in zip(p_names, p_active): 1795 if a: 1796 s[n] = bf[k] 1797# self.msg(f'{n} = {bf[k]}') 1798 k += 1 1799 else: 1800 s[n] = 0. 1801# self.msg(f'{n} = 0.0') 1802 1803 for r in sdata : 1804 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1805 r[f'D{self._4x}'] = (r[f'D{self._4x}raw'] - c - b * r[f'd{self._4x}'] - c2 * r['t'] - b2 * r['t'] * r[f'd{self._4x}']) / (a + a2 * r['t']) 1806 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1807 1808 s['CM'] = np.zeros((6,6)) 1809 i = 0 1810 k_active = [j for j,a in enumerate(p_active) if a] 1811 for j,a in enumerate(p_active): 1812 if a: 1813 s['CM'][j,k_active] = CM[i,:] 1814 i += 1 1815 1816 if not weighted_sessions: 1817 w = self.rmswd()['rmswd'] 1818 for r in self: 1819 r[f'wD{self._4x}'] *= w 1820 r[f'wD{self._4x}raw'] *= w 1821 for session in self.sessions: 1822 self.sessions[session]['CM'] *= w**2 1823 1824 for session in self.sessions: 1825 s = self.sessions[session] 1826 s['SE_a'] = s['CM'][0,0]**.5 1827 s['SE_b'] = s['CM'][1,1]**.5 1828 s['SE_c'] = s['CM'][2,2]**.5 1829 s['SE_a2'] = s['CM'][3,3]**.5 1830 s['SE_b2'] = s['CM'][4,4]**.5 1831 s['SE_c2'] = s['CM'][5,5]**.5 1832 1833 if not weighted_sessions: 1834 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1835 else: 1836 self.Nf = 0 1837 for sg in weighted_sessions: 1838 self.Nf += self.rmswd(sessions = sg)['Nf'] 1839 1840 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1841 1842 avgD4x = { 1843 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1844 for sample in self.samples 1845 } 1846 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1847 rD4x = (chi2/self.Nf)**.5 1848 self.repeatability[f'sigma_{self._4x}'] = rD4x 1849 1850 if consolidate: 1851 self.consolidate(tables = consolidate_tables, plots = consolidate_plots)
Compute absolute Δ4x values for all replicate analyses and for sample averages.
If method
argument is set to 'pooled'
, the standardization processes all sessions
in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous,
i.e. that their true Δ4x value does not change between sessions,
(Daëron, 2021). If method
argument is set to
'indep_sessions'
, the standardization processes each session independently, based only
on anchors analyses.
1854 def standardization_error(self, session, d4x, D4x, t = 0): 1855 ''' 1856 Compute standardization error for a given session and 1857 (δ47, Δ47) composition. 1858 ''' 1859 a = self.sessions[session]['a'] 1860 b = self.sessions[session]['b'] 1861 c = self.sessions[session]['c'] 1862 a2 = self.sessions[session]['a2'] 1863 b2 = self.sessions[session]['b2'] 1864 c2 = self.sessions[session]['c2'] 1865 CM = self.sessions[session]['CM'] 1866 1867 x, y = D4x, d4x 1868 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1869# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1870 dxdy = -(b+b2*t) / (a+a2*t) 1871 dxdz = 1. / (a+a2*t) 1872 dxda = -x / (a+a2*t) 1873 dxdb = -y / (a+a2*t) 1874 dxdc = -1. / (a+a2*t) 1875 dxda2 = -x * a2 / (a+a2*t) 1876 dxdb2 = -y * t / (a+a2*t) 1877 dxdc2 = -t / (a+a2*t) 1878 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1879 sx = (V @ CM @ V.T) ** .5 1880 return sx
Compute standardization error for a given session and (δ47, Δ47) composition.
1883 @make_verbal 1884 def summary(self, 1885 dir = 'output', 1886 filename = None, 1887 save_to_file = True, 1888 print_out = True, 1889 ): 1890 ''' 1891 Print out an/or save to disk a summary of the standardization results. 1892 1893 **Parameters** 1894 1895 + `dir`: the directory in which to save the table 1896 + `filename`: the name to the csv file to write to 1897 + `save_to_file`: whether to save the table to disk 1898 + `print_out`: whether to print out the table 1899 ''' 1900 1901 out = [] 1902 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1903 out += [['N analyses (anchors + unknowns)', f"{len(self)} ({len([r for r in self if r['Sample'] in self.anchors])} + {len([r for r in self if r['Sample'] in self.unknowns])})"]] 1904 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1905 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1906 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1907 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1908 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1909 out += [['Model degrees of freedom', f"{self.Nf}"]] 1910 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1911 out += [['Standardization method', self.standardization_method]] 1912 1913 if save_to_file: 1914 if not os.path.exists(dir): 1915 os.makedirs(dir) 1916 if filename is None: 1917 filename = f'D{self._4x}_summary.csv' 1918 with open(f'{dir}/{filename}', 'w') as fid: 1919 fid.write(make_csv(out)) 1920 if print_out: 1921 self.msg('\n' + pretty_table(out, header = 0))
Print out an/or save to disk a summary of the standardization results.
Parameters
dir
: the directory in which to save the tablefilename
: the name to the csv file to write tosave_to_file
: whether to save the table to diskprint_out
: whether to print out the table
1924 @make_verbal 1925 def table_of_sessions(self, 1926 dir = 'output', 1927 filename = None, 1928 save_to_file = True, 1929 print_out = True, 1930 output = None, 1931 ): 1932 ''' 1933 Print out an/or save to disk a table of sessions. 1934 1935 **Parameters** 1936 1937 + `dir`: the directory in which to save the table 1938 + `filename`: the name to the csv file to write to 1939 + `save_to_file`: whether to save the table to disk 1940 + `print_out`: whether to print out the table 1941 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1942 if set to `'raw'`: return a list of list of strings 1943 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1944 ''' 1945 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1946 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1947 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1948 1949 out = [['Session','Na','Nu','d13Cwg_VPDB','d18Owg_VSMOW','r_d13C','r_d18O',f'r_D{self._4x}','a ± SE','1e3 x b ± SE','c ± SE']] 1950 if include_a2: 1951 out[-1] += ['a2 ± SE'] 1952 if include_b2: 1953 out[-1] += ['b2 ± SE'] 1954 if include_c2: 1955 out[-1] += ['c2 ± SE'] 1956 for session in self.sessions: 1957 out += [[ 1958 session, 1959 f"{self.sessions[session]['Na']}", 1960 f"{self.sessions[session]['Nu']}", 1961 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1962 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1963 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1964 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1965 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1966 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1967 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1968 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1969 ]] 1970 if include_a2: 1971 if self.sessions[session]['scrambling_drift']: 1972 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1973 else: 1974 out[-1] += [''] 1975 if include_b2: 1976 if self.sessions[session]['slope_drift']: 1977 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1978 else: 1979 out[-1] += [''] 1980 if include_c2: 1981 if self.sessions[session]['wg_drift']: 1982 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1983 else: 1984 out[-1] += [''] 1985 1986 if save_to_file: 1987 if not os.path.exists(dir): 1988 os.makedirs(dir) 1989 if filename is None: 1990 filename = f'D{self._4x}_sessions.csv' 1991 with open(f'{dir}/{filename}', 'w') as fid: 1992 fid.write(make_csv(out)) 1993 if print_out: 1994 self.msg('\n' + pretty_table(out)) 1995 if output == 'raw': 1996 return out 1997 elif output == 'pretty': 1998 return pretty_table(out)
Print out an/or save to disk a table of sessions.
Parameters
dir
: the directory in which to save the tablefilename
: the name to the csv file to write tosave_to_file
: whether to save the table to diskprint_out
: whether to print out the tableoutput
: if set to'pretty'
: return a pretty text table (seepretty_table()
); if set to'raw'
: return a list of list of strings (e.g.,[['header1', 'header2'], ['0.1', '0.2']]
)
2001 @make_verbal 2002 def table_of_analyses( 2003 self, 2004 dir = 'output', 2005 filename = None, 2006 save_to_file = True, 2007 print_out = True, 2008 output = None, 2009 ): 2010 ''' 2011 Print out an/or save to disk a table of analyses. 2012 2013 **Parameters** 2014 2015 + `dir`: the directory in which to save the table 2016 + `filename`: the name to the csv file to write to 2017 + `save_to_file`: whether to save the table to disk 2018 + `print_out`: whether to print out the table 2019 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2020 if set to `'raw'`: return a list of list of strings 2021 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2022 ''' 2023 2024 out = [['UID','Session','Sample']] 2025 extra_fields = [f for f in [('SampleMass','.2f'),('ColdFingerPressure','.1f'),('AcidReactionYield','.3f')] if f[0] in {k for r in self for k in r}] 2026 for f in extra_fields: 2027 out[-1] += [f[0]] 2028 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 2029 for r in self: 2030 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 2031 for f in extra_fields: 2032 out[-1] += [f"{r[f[0]]:{f[1]}}"] 2033 out[-1] += [ 2034 f"{r['d13Cwg_VPDB']:.3f}", 2035 f"{r['d18Owg_VSMOW']:.3f}", 2036 f"{r['d45']:.6f}", 2037 f"{r['d46']:.6f}", 2038 f"{r['d47']:.6f}", 2039 f"{r['d48']:.6f}", 2040 f"{r['d49']:.6f}", 2041 f"{r['d13C_VPDB']:.6f}", 2042 f"{r['d18O_VSMOW']:.6f}", 2043 f"{r['D47raw']:.6f}", 2044 f"{r['D48raw']:.6f}", 2045 f"{r['D49raw']:.6f}", 2046 f"{r[f'D{self._4x}']:.6f}" 2047 ] 2048 if save_to_file: 2049 if not os.path.exists(dir): 2050 os.makedirs(dir) 2051 if filename is None: 2052 filename = f'D{self._4x}_analyses.csv' 2053 with open(f'{dir}/{filename}', 'w') as fid: 2054 fid.write(make_csv(out)) 2055 if print_out: 2056 self.msg('\n' + pretty_table(out)) 2057 return out
Print out an/or save to disk a table of analyses.
Parameters
dir
: the directory in which to save the tablefilename
: the name to the csv file to write tosave_to_file
: whether to save the table to diskprint_out
: whether to print out the tableoutput
: if set to'pretty'
: return a pretty text table (seepretty_table()
); if set to'raw'
: return a list of list of strings (e.g.,[['header1', 'header2'], ['0.1', '0.2']]
)
2059 @make_verbal 2060 def covar_table( 2061 self, 2062 correl = False, 2063 dir = 'output', 2064 filename = None, 2065 save_to_file = True, 2066 print_out = True, 2067 output = None, 2068 ): 2069 ''' 2070 Print out, save to disk and/or return the variance-covariance matrix of D4x 2071 for all unknown samples. 2072 2073 **Parameters** 2074 2075 + `dir`: the directory in which to save the csv 2076 + `filename`: the name of the csv file to write to 2077 + `save_to_file`: whether to save the csv 2078 + `print_out`: whether to print out the matrix 2079 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2080 if set to `'raw'`: return a list of list of strings 2081 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2082 ''' 2083 samples = sorted([u for u in self.unknowns]) 2084 out = [[''] + samples] 2085 for s1 in samples: 2086 out.append([s1]) 2087 for s2 in samples: 2088 if correl: 2089 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2090 else: 2091 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2092 2093 if save_to_file: 2094 if not os.path.exists(dir): 2095 os.makedirs(dir) 2096 if filename is None: 2097 if correl: 2098 filename = f'D{self._4x}_correl.csv' 2099 else: 2100 filename = f'D{self._4x}_covar.csv' 2101 with open(f'{dir}/{filename}', 'w') as fid: 2102 fid.write(make_csv(out)) 2103 if print_out: 2104 self.msg('\n'+pretty_table(out)) 2105 if output == 'raw': 2106 return out 2107 elif output == 'pretty': 2108 return pretty_table(out)
Print out, save to disk and/or return the variance-covariance matrix of D4x for all unknown samples.
Parameters
dir
: the directory in which to save the csvfilename
: the name of the csv file to write tosave_to_file
: whether to save the csvprint_out
: whether to print out the matrixoutput
: if set to'pretty'
: return a pretty text matrix (seepretty_table()
); if set to'raw'
: return a list of list of strings (e.g.,[['header1', 'header2'], ['0.1', '0.2']]
)
2110 @make_verbal 2111 def table_of_samples( 2112 self, 2113 dir = 'output', 2114 filename = None, 2115 save_to_file = True, 2116 print_out = True, 2117 output = None, 2118 ): 2119 ''' 2120 Print out, save to disk and/or return a table of samples. 2121 2122 **Parameters** 2123 2124 + `dir`: the directory in which to save the csv 2125 + `filename`: the name of the csv file to write to 2126 + `save_to_file`: whether to save the csv 2127 + `print_out`: whether to print out the table 2128 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2129 if set to `'raw'`: return a list of list of strings 2130 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2131 ''' 2132 2133 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2134 for sample in self.anchors: 2135 out += [[ 2136 f"{sample}", 2137 f"{self.samples[sample]['N']}", 2138 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2139 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2140 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2141 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2142 ]] 2143 for sample in self.unknowns: 2144 out += [[ 2145 f"{sample}", 2146 f"{self.samples[sample]['N']}", 2147 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2148 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2149 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2150 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2151 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2152 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2153 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2154 ]] 2155 if save_to_file: 2156 if not os.path.exists(dir): 2157 os.makedirs(dir) 2158 if filename is None: 2159 filename = f'D{self._4x}_samples.csv' 2160 with open(f'{dir}/{filename}', 'w') as fid: 2161 fid.write(make_csv(out)) 2162 if print_out: 2163 self.msg('\n'+pretty_table(out)) 2164 if output == 'raw': 2165 return out 2166 elif output == 'pretty': 2167 return pretty_table(out)
Print out, save to disk and/or return a table of samples.
Parameters
dir
: the directory in which to save the csvfilename
: the name of the csv file to write tosave_to_file
: whether to save the csvprint_out
: whether to print out the tableoutput
: if set to'pretty'
: return a pretty text table (seepretty_table()
); if set to'raw'
: return a list of list of strings (e.g.,[['header1', 'header2'], ['0.1', '0.2']]
)
2170 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2171 ''' 2172 Generate session plots and save them to disk. 2173 2174 **Parameters** 2175 2176 + `dir`: the directory in which to save the plots 2177 + `figsize`: the width and height (in inches) of each plot 2178 ''' 2179 if not os.path.exists(dir): 2180 os.makedirs(dir) 2181 2182 for session in self.sessions: 2183 sp = self.plot_single_session(session, xylimits = 'constant') 2184 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2185 ppl.close(sp.fig)
Generate session plots and save them to disk.
Parameters
dir
: the directory in which to save the plotsfigsize
: the width and height (in inches) of each plot
2188 @make_verbal 2189 def consolidate_samples(self): 2190 ''' 2191 Compile various statistics for each sample. 2192 2193 For each anchor sample: 2194 2195 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2196 + `SE_D47` or `SE_D48`: set to zero by definition 2197 2198 For each unknown sample: 2199 2200 + `D47` or `D48`: the standardized Δ4x value for this unknown 2201 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2202 2203 For each anchor and unknown: 2204 2205 + `N`: the total number of analyses of this sample 2206 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2207 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2208 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2209 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2210 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2211 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2212 ''' 2213 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2214 for sample in self.samples: 2215 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2216 if self.samples[sample]['N'] > 1: 2217 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2218 2219 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2220 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2221 2222 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2223 if len(D4x_pop) > 2: 2224 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2225 2226 if self.standardization_method == 'pooled': 2227 for sample in self.anchors: 2228 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2229 self.samples[sample][f'SE_D{self._4x}'] = 0. 2230 for sample in self.unknowns: 2231 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2232 try: 2233 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2234 except ValueError: 2235 # when `sample` is constrained by self.standardize(constraints = {...}), 2236 # it is no longer listed in self.standardization.var_names. 2237 # Temporary fix: define SE as zero for now 2238 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2239 2240 elif self.standardization_method == 'indep_sessions': 2241 for sample in self.anchors: 2242 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2243 self.samples[sample][f'SE_D{self._4x}'] = 0. 2244 for sample in self.unknowns: 2245 self.msg(f'Consolidating sample {sample}') 2246 self.unknowns[sample][f'session_D{self._4x}'] = {} 2247 session_avg = [] 2248 for session in self.sessions: 2249 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2250 if sdata: 2251 self.msg(f'{sample} found in session {session}') 2252 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2253 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2254 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2255 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2256 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2257 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2258 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2259 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2260 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2261 wsum = sum([weights[s] for s in weights]) 2262 for s in weights: 2263 self.unknowns[sample][f'session_D{self._4x}'][s] += [self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 / wsum]
Compile various statistics for each sample.
For each anchor sample:
D47
orD48
: the nominal Δ4x value for this anchor, specified byself.Nominal_D4x
SE_D47
orSE_D48
: set to zero by definition
For each unknown sample:
D47
orD48
: the standardized Δ4x value for this unknownSE_D47
orSE_D48
: the standard error of Δ4x for this unknown
For each anchor and unknown:
N
: the total number of analyses of this sampleSD_D47
orSD_D48
: the “sample” (in the statistical sense) standard deviation for this sampled13C_VPDB
: the average δ13CVPDB value for this sampled18O_VSMOW
: the average δ18OVSMOW value for this sample (as CO2)p_Levene
: the p-value from a Levene test of equal variance, indicating whether the Δ4x repeatability this sample differs significantly from that observed for the reference sample specified byself.LEVENE_REF_SAMPLE
.
2266 def consolidate_sessions(self): 2267 ''' 2268 Compute various statistics for each session. 2269 2270 + `Na`: Number of anchor analyses in the session 2271 + `Nu`: Number of unknown analyses in the session 2272 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2273 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2274 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2275 + `a`: scrambling factor 2276 + `b`: compositional slope 2277 + `c`: WG offset 2278 + `SE_a`: Model stadard erorr of `a` 2279 + `SE_b`: Model stadard erorr of `b` 2280 + `SE_c`: Model stadard erorr of `c` 2281 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2282 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2283 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2284 + `a2`: scrambling factor drift 2285 + `b2`: compositional slope drift 2286 + `c2`: WG offset drift 2287 + `Np`: Number of standardization parameters to fit 2288 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2289 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2290 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2291 ''' 2292 for session in self.sessions: 2293 if 'd13Cwg_VPDB' not in self.sessions[session]: 2294 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2295 if 'd18Owg_VSMOW' not in self.sessions[session]: 2296 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2297 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2298 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2299 2300 self.msg(f'Computing repeatabilities for session {session}') 2301 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2302 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2303 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2304 2305 if self.standardization_method == 'pooled': 2306 for session in self.sessions: 2307 2308 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2309 i = self.standardization.var_names.index(f'a_{pf(session)}') 2310 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2311 2312 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2313 i = self.standardization.var_names.index(f'b_{pf(session)}') 2314 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2315 2316 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2317 i = self.standardization.var_names.index(f'c_{pf(session)}') 2318 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2319 2320 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2321 if self.sessions[session]['scrambling_drift']: 2322 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2323 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2324 else: 2325 self.sessions[session]['SE_a2'] = 0. 2326 2327 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2328 if self.sessions[session]['slope_drift']: 2329 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2330 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2331 else: 2332 self.sessions[session]['SE_b2'] = 0. 2333 2334 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2335 if self.sessions[session]['wg_drift']: 2336 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2337 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2338 else: 2339 self.sessions[session]['SE_c2'] = 0. 2340 2341 i = self.standardization.var_names.index(f'a_{pf(session)}') 2342 j = self.standardization.var_names.index(f'b_{pf(session)}') 2343 k = self.standardization.var_names.index(f'c_{pf(session)}') 2344 CM = np.zeros((6,6)) 2345 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2346 try: 2347 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2348 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2349 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2350 try: 2351 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2352 CM[3,4] = self.standardization.covar[i2,j2] 2353 CM[4,3] = self.standardization.covar[j2,i2] 2354 except ValueError: 2355 pass 2356 try: 2357 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2358 CM[3,5] = self.standardization.covar[i2,k2] 2359 CM[5,3] = self.standardization.covar[k2,i2] 2360 except ValueError: 2361 pass 2362 except ValueError: 2363 pass 2364 try: 2365 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2366 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2367 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2368 try: 2369 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2370 CM[4,5] = self.standardization.covar[j2,k2] 2371 CM[5,4] = self.standardization.covar[k2,j2] 2372 except ValueError: 2373 pass 2374 except ValueError: 2375 pass 2376 try: 2377 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2378 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2379 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2380 except ValueError: 2381 pass 2382 2383 self.sessions[session]['CM'] = CM 2384 2385 elif self.standardization_method == 'indep_sessions': 2386 pass # Not implemented yet
Compute various statistics for each session.
Na
: Number of anchor analyses in the sessionNu
: Number of unknown analyses in the sessionr_d13C_VPDB
: δ13CVPDB repeatability of analyses within the sessionr_d18O_VSMOW
: δ18OVSMOW repeatability of analyses within the sessionr_D47
orr_D48
: Δ4x repeatability of analyses within the sessiona
: scrambling factorb
: compositional slopec
: WG offsetSE_a
: Model stadard erorr ofa
SE_b
: Model stadard erorr ofb
SE_c
: Model stadard erorr ofc
scrambling_drift
(boolean): whether to allow a temporal drift in the scrambling factor (a
)slope_drift
(boolean): whether to allow a temporal drift in the compositional slope (b
)wg_drift
(boolean): whether to allow a temporal drift in the WG offset (c
)a2
: scrambling factor driftb2
: compositional slope driftc2
: WG offset driftNp
: Number of standardization parameters to fitCM
: model covariance matrix for (a
,b
,c
,a2
,b2
,c2
)d13Cwg_VPDB
: δ13CVPDB of WGd18Owg_VSMOW
: δ18OVSMOW of WG
2389 @make_verbal 2390 def repeatabilities(self): 2391 ''' 2392 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2393 (for all samples, for anchors, and for unknowns). 2394 ''' 2395 self.msg('Computing reproducibilities for all sessions') 2396 2397 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2398 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2399 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2400 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2401 self.repeatability[f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', samples = 'all samples')
Compute analytical repeatabilities for δ13CVPDB, δ18OVSMOW, Δ4x (for all samples, for anchors, and for unknowns).
2404 @make_verbal 2405 def consolidate(self, tables = True, plots = True): 2406 ''' 2407 Collect information about samples, sessions and repeatabilities. 2408 ''' 2409 self.consolidate_samples() 2410 self.consolidate_sessions() 2411 self.repeatabilities() 2412 2413 if tables: 2414 self.summary() 2415 self.table_of_sessions() 2416 self.table_of_analyses() 2417 self.table_of_samples() 2418 2419 if plots: 2420 self.plot_sessions()
Collect information about samples, sessions and repeatabilities.
2423 @make_verbal 2424 def rmswd(self, 2425 samples = 'all samples', 2426 sessions = 'all sessions', 2427 ): 2428 ''' 2429 Compute the χ2, root mean squared weighted deviation 2430 (i.e. reduced χ2), and corresponding degrees of freedom of the 2431 Δ4x values for samples in `samples` and sessions in `sessions`. 2432 2433 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2434 ''' 2435 if samples == 'all samples': 2436 mysamples = [k for k in self.samples] 2437 elif samples == 'anchors': 2438 mysamples = [k for k in self.anchors] 2439 elif samples == 'unknowns': 2440 mysamples = [k for k in self.unknowns] 2441 else: 2442 mysamples = samples 2443 2444 if sessions == 'all sessions': 2445 sessions = [k for k in self.sessions] 2446 2447 chisq, Nf = 0, 0 2448 for sample in mysamples : 2449 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2450 if len(G) > 1 : 2451 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2452 Nf += (len(G) - 1) 2453 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2454 r = (chisq / Nf)**.5 if Nf > 0 else 0 2455 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2456 return {'rmswd': r, 'chisq': chisq, 'Nf': Nf}
Compute the χ2, root mean squared weighted deviation
(i.e. reduced χ2), and corresponding degrees of freedom of the
Δ4x values for samples in samples
and sessions in sessions
.
Only used in D4xdata.standardize()
with method='indep_sessions'
.
2459 @make_verbal 2460 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2461 ''' 2462 Compute the repeatability of `[r[key] for r in self]` 2463 ''' 2464 # NB: it's debatable whether rD47 should be computed 2465 # with Nf = len(self)-len(self.samples) instead of 2466 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2467 2468 if samples == 'all samples': 2469 mysamples = [k for k in self.samples] 2470 elif samples == 'anchors': 2471 mysamples = [k for k in self.anchors] 2472 elif samples == 'unknowns': 2473 mysamples = [k for k in self.unknowns] 2474 else: 2475 mysamples = samples 2476 2477 if sessions == 'all sessions': 2478 sessions = [k for k in self.sessions] 2479 2480 if key in ['D47', 'D48']: 2481 chisq, Nf = 0, 0 2482 for sample in mysamples : 2483 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2484 if len(X) > 1 : 2485 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2486 if sample in self.unknowns: 2487 Nf += len(X) - 1 2488 else: 2489 Nf += len(X) 2490 if samples in ['anchors', 'all samples']: 2491 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2492 r = (chisq / Nf)**.5 if Nf > 0 else 0 2493 2494 else: # if key not in ['D47', 'D48'] 2495 chisq, Nf = 0, 0 2496 for sample in mysamples : 2497 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2498 if len(X) > 1 : 2499 Nf += len(X) - 1 2500 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2501 r = (chisq / Nf)**.5 if Nf > 0 else 0 2502 2503 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2504 return r
Compute the repeatability of [r[key] for r in self]
2506 def sample_average(self, samples, weights = 'equal', normalize = True): 2507 ''' 2508 Weighted average Δ4x value of a group of samples, accounting for covariance. 2509 2510 Returns the weighed average Δ4x value and associated SE 2511 of a group of samples. Weights are equal by default. If `normalize` is 2512 true, `weights` will be rescaled so that their sum equals 1. 2513 2514 **Examples** 2515 2516 ```python 2517 self.sample_average(['X','Y'], [1, 2]) 2518 ``` 2519 2520 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2521 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2522 values of samples X and Y, respectively. 2523 2524 ```python 2525 self.sample_average(['X','Y'], [1, -1], normalize = False) 2526 ``` 2527 2528 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2529 ''' 2530 if weights == 'equal': 2531 weights = [1/len(samples)] * len(samples) 2532 2533 if normalize: 2534 s = sum(weights) 2535 if s: 2536 weights = [w/s for w in weights] 2537 2538 try: 2539# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2540# C = self.standardization.covar[indices,:][:,indices] 2541 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2542 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2543 return correlated_sum(X, C, weights) 2544 except ValueError: 2545 return (0., 0.)
Weighted average Δ4x value of a group of samples, accounting for covariance.
Returns the weighed average Δ4x value and associated SE
of a group of samples. Weights are equal by default. If normalize
is
true, weights
will be rescaled so that their sum equals 1.
Examples
self.sample_average(['X','Y'], [1, 2])
returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, where Δ4x(X) and Δ4x(Y) are the average Δ4x values of samples X and Y, respectively.
self.sample_average(['X','Y'], [1, -1], normalize = False)
returns the value and SE of the difference Δ4x(X) - Δ4x(Y).
2548 def sample_D4x_covar(self, sample1, sample2 = None): 2549 ''' 2550 Covariance between Δ4x values of samples 2551 2552 Returns the error covariance between the average Δ4x values of two 2553 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2554 returns the Δ4x variance for that sample. 2555 ''' 2556 if sample2 is None: 2557 sample2 = sample1 2558 if self.standardization_method == 'pooled': 2559 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2560 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2561 return self.standardization.covar[i, j] 2562 elif self.standardization_method == 'indep_sessions': 2563 if sample1 == sample2: 2564 return self.samples[sample1][f'SE_D{self._4x}']**2 2565 else: 2566 c = 0 2567 for session in self.sessions: 2568 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2569 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2570 if sdata1 and sdata2: 2571 a = self.sessions[session]['a'] 2572 # !! TODO: CM below does not account for temporal changes in standardization parameters 2573 CM = self.sessions[session]['CM'][:3,:3] 2574 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2575 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2576 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2577 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2578 c += ( 2579 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2580 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2581 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2582 @ CM 2583 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2584 ) / a**2 2585 return float(c)
Covariance between Δ4x values of samples
Returns the error covariance between the average Δ4x values of two
samples. If if only sample_1
is specified, or if sample_1 == sample_2
),
returns the Δ4x variance for that sample.
2587 def sample_D4x_correl(self, sample1, sample2 = None): 2588 ''' 2589 Correlation between Δ4x errors of samples 2590 2591 Returns the error correlation between the average Δ4x values of two samples. 2592 ''' 2593 if sample2 is None or sample2 == sample1: 2594 return 1. 2595 return ( 2596 self.sample_D4x_covar(sample1, sample2) 2597 / self.unknowns[sample1][f'SE_D{self._4x}'] 2598 / self.unknowns[sample2][f'SE_D{self._4x}'] 2599 )
Correlation between Δ4x errors of samples
Returns the error correlation between the average Δ4x values of two samples.
2601 def plot_single_session(self, 2602 session, 2603 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2604 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2605 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2606 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2607 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2608 xylimits = 'free', # | 'constant' 2609 x_label = None, 2610 y_label = None, 2611 error_contour_interval = 'auto', 2612 fig = 'new', 2613 ): 2614 ''' 2615 Generate plot for a single session 2616 ''' 2617 if x_label is None: 2618 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2619 if y_label is None: 2620 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2621 2622 out = _SessionPlot() 2623 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2624 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2625 2626 if fig == 'new': 2627 out.fig = ppl.figure(figsize = (6,6)) 2628 ppl.subplots_adjust(.1,.1,.9,.9) 2629 2630 out.anchor_analyses, = ppl.plot( 2631 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2632 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2633 **kw_plot_anchors) 2634 out.unknown_analyses, = ppl.plot( 2635 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2636 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2637 **kw_plot_unknowns) 2638 out.anchor_avg = ppl.plot( 2639 np.array([ np.array([ 2640 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2641 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2642 ]) for sample in anchors]).T, 2643 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2644 **kw_plot_anchor_avg) 2645 out.unknown_avg = ppl.plot( 2646 np.array([ np.array([ 2647 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2648 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2649 ]) for sample in unknowns]).T, 2650 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2651 **kw_plot_unknown_avg) 2652 if xylimits == 'constant': 2653 x = [r[f'd{self._4x}'] for r in self] 2654 y = [r[f'D{self._4x}'] for r in self] 2655 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2656 w, h = x2-x1, y2-y1 2657 x1 -= w/20 2658 x2 += w/20 2659 y1 -= h/20 2660 y2 += h/20 2661 ppl.axis([x1, x2, y1, y2]) 2662 elif xylimits == 'free': 2663 x1, x2, y1, y2 = ppl.axis() 2664 else: 2665 x1, x2, y1, y2 = ppl.axis(xylimits) 2666 2667 if error_contour_interval != 'none': 2668 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2669 XI,YI = np.meshgrid(xi, yi) 2670 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2671 if error_contour_interval == 'auto': 2672 rng = np.max(SI) - np.min(SI) 2673 if rng <= 0.01: 2674 cinterval = 0.001 2675 elif rng <= 0.03: 2676 cinterval = 0.004 2677 elif rng <= 0.1: 2678 cinterval = 0.01 2679 elif rng <= 0.3: 2680 cinterval = 0.03 2681 elif rng <= 1.: 2682 cinterval = 0.1 2683 else: 2684 cinterval = 0.5 2685 else: 2686 cinterval = error_contour_interval 2687 2688 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2689 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2690 out.clabel = ppl.clabel(out.contour) 2691 2692 ppl.xlabel(x_label) 2693 ppl.ylabel(y_label) 2694 ppl.title(session, weight = 'bold') 2695 ppl.grid(alpha = .2) 2696 out.ax = ppl.gca() 2697 2698 return out
Generate plot for a single session
2700 def plot_residuals( 2701 self, 2702 hist = False, 2703 binwidth = 2/3, 2704 dir = 'output', 2705 filename = None, 2706 highlight = [], 2707 colors = None, 2708 figsize = None, 2709 ): 2710 ''' 2711 Plot residuals of each analysis as a function of time (actually, as a function of 2712 the order of analyses in the `D4xdata` object) 2713 2714 + `hist`: whether to add a histogram of residuals 2715 + `histbins`: specify bin edges for the histogram 2716 + `dir`: the directory in which to save the plot 2717 + `highlight`: a list of samples to highlight 2718 + `colors`: a dict of `{<sample>: <color>}` for all samples 2719 + `figsize`: (width, height) of figure 2720 ''' 2721 # Layout 2722 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2723 if hist: 2724 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2725 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2726 else: 2727 ppl.subplots_adjust(.08,.05,.78,.8) 2728 ax1 = ppl.subplot(111) 2729 2730 # Colors 2731 N = len(self.anchors) 2732 if colors is None: 2733 if len(highlight) > 0: 2734 Nh = len(highlight) 2735 if Nh == 1: 2736 colors = {highlight[0]: (0,0,0)} 2737 elif Nh == 3: 2738 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2739 elif Nh == 4: 2740 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2741 else: 2742 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2743 else: 2744 if N == 3: 2745 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2746 elif N == 4: 2747 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2748 else: 2749 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2750 2751 ppl.sca(ax1) 2752 2753 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2754 2755 session = self[0]['Session'] 2756 x1 = 0 2757# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2758 x_sessions = {} 2759 one_or_more_singlets = False 2760 one_or_more_multiplets = False 2761 multiplets = set() 2762 for k,r in enumerate(self): 2763 if r['Session'] != session: 2764 x2 = k-1 2765 x_sessions[session] = (x1+x2)/2 2766 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2767 session = r['Session'] 2768 x1 = k 2769 singlet = len(self.samples[r['Sample']]['data']) == 1 2770 if not singlet: 2771 multiplets.add(r['Sample']) 2772 if r['Sample'] in self.unknowns: 2773 if singlet: 2774 one_or_more_singlets = True 2775 else: 2776 one_or_more_multiplets = True 2777 kw = dict( 2778 marker = 'x' if singlet else '+', 2779 ms = 4 if singlet else 5, 2780 ls = 'None', 2781 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2782 mew = 1, 2783 alpha = 0.2 if singlet else 1, 2784 ) 2785 if highlight and r['Sample'] not in highlight: 2786 kw['alpha'] = 0.2 2787 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2788 x2 = k 2789 x_sessions[session] = (x1+x2)/2 2790 2791 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2792 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2793 if not hist: 2794 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2795 ppl.text(len(self), self.repeatability['r_D47']*1000*self.t95, f" 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", size = 9, alpha = 1, va = 'center') 2796 2797 xmin, xmax, ymin, ymax = ppl.axis() 2798 for s in x_sessions: 2799 ppl.text( 2800 x_sessions[s], 2801 ymax +1, 2802 s, 2803 va = 'bottom', 2804 **( 2805 dict(ha = 'center') 2806 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2807 else dict(ha = 'left', rotation = 45) 2808 ) 2809 ) 2810 2811 if hist: 2812 ppl.sca(ax2) 2813 2814 for s in colors: 2815 kw['marker'] = '+' 2816 kw['ms'] = 5 2817 kw['mec'] = colors[s] 2818 kw['label'] = s 2819 kw['alpha'] = 1 2820 ppl.plot([], [], **kw) 2821 2822 kw['mec'] = (0,0,0) 2823 2824 if one_or_more_singlets: 2825 kw['marker'] = 'x' 2826 kw['ms'] = 4 2827 kw['alpha'] = .2 2828 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2829 ppl.plot([], [], **kw) 2830 2831 if one_or_more_multiplets: 2832 kw['marker'] = '+' 2833 kw['ms'] = 4 2834 kw['alpha'] = 1 2835 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2836 ppl.plot([], [], **kw) 2837 2838 if hist: 2839 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2840 else: 2841 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2842 leg.set_zorder(-1000) 2843 2844 ppl.sca(ax1) 2845 2846 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2847 ppl.xticks([]) 2848 ppl.axis([-1, len(self), None, None]) 2849 2850 if hist: 2851 ppl.sca(ax2) 2852 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2853 ppl.hist( 2854 X, 2855 orientation = 'horizontal', 2856 histtype = 'stepfilled', 2857 ec = [.4]*3, 2858 fc = [.25]*3, 2859 alpha = .25, 2860 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2861 ) 2862 ppl.axis([None, None, ymin, ymax]) 2863 ppl.text(0, 0, 2864 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2865 size = 8, 2866 alpha = 1, 2867 va = 'center', 2868 ha = 'left', 2869 ) 2870 2871 ppl.xticks([]) 2872 ppl.yticks([]) 2873# ax2.spines['left'].set_visible(False) 2874 ax2.spines['right'].set_visible(False) 2875 ax2.spines['top'].set_visible(False) 2876 ax2.spines['bottom'].set_visible(False) 2877 2878 2879 if not os.path.exists(dir): 2880 os.makedirs(dir) 2881 if filename is None: 2882 return fig 2883 elif filename == '': 2884 filename = f'D{self._4x}_residuals.pdf' 2885 ppl.savefig(f'{dir}/{filename}') 2886 ppl.close(fig)
Plot residuals of each analysis as a function of time (actually, as a function of
the order of analyses in the D4xdata
object)
hist
: whether to add a histogram of residualshistbins
: specify bin edges for the histogramdir
: the directory in which to save the plothighlight
: a list of samples to highlightcolors
: a dict of{<sample>: <color>}
for all samplesfigsize
: (width, height) of figure
2889 def simulate(self, *args, **kwargs): 2890 ''' 2891 Legacy function with warning message pointing to `virtual_data()` 2892 ''' 2893 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()')
Legacy function with warning message pointing to virtual_data()
2895 def plot_distribution_of_analyses( 2896 self, 2897 dir = 'output', 2898 filename = None, 2899 vs_time = False, 2900 figsize = (6,4), 2901 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2902 output = None, 2903 ): 2904 ''' 2905 Plot temporal distribution of all analyses in the data set. 2906 2907 **Parameters** 2908 2909 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2910 ''' 2911 2912 asamples = [s for s in self.anchors] 2913 usamples = [s for s in self.unknowns] 2914 if output is None or output == 'fig': 2915 fig = ppl.figure(figsize = figsize) 2916 ppl.subplots_adjust(*subplots_adjust) 2917 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2918 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2919 Xmax += (Xmax-Xmin)/40 2920 Xmin -= (Xmax-Xmin)/41 2921 for k, s in enumerate(asamples + usamples): 2922 if vs_time: 2923 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2924 else: 2925 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2926 Y = [-k for x in X] 2927 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2928 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2929 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2930 ppl.axis([Xmin, Xmax, -k-1, 1]) 2931 ppl.xlabel('\ntime') 2932 ppl.gca().annotate('', 2933 xy = (0.6, -0.02), 2934 xycoords = 'axes fraction', 2935 xytext = (.4, -0.02), 2936 arrowprops = dict(arrowstyle = "->", color = 'k'), 2937 ) 2938 2939 2940 x2 = -1 2941 for session in self.sessions: 2942 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2943 if vs_time: 2944 ppl.axvline(x1, color = 'k', lw = .75) 2945 if x2 > -1: 2946 if not vs_time: 2947 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2948 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2949# from xlrd import xldate_as_datetime 2950# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2951 if vs_time: 2952 ppl.axvline(x2, color = 'k', lw = .75) 2953 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2954 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2955 2956 ppl.xticks([]) 2957 ppl.yticks([]) 2958 2959 if output is None: 2960 if not os.path.exists(dir): 2961 os.makedirs(dir) 2962 if filename == None: 2963 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2964 ppl.savefig(f'{dir}/{filename}') 2965 ppl.close(fig) 2966 elif output == 'ax': 2967 return ppl.gca() 2968 elif output == 'fig': 2969 return fig
Plot temporal distribution of all analyses in the data set.
Parameters
vs_time
: ifTrue
, plot as a function ofTimeTag
rather than sequentially.
Inherited Members
- builtins.list
- clear
- copy
- append
- insert
- extend
- pop
- remove
- index
- count
- reverse
- sort
2972class D47data(D4xdata): 2973 ''' 2974 Store and process data for a large set of Δ47 analyses, 2975 usually comprising more than one analytical session. 2976 ''' 2977 2978 Nominal_D4x = { 2979 'ETH-1': 0.2052, 2980 'ETH-2': 0.2085, 2981 'ETH-3': 0.6132, 2982 'ETH-4': 0.4511, 2983 'IAEA-C1': 0.3018, 2984 'IAEA-C2': 0.6409, 2985 'MERCK': 0.5135, 2986 } # I-CDES (Bernasconi et al., 2021) 2987 ''' 2988 Nominal Δ47 values assigned to the Δ47 anchor samples, used by 2989 `D47data.standardize()` to normalize unknown samples to an absolute Δ47 2990 reference frame. 2991 2992 By default equal to (after [Bernasconi et al. (2021)](https://doi.org/10.1029/2020GC009588)): 2993 ```py 2994 { 2995 'ETH-1' : 0.2052, 2996 'ETH-2' : 0.2085, 2997 'ETH-3' : 0.6132, 2998 'ETH-4' : 0.4511, 2999 'IAEA-C1' : 0.3018, 3000 'IAEA-C2' : 0.6409, 3001 'MERCK' : 0.5135, 3002 } 3003 ``` 3004 ''' 3005 3006 3007 @property 3008 def Nominal_D47(self): 3009 return self.Nominal_D4x 3010 3011 3012 @Nominal_D47.setter 3013 def Nominal_D47(self, new): 3014 self.Nominal_D4x = dict(**new) 3015 self.refresh() 3016 3017 3018 def __init__(self, l = [], **kwargs): 3019 ''' 3020 **Parameters:** same as `D4xdata.__init__()` 3021 ''' 3022 D4xdata.__init__(self, l = l, mass = '47', **kwargs) 3023 3024 3025 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 3026 ''' 3027 Find all samples for which `Teq` is specified, compute equilibrium Δ47 3028 value for that temperature, and add treat these samples as additional anchors. 3029 3030 **Parameters** 3031 3032 + `fCo2eqD47`: Which CO2 equilibrium law to use 3033 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 3034 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 3035 + `priority`: if `replace`: forget old anchors and only use the new ones; 3036 if `new`: keep pre-existing anchors but update them in case of conflict 3037 between old and new Δ47 values; 3038 if `old`: keep pre-existing anchors but preserve their original Δ47 3039 values in case of conflict. 3040 ''' 3041 f = { 3042 'petersen': fCO2eqD47_Petersen, 3043 'wang': fCO2eqD47_Wang, 3044 }[fCo2eqD47] 3045 foo = {} 3046 for r in self: 3047 if 'Teq' in r: 3048 if r['Sample'] in foo: 3049 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 3050 else: 3051 foo[r['Sample']] = f(r['Teq']) 3052 else: 3053 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 3054 3055 if priority == 'replace': 3056 self.Nominal_D47 = {} 3057 for s in foo: 3058 if priority != 'old' or s not in self.Nominal_D47: 3059 self.Nominal_D47[s] = foo[s]
Store and process data for a large set of Δ47 analyses, usually comprising more than one analytical session.
3018 def __init__(self, l = [], **kwargs): 3019 ''' 3020 **Parameters:** same as `D4xdata.__init__()` 3021 ''' 3022 D4xdata.__init__(self, l = l, mass = '47', **kwargs)
Parameters: same as D4xdata.__init__()
Nominal Δ47 values assigned to the Δ47 anchor samples, used by
D47data.standardize()
to normalize unknown samples to an absolute Δ47
reference frame.
By default equal to (after Bernasconi et al. (2021)):
{
'ETH-1' : 0.2052,
'ETH-2' : 0.2085,
'ETH-3' : 0.6132,
'ETH-4' : 0.4511,
'IAEA-C1' : 0.3018,
'IAEA-C2' : 0.6409,
'MERCK' : 0.5135,
}
3025 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 3026 ''' 3027 Find all samples for which `Teq` is specified, compute equilibrium Δ47 3028 value for that temperature, and add treat these samples as additional anchors. 3029 3030 **Parameters** 3031 3032 + `fCo2eqD47`: Which CO2 equilibrium law to use 3033 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 3034 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 3035 + `priority`: if `replace`: forget old anchors and only use the new ones; 3036 if `new`: keep pre-existing anchors but update them in case of conflict 3037 between old and new Δ47 values; 3038 if `old`: keep pre-existing anchors but preserve their original Δ47 3039 values in case of conflict. 3040 ''' 3041 f = { 3042 'petersen': fCO2eqD47_Petersen, 3043 'wang': fCO2eqD47_Wang, 3044 }[fCo2eqD47] 3045 foo = {} 3046 for r in self: 3047 if 'Teq' in r: 3048 if r['Sample'] in foo: 3049 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 3050 else: 3051 foo[r['Sample']] = f(r['Teq']) 3052 else: 3053 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 3054 3055 if priority == 'replace': 3056 self.Nominal_D47 = {} 3057 for s in foo: 3058 if priority != 'old' or s not in self.Nominal_D47: 3059 self.Nominal_D47[s] = foo[s]
Find all samples for which Teq
is specified, compute equilibrium Δ47
value for that temperature, and add treat these samples as additional anchors.
Parameters
fCo2eqD47
: Which CO2 equilibrium law to use (petersen
: Petersen et al. (2019);wang
: Wang et al. (2019)).priority
: ifreplace
: forget old anchors and only use the new ones; ifnew
: keep pre-existing anchors but update them in case of conflict between old and new Δ47 values; ifold
: keep pre-existing anchors but preserve their original Δ47 values in case of conflict.
Inherited Members
- D4xdata
- R13_VPDB
- R18_VSMOW
- LAMBDA_17
- R17_VSMOW
- R18_VPDB
- R17_VPDB
- LEVENE_REF_SAMPLE
- ALPHA_18O_ACID_REACTION
- Nominal_d13C_VPDB
- Nominal_d18O_VPDB
- d13C_STANDARDIZATION_METHOD
- d18O_STANDARDIZATION_METHOD
- make_verbal
- msg
- vmsg
- log
- refresh
- refresh_sessions
- refresh_samples
- read
- input
- wg
- compute_bulk_delta
- crunch
- fill_in_missing_info
- standardize_d13C
- standardize_d18O
- compute_bulk_and_clumping_deltas
- compute_isobar_ratios
- split_samples
- unsplit_samples
- assign_timestamps
- report
- combine_samples
- standardize
- standardization_error
- summary
- table_of_sessions
- table_of_analyses
- covar_table
- table_of_samples
- plot_sessions
- consolidate_samples
- consolidate_sessions
- repeatabilities
- consolidate
- rmswd
- compute_r
- sample_average
- sample_D4x_covar
- sample_D4x_correl
- plot_single_session
- plot_residuals
- simulate
- plot_distribution_of_analyses
- builtins.list
- clear
- copy
- append
- insert
- extend
- pop
- remove
- index
- count
- reverse
- sort
3064class D48data(D4xdata): 3065 ''' 3066 Store and process data for a large set of Δ48 analyses, 3067 usually comprising more than one analytical session. 3068 ''' 3069 3070 Nominal_D4x = { 3071 'ETH-1': 0.138, 3072 'ETH-2': 0.138, 3073 'ETH-3': 0.270, 3074 'ETH-4': 0.223, 3075 'GU-1': -0.419, 3076 } # (Fiebig et al., 2019, 2021) 3077 ''' 3078 Nominal Δ48 values assigned to the Δ48 anchor samples, used by 3079 `D48data.standardize()` to normalize unknown samples to an absolute Δ48 3080 reference frame. 3081 3082 By default equal to (after [Fiebig et al. (2019)](https://doi.org/10.1016/j.chemgeo.2019.05.019), 3083 Fiebig et al. (in press)): 3084 3085 ```py 3086 { 3087 'ETH-1' : 0.138, 3088 'ETH-2' : 0.138, 3089 'ETH-3' : 0.270, 3090 'ETH-4' : 0.223, 3091 'GU-1' : -0.419, 3092 } 3093 ``` 3094 ''' 3095 3096 3097 @property 3098 def Nominal_D48(self): 3099 return self.Nominal_D4x 3100 3101 3102 @Nominal_D48.setter 3103 def Nominal_D48(self, new): 3104 self.Nominal_D4x = dict(**new) 3105 self.refresh() 3106 3107 3108 def __init__(self, l = [], **kwargs): 3109 ''' 3110 **Parameters:** same as `D4xdata.__init__()` 3111 ''' 3112 D4xdata.__init__(self, l = l, mass = '48', **kwargs)
Store and process data for a large set of Δ48 analyses, usually comprising more than one analytical session.
3108 def __init__(self, l = [], **kwargs): 3109 ''' 3110 **Parameters:** same as `D4xdata.__init__()` 3111 ''' 3112 D4xdata.__init__(self, l = l, mass = '48', **kwargs)
Parameters: same as D4xdata.__init__()
Nominal Δ48 values assigned to the Δ48 anchor samples, used by
D48data.standardize()
to normalize unknown samples to an absolute Δ48
reference frame.
By default equal to (after Fiebig et al. (2019), Fiebig et al. (in press)):
{
'ETH-1' : 0.138,
'ETH-2' : 0.138,
'ETH-3' : 0.270,
'ETH-4' : 0.223,
'GU-1' : -0.419,
}
Inherited Members
- D4xdata
- R13_VPDB
- R18_VSMOW
- LAMBDA_17
- R17_VSMOW
- R18_VPDB
- R17_VPDB
- LEVENE_REF_SAMPLE
- ALPHA_18O_ACID_REACTION
- Nominal_d13C_VPDB
- Nominal_d18O_VPDB
- d13C_STANDARDIZATION_METHOD
- d18O_STANDARDIZATION_METHOD
- make_verbal
- msg
- vmsg
- log
- refresh
- refresh_sessions
- refresh_samples
- read
- input
- wg
- compute_bulk_delta
- crunch
- fill_in_missing_info
- standardize_d13C
- standardize_d18O
- compute_bulk_and_clumping_deltas
- compute_isobar_ratios
- split_samples
- unsplit_samples
- assign_timestamps
- report
- combine_samples
- standardize
- standardization_error
- summary
- table_of_sessions
- table_of_analyses
- covar_table
- table_of_samples
- plot_sessions
- consolidate_samples
- consolidate_sessions
- repeatabilities
- consolidate
- rmswd
- compute_r
- sample_average
- sample_D4x_covar
- sample_D4x_correl
- plot_single_session
- plot_residuals
- simulate
- plot_distribution_of_analyses
- builtins.list
- clear
- copy
- append
- insert
- extend
- pop
- remove
- index
- count
- reverse
- sort