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-11' 24__version__ = '2.0.4' 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 849class D4xdata(list): 850 ''' 851 Store and process data for a large set of Δ47 and/or Δ48 852 analyses, usually comprising more than one analytical session. 853 ''' 854 855 ### 17O CORRECTION PARAMETERS 856 R13_VPDB = 0.01118 # (Chang & Li, 1990) 857 ''' 858 Absolute (13C/12C) ratio of VPDB. 859 By default equal to 0.01118 ([Chang & Li, 1990](http://www.cnki.com.cn/Article/CJFDTotal-JXTW199004006.htm)) 860 ''' 861 862 R18_VSMOW = 0.0020052 # (Baertschi, 1976) 863 ''' 864 Absolute (18O/16C) ratio of VSMOW. 865 By default equal to 0.0020052 ([Baertschi, 1976](https://doi.org/10.1016/0012-821X(76)90115-1)) 866 ''' 867 868 LAMBDA_17 = 0.528 # (Barkan & Luz, 2005) 869 ''' 870 Mass-dependent exponent for triple oxygen isotopes. 871 By default equal to 0.528 ([Barkan & Luz, 2005](https://doi.org/10.1002/rcm.2250)) 872 ''' 873 874 R17_VSMOW = 0.00038475 # (Assonov & Brenninkmeijer, 2003, rescaled to R13_VPDB) 875 ''' 876 Absolute (17O/16C) ratio of VSMOW. 877 By default equal to 0.00038475 878 ([Assonov & Brenninkmeijer, 2003](https://dx.doi.org/10.1002/rcm.1011), 879 rescaled to `R13_VPDB`) 880 ''' 881 882 R18_VPDB = R18_VSMOW * 1.03092 883 ''' 884 Absolute (18O/16C) ratio of VPDB. 885 By definition equal to `R18_VSMOW * 1.03092`. 886 ''' 887 888 R17_VPDB = R17_VSMOW * 1.03092 ** LAMBDA_17 889 ''' 890 Absolute (17O/16C) ratio of VPDB. 891 By definition equal to `R17_VSMOW * 1.03092 ** LAMBDA_17`. 892 ''' 893 894 LEVENE_REF_SAMPLE = 'ETH-3' 895 ''' 896 After the Δ4x standardization step, each sample is tested to 897 assess whether the Δ4x variance within all analyses for that 898 sample differs significantly from that observed for a given reference 899 sample (using [Levene's test](https://en.wikipedia.org/wiki/Levene%27s_test), 900 which yields a p-value corresponding to the null hypothesis that the 901 underlying variances are equal). 902 903 `LEVENE_REF_SAMPLE` (by default equal to `'ETH-3'`) specifies which 904 sample should be used as a reference for this test. 905 ''' 906 907 ALPHA_18O_ACID_REACTION = round(np.exp(3.59 / (90 + 273.15) - 1.79e-3), 6) # (Kim et al., 2007, calcite) 908 ''' 909 Specifies the 18O/16O fractionation factor generally applicable 910 to acid reactions in the dataset. Currently used by `D4xdata.wg()`, 911 `D4xdata.standardize_d13C`, and `D4xdata.standardize_d18O`. 912 913 By default equal to 1.008129 (calcite reacted at 90 °C, 914 [Kim et al., 2007](https://dx.doi.org/10.1016/j.chemgeo.2007.08.005)). 915 ''' 916 917 Nominal_d13C_VPDB = { 918 'ETH-1': 2.02, 919 'ETH-2': -10.17, 920 'ETH-3': 1.71, 921 } # (Bernasconi et al., 2018) 922 ''' 923 Nominal δ13C_VPDB values assigned to carbonate standards, used by 924 `D4xdata.standardize_d13C()`. 925 926 By default equal to `{'ETH-1': 2.02, 'ETH-2': -10.17, 'ETH-3': 1.71}` after 927 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 928 ''' 929 930 Nominal_d18O_VPDB = { 931 'ETH-1': -2.19, 932 'ETH-2': -18.69, 933 'ETH-3': -1.78, 934 } # (Bernasconi et al., 2018) 935 ''' 936 Nominal δ18O_VPDB values assigned to carbonate standards, used by 937 `D4xdata.standardize_d18O()`. 938 939 By default equal to `{'ETH-1': -2.19, 'ETH-2': -18.69, 'ETH-3': -1.78}` after 940 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 941 ''' 942 943 d13C_STANDARDIZATION_METHOD = '2pt' 944 ''' 945 Method by which to standardize δ13C values: 946 947 + `none`: do not apply any δ13C standardization. 948 + `'1pt'`: within each session, offset all initial δ13C values so as to 949 minimize the difference between final δ13C_VPDB values and 950 `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` is defined). 951 + `'2pt'`: within each session, apply a affine trasformation to all δ13C 952 values so as to minimize the difference between final δ13C_VPDB 953 values and `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` 954 is defined). 955 ''' 956 957 d18O_STANDARDIZATION_METHOD = '2pt' 958 ''' 959 Method by which to standardize δ18O values: 960 961 + `none`: do not apply any δ18O standardization. 962 + `'1pt'`: within each session, offset all initial δ18O values so as to 963 minimize the difference between final δ18O_VPDB values and 964 `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` is defined). 965 + `'2pt'`: within each session, apply a affine trasformation to all δ18O 966 values so as to minimize the difference between final δ18O_VPDB 967 values and `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` 968 is defined). 969 ''' 970 971 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 972 ''' 973 **Parameters** 974 975 + `l`: a list of dictionaries, with each dictionary including at least the keys 976 `Sample`, `d45`, `d46`, and `d47` or `d48`. 977 + `mass`: `'47'` or `'48'` 978 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 979 + `session`: define session name for analyses without a `Session` key 980 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 981 982 Returns a `D4xdata` object derived from `list`. 983 ''' 984 self._4x = mass 985 self.verbose = verbose 986 self.prefix = 'D4xdata' 987 self.logfile = logfile 988 list.__init__(self, l) 989 self.Nf = None 990 self.repeatability = {} 991 self.refresh(session = session) 992 993 994 def make_verbal(oldfun): 995 ''' 996 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 997 ''' 998 @wraps(oldfun) 999 def newfun(*args, verbose = '', **kwargs): 1000 myself = args[0] 1001 oldprefix = myself.prefix 1002 myself.prefix = oldfun.__name__ 1003 if verbose != '': 1004 oldverbose = myself.verbose 1005 myself.verbose = verbose 1006 out = oldfun(*args, **kwargs) 1007 myself.prefix = oldprefix 1008 if verbose != '': 1009 myself.verbose = oldverbose 1010 return out 1011 return newfun 1012 1013 1014 def msg(self, txt): 1015 ''' 1016 Log a message to `self.logfile`, and print it out if `verbose = True` 1017 ''' 1018 self.log(txt) 1019 if self.verbose: 1020 print(f'{f"[{self.prefix}]":<16} {txt}') 1021 1022 1023 def vmsg(self, txt): 1024 ''' 1025 Log a message to `self.logfile` and print it out 1026 ''' 1027 self.log(txt) 1028 print(txt) 1029 1030 1031 def log(self, *txts): 1032 ''' 1033 Log a message to `self.logfile` 1034 ''' 1035 if self.logfile: 1036 with open(self.logfile, 'a') as fid: 1037 for txt in txts: 1038 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}') 1039 1040 1041 def refresh(self, session = 'mySession'): 1042 ''' 1043 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1044 ''' 1045 self.fill_in_missing_info(session = session) 1046 self.refresh_sessions() 1047 self.refresh_samples() 1048 1049 1050 def refresh_sessions(self): 1051 ''' 1052 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1053 to `False` for all sessions. 1054 ''' 1055 self.sessions = { 1056 s: {'data': [r for r in self if r['Session'] == s]} 1057 for s in sorted({r['Session'] for r in self}) 1058 } 1059 for s in self.sessions: 1060 self.sessions[s]['scrambling_drift'] = False 1061 self.sessions[s]['slope_drift'] = False 1062 self.sessions[s]['wg_drift'] = False 1063 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1064 self.sessions[s]['d18O_standardization_method'] = self.d18O_STANDARDIZATION_METHOD 1065 1066 1067 def refresh_samples(self): 1068 ''' 1069 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1070 ''' 1071 self.samples = { 1072 s: {'data': [r for r in self if r['Sample'] == s]} 1073 for s in sorted({r['Sample'] for r in self}) 1074 } 1075 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1076 self.unknowns = {s: self.samples[s] for s in self.samples if s not in self.Nominal_D4x} 1077 1078 1079 def read(self, filename, sep = '', session = ''): 1080 ''' 1081 Read file in csv format to load data into a `D47data` object. 1082 1083 In the csv file, spaces before and after field separators (`','` by default) 1084 are optional. Each line corresponds to a single analysis. 1085 1086 The required fields are: 1087 1088 + `UID`: a unique identifier 1089 + `Session`: an identifier for the analytical session 1090 + `Sample`: a sample identifier 1091 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1092 1093 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1094 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1095 and `d49` are optional, and set to NaN by default. 1096 1097 **Parameters** 1098 1099 + `fileneme`: the path of the file to read 1100 + `sep`: csv separator delimiting the fields 1101 + `session`: set `Session` field to this string for all analyses 1102 ''' 1103 with open(filename) as fid: 1104 self.input(fid.read(), sep = sep, session = session) 1105 1106 1107 def input(self, txt, sep = '', session = ''): 1108 ''' 1109 Read `txt` string in csv format to load analysis data into a `D47data` object. 1110 1111 In the csv string, spaces before and after field separators (`','` by default) 1112 are optional. Each line corresponds to a single analysis. 1113 1114 The required fields are: 1115 1116 + `UID`: a unique identifier 1117 + `Session`: an identifier for the analytical session 1118 + `Sample`: a sample identifier 1119 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1120 1121 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1122 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1123 and `d49` are optional, and set to NaN by default. 1124 1125 **Parameters** 1126 1127 + `txt`: the csv string to read 1128 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1129 whichever appers most often in `txt`. 1130 + `session`: set `Session` field to this string for all analyses 1131 ''' 1132 if sep == '': 1133 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1134 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1135 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:]] 1136 1137 if session != '': 1138 for r in data: 1139 r['Session'] = session 1140 1141 self += data 1142 self.refresh() 1143 1144 1145 @make_verbal 1146 def wg(self, samples = None, a18_acid = None): 1147 ''' 1148 Compute bulk composition of the working gas for each session based on 1149 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1150 `self.Nominal_d18O_VPDB`. 1151 ''' 1152 1153 self.msg('Computing WG composition:') 1154 1155 if a18_acid is None: 1156 a18_acid = self.ALPHA_18O_ACID_REACTION 1157 if samples is None: 1158 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1159 1160 assert a18_acid, f'Acid fractionation factor should not be zero.' 1161 1162 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1163 R45R46_standards = {} 1164 for sample in samples: 1165 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1166 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1167 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1168 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1169 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1170 1171 C12_s = 1 / (1 + R13_s) 1172 C13_s = R13_s / (1 + R13_s) 1173 C16_s = 1 / (1 + R17_s + R18_s) 1174 C17_s = R17_s / (1 + R17_s + R18_s) 1175 C18_s = R18_s / (1 + R17_s + R18_s) 1176 1177 C626_s = C12_s * C16_s ** 2 1178 C627_s = 2 * C12_s * C16_s * C17_s 1179 C628_s = 2 * C12_s * C16_s * C18_s 1180 C636_s = C13_s * C16_s ** 2 1181 C637_s = 2 * C13_s * C16_s * C17_s 1182 C727_s = C12_s * C17_s ** 2 1183 1184 R45_s = (C627_s + C636_s) / C626_s 1185 R46_s = (C628_s + C637_s + C727_s) / C626_s 1186 R45R46_standards[sample] = (R45_s, R46_s) 1187 1188 for s in self.sessions: 1189 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1190 assert db, f'No sample from {samples} found in session "{s}".' 1191# dbsamples = sorted({r['Sample'] for r in db}) 1192 1193 X = [r['d45'] for r in db] 1194 Y = [R45R46_standards[r['Sample']][0] for r in db] 1195 x1, x2 = np.min(X), np.max(X) 1196 1197 if x1 < x2: 1198 wgcoord = x1/(x1-x2) 1199 else: 1200 wgcoord = 999 1201 1202 if wgcoord < -.5 or wgcoord > 1.5: 1203 # unreasonable to extrapolate to d45 = 0 1204 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1205 else : 1206 # d45 = 0 is reasonably well bracketed 1207 R45_wg = np.polyfit(X, Y, 1)[1] 1208 1209 X = [r['d46'] for r in db] 1210 Y = [R45R46_standards[r['Sample']][1] for r in db] 1211 x1, x2 = np.min(X), np.max(X) 1212 1213 if x1 < x2: 1214 wgcoord = x1/(x1-x2) 1215 else: 1216 wgcoord = 999 1217 1218 if wgcoord < -.5 or wgcoord > 1.5: 1219 # unreasonable to extrapolate to d46 = 0 1220 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1221 else : 1222 # d46 = 0 is reasonably well bracketed 1223 R46_wg = np.polyfit(X, Y, 1)[1] 1224 1225 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1226 1227 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1228 1229 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1230 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1231 for r in self.sessions[s]['data']: 1232 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1233 r['d18Owg_VSMOW'] = d18Owg_VSMOW 1234 1235 1236 def compute_bulk_delta(self, R45, R46, D17O = 0): 1237 ''' 1238 Compute δ13C_VPDB and δ18O_VSMOW, 1239 by solving the generalized form of equation (17) from 1240 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1241 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1242 solving the corresponding second-order Taylor polynomial. 1243 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1244 ''' 1245 1246 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1247 1248 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1249 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1250 C = 2 * self.R18_VSMOW 1251 D = -R46 1252 1253 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1254 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1255 cc = A + B + C + D 1256 1257 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1258 1259 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1260 R17 = K * R18 ** self.LAMBDA_17 1261 R13 = R45 - 2 * R17 1262 1263 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1264 1265 return d13C_VPDB, d18O_VSMOW 1266 1267 1268 @make_verbal 1269 def crunch(self, verbose = ''): 1270 ''' 1271 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1272 ''' 1273 for r in self: 1274 self.compute_bulk_and_clumping_deltas(r) 1275 self.standardize_d13C() 1276 self.standardize_d18O() 1277 self.msg(f"Crunched {len(self)} analyses.") 1278 1279 1280 def fill_in_missing_info(self, session = 'mySession'): 1281 ''' 1282 Fill in optional fields with default values 1283 ''' 1284 for i,r in enumerate(self): 1285 if 'D17O' not in r: 1286 r['D17O'] = 0. 1287 if 'UID' not in r: 1288 r['UID'] = f'{i+1}' 1289 if 'Session' not in r: 1290 r['Session'] = session 1291 for k in ['d47', 'd48', 'd49']: 1292 if k not in r: 1293 r[k] = np.nan 1294 1295 1296 def standardize_d13C(self): 1297 ''' 1298 Perform δ13C standadization within each session `s` according to 1299 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1300 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1301 may be redefined abitrarily at a later stage. 1302 ''' 1303 for s in self.sessions: 1304 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1305 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] 1306 X,Y = zip(*XY) 1307 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1308 offset = np.mean(Y) - np.mean(X) 1309 for r in self.sessions[s]['data']: 1310 r['d13C_VPDB'] += offset 1311 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1312 a,b = np.polyfit(X,Y,1) 1313 for r in self.sessions[s]['data']: 1314 r['d13C_VPDB'] = a * r['d13C_VPDB'] + b 1315 1316 def standardize_d18O(self): 1317 ''' 1318 Perform δ18O standadization within each session `s` according to 1319 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1320 which is defined by default by `D47data.refresh_sessions()`as equal to 1321 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1322 ''' 1323 for s in self.sessions: 1324 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1325 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] 1326 X,Y = zip(*XY) 1327 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1328 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1329 offset = np.mean(Y) - np.mean(X) 1330 for r in self.sessions[s]['data']: 1331 r['d18O_VSMOW'] += offset 1332 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1333 a,b = np.polyfit(X,Y,1) 1334 for r in self.sessions[s]['data']: 1335 r['d18O_VSMOW'] = a * r['d18O_VSMOW'] + b 1336 1337 1338 def compute_bulk_and_clumping_deltas(self, r): 1339 ''' 1340 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1341 ''' 1342 1343 # Compute working gas R13, R18, and isobar ratios 1344 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1345 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1346 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1347 1348 # Compute analyte isobar ratios 1349 R45 = (1 + r['d45'] / 1000) * R45_wg 1350 R46 = (1 + r['d46'] / 1000) * R46_wg 1351 R47 = (1 + r['d47'] / 1000) * R47_wg 1352 R48 = (1 + r['d48'] / 1000) * R48_wg 1353 R49 = (1 + r['d49'] / 1000) * R49_wg 1354 1355 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1356 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1357 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1358 1359 # Compute stochastic isobar ratios of the analyte 1360 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1361 R13, R18, D17O = r['D17O'] 1362 ) 1363 1364 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1365 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1366 if (R45 / R45stoch - 1) > 5e-8: 1367 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1368 if (R46 / R46stoch - 1) > 5e-8: 1369 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1370 1371 # Compute raw clumped isotope anomalies 1372 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1373 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1374 r['D49raw'] = 1000 * (R49 / R49stoch - 1) 1375 1376 1377 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1378 ''' 1379 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1380 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1381 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1382 ''' 1383 1384 # Compute R17 1385 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1386 1387 # Compute isotope concentrations 1388 C12 = (1 + R13) ** -1 1389 C13 = C12 * R13 1390 C16 = (1 + R17 + R18) ** -1 1391 C17 = C16 * R17 1392 C18 = C16 * R18 1393 1394 # Compute stochastic isotopologue concentrations 1395 C626 = C16 * C12 * C16 1396 C627 = C16 * C12 * C17 * 2 1397 C628 = C16 * C12 * C18 * 2 1398 C636 = C16 * C13 * C16 1399 C637 = C16 * C13 * C17 * 2 1400 C638 = C16 * C13 * C18 * 2 1401 C727 = C17 * C12 * C17 1402 C728 = C17 * C12 * C18 * 2 1403 C737 = C17 * C13 * C17 1404 C738 = C17 * C13 * C18 * 2 1405 C828 = C18 * C12 * C18 1406 C838 = C18 * C13 * C18 1407 1408 # Compute stochastic isobar ratios 1409 R45 = (C636 + C627) / C626 1410 R46 = (C628 + C637 + C727) / C626 1411 R47 = (C638 + C728 + C737) / C626 1412 R48 = (C738 + C828) / C626 1413 R49 = C838 / C626 1414 1415 # Account for stochastic anomalies 1416 R47 *= 1 + D47 / 1000 1417 R48 *= 1 + D48 / 1000 1418 R49 *= 1 + D49 / 1000 1419 1420 # Return isobar ratios 1421 return R45, R46, R47, R48, R49 1422 1423 1424 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1425 ''' 1426 Split unknown samples by UID (treat all analyses as different samples) 1427 or by session (treat analyses of a given sample in different sessions as 1428 different samples). 1429 1430 **Parameters** 1431 1432 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1433 + `grouping`: `by_uid` | `by_session` 1434 ''' 1435 if samples_to_split == 'all': 1436 samples_to_split = [s for s in self.unknowns] 1437 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1438 self.grouping = grouping.lower() 1439 if self.grouping in gkeys: 1440 gkey = gkeys[self.grouping] 1441 for r in self: 1442 if r['Sample'] in samples_to_split: 1443 r['Sample_original'] = r['Sample'] 1444 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1445 elif r['Sample'] in self.unknowns: 1446 r['Sample_original'] = r['Sample'] 1447 self.refresh_samples() 1448 1449 1450 def unsplit_samples(self, tables = False): 1451 ''' 1452 Reverse the effects of `D47data.split_samples()`. 1453 1454 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1455 1456 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1457 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1458 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1459 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1460 that case session-averaged Δ4x values are statistically independent). 1461 ''' 1462 unknowns_old = sorted({s for s in self.unknowns}) 1463 CM_old = self.standardization.covar[:,:] 1464 VD_old = self.standardization.params.valuesdict().copy() 1465 vars_old = self.standardization.var_names 1466 1467 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1468 1469 Ns = len(vars_old) - len(unknowns_old) 1470 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1471 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1472 1473 W = np.zeros((len(vars_new), len(vars_old))) 1474 W[:Ns,:Ns] = np.eye(Ns) 1475 for u in unknowns_new: 1476 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1477 if self.grouping == 'by_session': 1478 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1479 elif self.grouping == 'by_uid': 1480 weights = [1 for s in splits] 1481 sw = sum(weights) 1482 weights = [w/sw for w in weights] 1483 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1484 1485 CM_new = W @ CM_old @ W.T 1486 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1487 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1488 1489 self.standardization.covar = CM_new 1490 self.standardization.params.valuesdict = lambda : VD_new 1491 self.standardization.var_names = vars_new 1492 1493 for r in self: 1494 if r['Sample'] in self.unknowns: 1495 r['Sample_split'] = r['Sample'] 1496 r['Sample'] = r['Sample_original'] 1497 1498 self.refresh_samples() 1499 self.consolidate_samples() 1500 self.repeatabilities() 1501 1502 if tables: 1503 self.table_of_analyses() 1504 self.table_of_samples() 1505 1506 def assign_timestamps(self): 1507 ''' 1508 Assign a time field `t` of type `float` to each analysis. 1509 1510 If `TimeTag` is one of the data fields, `t` is equal within a given session 1511 to `TimeTag` minus the mean value of `TimeTag` for that session. 1512 Otherwise, `TimeTag` is by default equal to the index of each analysis 1513 in the dataset and `t` is defined as above. 1514 ''' 1515 for session in self.sessions: 1516 sdata = self.sessions[session]['data'] 1517 try: 1518 t0 = np.mean([r['TimeTag'] for r in sdata]) 1519 for r in sdata: 1520 r['t'] = r['TimeTag'] - t0 1521 except KeyError: 1522 t0 = (len(sdata)-1)/2 1523 for t,r in enumerate(sdata): 1524 r['t'] = t - t0 1525 1526 1527 def report(self): 1528 ''' 1529 Prints a report on the standardization fit. 1530 Only applicable after `D4xdata.standardize(method='pooled')`. 1531 ''' 1532 report_fit(self.standardization) 1533 1534 1535 def combine_samples(self, sample_groups): 1536 ''' 1537 Combine analyses of different samples to compute weighted average Δ4x 1538 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1539 dictionary. 1540 1541 Caution: samples are weighted by number of replicate analyses, which is a 1542 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1543 correlated analytical errors for one or more samples). 1544 1545 Returns a tuplet of: 1546 1547 + the list of group names 1548 + an array of the corresponding Δ4x values 1549 + the corresponding (co)variance matrix 1550 1551 **Parameters** 1552 1553 + `sample_groups`: a dictionary of the form: 1554 ```py 1555 {'group1': ['sample_1', 'sample_2'], 1556 'group2': ['sample_3', 'sample_4', 'sample_5']} 1557 ``` 1558 ''' 1559 1560 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1561 groups = sorted(sample_groups.keys()) 1562 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1563 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1564 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1565 W = np.array([ 1566 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1567 for j in groups]) 1568 D4x_new = W @ D4x_old 1569 CM_new = W @ CM_old @ W.T 1570 1571 return groups, D4x_new[:,0], CM_new 1572 1573 1574 @make_verbal 1575 def standardize(self, 1576 method = 'pooled', 1577 weighted_sessions = [], 1578 consolidate = True, 1579 consolidate_tables = False, 1580 consolidate_plots = False, 1581 constraints = {}, 1582 ): 1583 ''' 1584 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1585 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1586 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1587 i.e. that their true Δ4x value does not change between sessions, 1588 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1589 `'indep_sessions'`, the standardization processes each session independently, based only 1590 on anchors analyses. 1591 ''' 1592 1593 self.standardization_method = method 1594 self.assign_timestamps() 1595 1596 if method == 'pooled': 1597 if weighted_sessions: 1598 for session_group in weighted_sessions: 1599 if self._4x == '47': 1600 X = D47data([r for r in self if r['Session'] in session_group]) 1601 elif self._4x == '48': 1602 X = D48data([r for r in self if r['Session'] in session_group]) 1603 X.Nominal_D4x = self.Nominal_D4x.copy() 1604 X.refresh() 1605 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1606 w = np.sqrt(result.redchi) 1607 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1608 for r in X: 1609 r[f'wD{self._4x}raw'] *= w 1610 else: 1611 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1612 for r in self: 1613 r[f'wD{self._4x}raw'] = 1. 1614 1615 params = Parameters() 1616 for k,session in enumerate(self.sessions): 1617 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1618 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1619 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1620 s = pf(session) 1621 params.add(f'a_{s}', value = 0.9) 1622 params.add(f'b_{s}', value = 0.) 1623 params.add(f'c_{s}', value = -0.9) 1624 params.add(f'a2_{s}', value = 0., vary = self.sessions[session]['scrambling_drift']) 1625 params.add(f'b2_{s}', value = 0., vary = self.sessions[session]['slope_drift']) 1626 params.add(f'c2_{s}', value = 0., vary = self.sessions[session]['wg_drift']) 1627 for sample in self.unknowns: 1628 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1629 1630 for k in constraints: 1631 params[k].expr = constraints[k] 1632 1633 def residuals(p): 1634 R = [] 1635 for r in self: 1636 session = pf(r['Session']) 1637 sample = pf(r['Sample']) 1638 if r['Sample'] in self.Nominal_D4x: 1639 R += [ ( 1640 r[f'D{self._4x}raw'] - ( 1641 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1642 + p[f'b_{session}'] * r[f'd{self._4x}'] 1643 + p[f'c_{session}'] 1644 + r['t'] * ( 1645 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1646 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1647 + p[f'c2_{session}'] 1648 ) 1649 ) 1650 ) / r[f'wD{self._4x}raw'] ] 1651 else: 1652 R += [ ( 1653 r[f'D{self._4x}raw'] - ( 1654 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1655 + p[f'b_{session}'] * r[f'd{self._4x}'] 1656 + p[f'c_{session}'] 1657 + r['t'] * ( 1658 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1659 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1660 + p[f'c2_{session}'] 1661 ) 1662 ) 1663 ) / r[f'wD{self._4x}raw'] ] 1664 return R 1665 1666 M = Minimizer(residuals, params) 1667 result = M.least_squares() 1668 self.Nf = result.nfree 1669 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1670# if self.verbose: 1671# report_fit(result) 1672 1673 for r in self: 1674 s = pf(r["Session"]) 1675 a = result.params.valuesdict()[f'a_{s}'] 1676 b = result.params.valuesdict()[f'b_{s}'] 1677 c = result.params.valuesdict()[f'c_{s}'] 1678 a2 = result.params.valuesdict()[f'a2_{s}'] 1679 b2 = result.params.valuesdict()[f'b2_{s}'] 1680 c2 = result.params.valuesdict()[f'c2_{s}'] 1681 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']) 1682 1683 self.standardization = result 1684 1685 for session in self.sessions: 1686 self.sessions[session]['Np'] = 3 1687 for k in ['scrambling', 'slope', 'wg']: 1688 if self.sessions[session][f'{k}_drift']: 1689 self.sessions[session]['Np'] += 1 1690 1691 if consolidate: 1692 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1693 return result 1694 1695 1696 elif method == 'indep_sessions': 1697 1698 if weighted_sessions: 1699 for session_group in weighted_sessions: 1700 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1701 X.Nominal_D4x = self.Nominal_D4x.copy() 1702 X.refresh() 1703 # This is only done to assign r['wD47raw'] for r in X: 1704 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1705 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}') 1706 else: 1707 self.msg('All weights set to 1 ‰') 1708 for r in self: 1709 r[f'wD{self._4x}raw'] = 1 1710 1711 for session in self.sessions: 1712 s = self.sessions[session] 1713 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1714 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1715 s['Np'] = sum(p_active) 1716 sdata = s['data'] 1717 1718 A = np.array([ 1719 [ 1720 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1721 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1722 1 / r[f'wD{self._4x}raw'], 1723 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1724 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1725 r['t'] / r[f'wD{self._4x}raw'] 1726 ] 1727 for r in sdata if r['Sample'] in self.anchors 1728 ])[:,p_active] # only keep columns for the active parameters 1729 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]) 1730 s['Na'] = Y.size 1731 CM = linalg.inv(A.T @ A) 1732 bf = (CM @ A.T @ Y).T[0,:] 1733 k = 0 1734 for n,a in zip(p_names, p_active): 1735 if a: 1736 s[n] = bf[k] 1737# self.msg(f'{n} = {bf[k]}') 1738 k += 1 1739 else: 1740 s[n] = 0. 1741# self.msg(f'{n} = 0.0') 1742 1743 for r in sdata : 1744 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1745 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']) 1746 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1747 1748 s['CM'] = np.zeros((6,6)) 1749 i = 0 1750 k_active = [j for j,a in enumerate(p_active) if a] 1751 for j,a in enumerate(p_active): 1752 if a: 1753 s['CM'][j,k_active] = CM[i,:] 1754 i += 1 1755 1756 if not weighted_sessions: 1757 w = self.rmswd()['rmswd'] 1758 for r in self: 1759 r[f'wD{self._4x}'] *= w 1760 r[f'wD{self._4x}raw'] *= w 1761 for session in self.sessions: 1762 self.sessions[session]['CM'] *= w**2 1763 1764 for session in self.sessions: 1765 s = self.sessions[session] 1766 s['SE_a'] = s['CM'][0,0]**.5 1767 s['SE_b'] = s['CM'][1,1]**.5 1768 s['SE_c'] = s['CM'][2,2]**.5 1769 s['SE_a2'] = s['CM'][3,3]**.5 1770 s['SE_b2'] = s['CM'][4,4]**.5 1771 s['SE_c2'] = s['CM'][5,5]**.5 1772 1773 if not weighted_sessions: 1774 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1775 else: 1776 self.Nf = 0 1777 for sg in weighted_sessions: 1778 self.Nf += self.rmswd(sessions = sg)['Nf'] 1779 1780 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1781 1782 avgD4x = { 1783 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1784 for sample in self.samples 1785 } 1786 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1787 rD4x = (chi2/self.Nf)**.5 1788 self.repeatability[f'sigma_{self._4x}'] = rD4x 1789 1790 if consolidate: 1791 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1792 1793 1794 def standardization_error(self, session, d4x, D4x, t = 0): 1795 ''' 1796 Compute standardization error for a given session and 1797 (δ47, Δ47) composition. 1798 ''' 1799 a = self.sessions[session]['a'] 1800 b = self.sessions[session]['b'] 1801 c = self.sessions[session]['c'] 1802 a2 = self.sessions[session]['a2'] 1803 b2 = self.sessions[session]['b2'] 1804 c2 = self.sessions[session]['c2'] 1805 CM = self.sessions[session]['CM'] 1806 1807 x, y = D4x, d4x 1808 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1809# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1810 dxdy = -(b+b2*t) / (a+a2*t) 1811 dxdz = 1. / (a+a2*t) 1812 dxda = -x / (a+a2*t) 1813 dxdb = -y / (a+a2*t) 1814 dxdc = -1. / (a+a2*t) 1815 dxda2 = -x * a2 / (a+a2*t) 1816 dxdb2 = -y * t / (a+a2*t) 1817 dxdc2 = -t / (a+a2*t) 1818 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1819 sx = (V @ CM @ V.T) ** .5 1820 return sx 1821 1822 1823 @make_verbal 1824 def summary(self, 1825 dir = 'output', 1826 filename = None, 1827 save_to_file = True, 1828 print_out = True, 1829 ): 1830 ''' 1831 Print out an/or save to disk a summary of the standardization results. 1832 1833 **Parameters** 1834 1835 + `dir`: the directory in which to save the table 1836 + `filename`: the name to the csv file to write to 1837 + `save_to_file`: whether to save the table to disk 1838 + `print_out`: whether to print out the table 1839 ''' 1840 1841 out = [] 1842 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1843 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])})"]] 1844 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1845 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1846 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1847 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1848 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1849 out += [['Model degrees of freedom', f"{self.Nf}"]] 1850 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1851 out += [['Standardization method', self.standardization_method]] 1852 1853 if save_to_file: 1854 if not os.path.exists(dir): 1855 os.makedirs(dir) 1856 if filename is None: 1857 filename = f'D{self._4x}_summary.csv' 1858 with open(f'{dir}/{filename}', 'w') as fid: 1859 fid.write(make_csv(out)) 1860 if print_out: 1861 self.msg('\n' + pretty_table(out, header = 0)) 1862 1863 1864 @make_verbal 1865 def table_of_sessions(self, 1866 dir = 'output', 1867 filename = None, 1868 save_to_file = True, 1869 print_out = True, 1870 output = None, 1871 ): 1872 ''' 1873 Print out an/or save to disk a table of sessions. 1874 1875 **Parameters** 1876 1877 + `dir`: the directory in which to save the table 1878 + `filename`: the name to the csv file to write to 1879 + `save_to_file`: whether to save the table to disk 1880 + `print_out`: whether to print out the table 1881 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1882 if set to `'raw'`: return a list of list of strings 1883 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1884 ''' 1885 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1886 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1887 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1888 1889 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']] 1890 if include_a2: 1891 out[-1] += ['a2 ± SE'] 1892 if include_b2: 1893 out[-1] += ['b2 ± SE'] 1894 if include_c2: 1895 out[-1] += ['c2 ± SE'] 1896 for session in self.sessions: 1897 out += [[ 1898 session, 1899 f"{self.sessions[session]['Na']}", 1900 f"{self.sessions[session]['Nu']}", 1901 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1902 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1903 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1904 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1905 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1906 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1907 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1908 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1909 ]] 1910 if include_a2: 1911 if self.sessions[session]['scrambling_drift']: 1912 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1913 else: 1914 out[-1] += [''] 1915 if include_b2: 1916 if self.sessions[session]['slope_drift']: 1917 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1918 else: 1919 out[-1] += [''] 1920 if include_c2: 1921 if self.sessions[session]['wg_drift']: 1922 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1923 else: 1924 out[-1] += [''] 1925 1926 if save_to_file: 1927 if not os.path.exists(dir): 1928 os.makedirs(dir) 1929 if filename is None: 1930 filename = f'D{self._4x}_sessions.csv' 1931 with open(f'{dir}/{filename}', 'w') as fid: 1932 fid.write(make_csv(out)) 1933 if print_out: 1934 self.msg('\n' + pretty_table(out)) 1935 if output == 'raw': 1936 return out 1937 elif output == 'pretty': 1938 return pretty_table(out) 1939 1940 1941 @make_verbal 1942 def table_of_analyses( 1943 self, 1944 dir = 'output', 1945 filename = None, 1946 save_to_file = True, 1947 print_out = True, 1948 output = None, 1949 ): 1950 ''' 1951 Print out an/or save to disk a table of analyses. 1952 1953 **Parameters** 1954 1955 + `dir`: the directory in which to save the table 1956 + `filename`: the name to the csv file to write to 1957 + `save_to_file`: whether to save the table to disk 1958 + `print_out`: whether to print out the table 1959 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1960 if set to `'raw'`: return a list of list of strings 1961 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1962 ''' 1963 1964 out = [['UID','Session','Sample']] 1965 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}] 1966 for f in extra_fields: 1967 out[-1] += [f[0]] 1968 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 1969 for r in self: 1970 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 1971 for f in extra_fields: 1972 out[-1] += [f"{r[f[0]]:{f[1]}}"] 1973 out[-1] += [ 1974 f"{r['d13Cwg_VPDB']:.3f}", 1975 f"{r['d18Owg_VSMOW']:.3f}", 1976 f"{r['d45']:.6f}", 1977 f"{r['d46']:.6f}", 1978 f"{r['d47']:.6f}", 1979 f"{r['d48']:.6f}", 1980 f"{r['d49']:.6f}", 1981 f"{r['d13C_VPDB']:.6f}", 1982 f"{r['d18O_VSMOW']:.6f}", 1983 f"{r['D47raw']:.6f}", 1984 f"{r['D48raw']:.6f}", 1985 f"{r['D49raw']:.6f}", 1986 f"{r[f'D{self._4x}']:.6f}" 1987 ] 1988 if save_to_file: 1989 if not os.path.exists(dir): 1990 os.makedirs(dir) 1991 if filename is None: 1992 filename = f'D{self._4x}_analyses.csv' 1993 with open(f'{dir}/{filename}', 'w') as fid: 1994 fid.write(make_csv(out)) 1995 if print_out: 1996 self.msg('\n' + pretty_table(out)) 1997 return out 1998 1999 @make_verbal 2000 def covar_table( 2001 self, 2002 correl = False, 2003 dir = 'output', 2004 filename = None, 2005 save_to_file = True, 2006 print_out = True, 2007 output = None, 2008 ): 2009 ''' 2010 Print out, save to disk and/or return the variance-covariance matrix of D4x 2011 for all unknown samples. 2012 2013 **Parameters** 2014 2015 + `dir`: the directory in which to save the csv 2016 + `filename`: the name of the csv file to write to 2017 + `save_to_file`: whether to save the csv 2018 + `print_out`: whether to print out the matrix 2019 + `output`: if set to `'pretty'`: return a pretty text matrix (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 samples = sorted([u for u in self.unknowns]) 2024 out = [[''] + samples] 2025 for s1 in samples: 2026 out.append([s1]) 2027 for s2 in samples: 2028 if correl: 2029 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2030 else: 2031 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2032 2033 if save_to_file: 2034 if not os.path.exists(dir): 2035 os.makedirs(dir) 2036 if filename is None: 2037 if correl: 2038 filename = f'D{self._4x}_correl.csv' 2039 else: 2040 filename = f'D{self._4x}_covar.csv' 2041 with open(f'{dir}/{filename}', 'w') as fid: 2042 fid.write(make_csv(out)) 2043 if print_out: 2044 self.msg('\n'+pretty_table(out)) 2045 if output == 'raw': 2046 return out 2047 elif output == 'pretty': 2048 return pretty_table(out) 2049 2050 @make_verbal 2051 def table_of_samples( 2052 self, 2053 dir = 'output', 2054 filename = None, 2055 save_to_file = True, 2056 print_out = True, 2057 output = None, 2058 ): 2059 ''' 2060 Print out, save to disk and/or return a table of samples. 2061 2062 **Parameters** 2063 2064 + `dir`: the directory in which to save the csv 2065 + `filename`: the name of the csv file to write to 2066 + `save_to_file`: whether to save the csv 2067 + `print_out`: whether to print out the table 2068 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2069 if set to `'raw'`: return a list of list of strings 2070 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2071 ''' 2072 2073 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2074 for sample in self.anchors: 2075 out += [[ 2076 f"{sample}", 2077 f"{self.samples[sample]['N']}", 2078 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2079 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2080 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2081 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2082 ]] 2083 for sample in self.unknowns: 2084 out += [[ 2085 f"{sample}", 2086 f"{self.samples[sample]['N']}", 2087 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2088 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2089 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2090 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2091 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2092 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2093 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2094 ]] 2095 if save_to_file: 2096 if not os.path.exists(dir): 2097 os.makedirs(dir) 2098 if filename is None: 2099 filename = f'D{self._4x}_samples.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 2110 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2111 ''' 2112 Generate session plots and save them to disk. 2113 2114 **Parameters** 2115 2116 + `dir`: the directory in which to save the plots 2117 + `figsize`: the width and height (in inches) of each plot 2118 ''' 2119 if not os.path.exists(dir): 2120 os.makedirs(dir) 2121 2122 for session in self.sessions: 2123 sp = self.plot_single_session(session, xylimits = 'constant') 2124 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2125 ppl.close(sp.fig) 2126 2127 2128 @make_verbal 2129 def consolidate_samples(self): 2130 ''' 2131 Compile various statistics for each sample. 2132 2133 For each anchor sample: 2134 2135 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2136 + `SE_D47` or `SE_D48`: set to zero by definition 2137 2138 For each unknown sample: 2139 2140 + `D47` or `D48`: the standardized Δ4x value for this unknown 2141 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2142 2143 For each anchor and unknown: 2144 2145 + `N`: the total number of analyses of this sample 2146 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2147 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2148 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2149 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2150 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2151 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2152 ''' 2153 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2154 for sample in self.samples: 2155 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2156 if self.samples[sample]['N'] > 1: 2157 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2158 2159 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2160 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2161 2162 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2163 if len(D4x_pop) > 2: 2164 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2165 2166 if self.standardization_method == 'pooled': 2167 for sample in self.anchors: 2168 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2169 self.samples[sample][f'SE_D{self._4x}'] = 0. 2170 for sample in self.unknowns: 2171 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2172 try: 2173 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2174 except ValueError: 2175 # when `sample` is constrained by self.standardize(constraints = {...}), 2176 # it is no longer listed in self.standardization.var_names. 2177 # Temporary fix: define SE as zero for now 2178 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2179 2180 elif self.standardization_method == 'indep_sessions': 2181 for sample in self.anchors: 2182 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2183 self.samples[sample][f'SE_D{self._4x}'] = 0. 2184 for sample in self.unknowns: 2185 self.msg(f'Consolidating sample {sample}') 2186 self.unknowns[sample][f'session_D{self._4x}'] = {} 2187 session_avg = [] 2188 for session in self.sessions: 2189 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2190 if sdata: 2191 self.msg(f'{sample} found in session {session}') 2192 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2193 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2194 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2195 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2196 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2197 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2198 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2199 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2200 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2201 wsum = sum([weights[s] for s in weights]) 2202 for s in weights: 2203 self.unknowns[sample][f'session_D{self._4x}'][s] += [self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 / wsum] 2204 2205 2206 def consolidate_sessions(self): 2207 ''' 2208 Compute various statistics for each session. 2209 2210 + `Na`: Number of anchor analyses in the session 2211 + `Nu`: Number of unknown analyses in the session 2212 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2213 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2214 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2215 + `a`: scrambling factor 2216 + `b`: compositional slope 2217 + `c`: WG offset 2218 + `SE_a`: Model stadard erorr of `a` 2219 + `SE_b`: Model stadard erorr of `b` 2220 + `SE_c`: Model stadard erorr of `c` 2221 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2222 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2223 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2224 + `a2`: scrambling factor drift 2225 + `b2`: compositional slope drift 2226 + `c2`: WG offset drift 2227 + `Np`: Number of standardization parameters to fit 2228 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2229 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2230 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2231 ''' 2232 for session in self.sessions: 2233 if 'd13Cwg_VPDB' not in self.sessions[session]: 2234 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2235 if 'd18Owg_VSMOW' not in self.sessions[session]: 2236 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2237 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2238 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2239 2240 self.msg(f'Computing repeatabilities for session {session}') 2241 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2242 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2243 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2244 2245 if self.standardization_method == 'pooled': 2246 for session in self.sessions: 2247 2248 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2249 i = self.standardization.var_names.index(f'a_{pf(session)}') 2250 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2251 2252 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2253 i = self.standardization.var_names.index(f'b_{pf(session)}') 2254 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2255 2256 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2257 i = self.standardization.var_names.index(f'c_{pf(session)}') 2258 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2259 2260 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2261 if self.sessions[session]['scrambling_drift']: 2262 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2263 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2264 else: 2265 self.sessions[session]['SE_a2'] = 0. 2266 2267 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2268 if self.sessions[session]['slope_drift']: 2269 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2270 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2271 else: 2272 self.sessions[session]['SE_b2'] = 0. 2273 2274 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2275 if self.sessions[session]['wg_drift']: 2276 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2277 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2278 else: 2279 self.sessions[session]['SE_c2'] = 0. 2280 2281 i = self.standardization.var_names.index(f'a_{pf(session)}') 2282 j = self.standardization.var_names.index(f'b_{pf(session)}') 2283 k = self.standardization.var_names.index(f'c_{pf(session)}') 2284 CM = np.zeros((6,6)) 2285 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2286 try: 2287 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2288 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2289 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2290 try: 2291 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2292 CM[3,4] = self.standardization.covar[i2,j2] 2293 CM[4,3] = self.standardization.covar[j2,i2] 2294 except ValueError: 2295 pass 2296 try: 2297 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2298 CM[3,5] = self.standardization.covar[i2,k2] 2299 CM[5,3] = self.standardization.covar[k2,i2] 2300 except ValueError: 2301 pass 2302 except ValueError: 2303 pass 2304 try: 2305 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2306 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2307 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2308 try: 2309 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2310 CM[4,5] = self.standardization.covar[j2,k2] 2311 CM[5,4] = self.standardization.covar[k2,j2] 2312 except ValueError: 2313 pass 2314 except ValueError: 2315 pass 2316 try: 2317 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2318 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2319 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2320 except ValueError: 2321 pass 2322 2323 self.sessions[session]['CM'] = CM 2324 2325 elif self.standardization_method == 'indep_sessions': 2326 pass # Not implemented yet 2327 2328 2329 @make_verbal 2330 def repeatabilities(self): 2331 ''' 2332 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2333 (for all samples, for anchors, and for unknowns). 2334 ''' 2335 self.msg('Computing reproducibilities for all sessions') 2336 2337 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2338 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2339 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2340 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2341 self.repeatability[f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', samples = 'all samples') 2342 2343 2344 @make_verbal 2345 def consolidate(self, tables = True, plots = True): 2346 ''' 2347 Collect information about samples, sessions and repeatabilities. 2348 ''' 2349 self.consolidate_samples() 2350 self.consolidate_sessions() 2351 self.repeatabilities() 2352 2353 if tables: 2354 self.summary() 2355 self.table_of_sessions() 2356 self.table_of_analyses() 2357 self.table_of_samples() 2358 2359 if plots: 2360 self.plot_sessions() 2361 2362 2363 @make_verbal 2364 def rmswd(self, 2365 samples = 'all samples', 2366 sessions = 'all sessions', 2367 ): 2368 ''' 2369 Compute the χ2, root mean squared weighted deviation 2370 (i.e. reduced χ2), and corresponding degrees of freedom of the 2371 Δ4x values for samples in `samples` and sessions in `sessions`. 2372 2373 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2374 ''' 2375 if samples == 'all samples': 2376 mysamples = [k for k in self.samples] 2377 elif samples == 'anchors': 2378 mysamples = [k for k in self.anchors] 2379 elif samples == 'unknowns': 2380 mysamples = [k for k in self.unknowns] 2381 else: 2382 mysamples = samples 2383 2384 if sessions == 'all sessions': 2385 sessions = [k for k in self.sessions] 2386 2387 chisq, Nf = 0, 0 2388 for sample in mysamples : 2389 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2390 if len(G) > 1 : 2391 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2392 Nf += (len(G) - 1) 2393 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2394 r = (chisq / Nf)**.5 if Nf > 0 else 0 2395 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2396 return {'rmswd': r, 'chisq': chisq, 'Nf': Nf} 2397 2398 2399 @make_verbal 2400 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2401 ''' 2402 Compute the repeatability of `[r[key] for r in self]` 2403 ''' 2404 # NB: it's debatable whether rD47 should be computed 2405 # with Nf = len(self)-len(self.samples) instead of 2406 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2407 2408 if samples == 'all samples': 2409 mysamples = [k for k in self.samples] 2410 elif samples == 'anchors': 2411 mysamples = [k for k in self.anchors] 2412 elif samples == 'unknowns': 2413 mysamples = [k for k in self.unknowns] 2414 else: 2415 mysamples = samples 2416 2417 if sessions == 'all sessions': 2418 sessions = [k for k in self.sessions] 2419 2420 if key in ['D47', 'D48']: 2421 chisq, Nf = 0, 0 2422 for sample in mysamples : 2423 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2424 if len(X) > 1 : 2425 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2426 if sample in self.unknowns: 2427 Nf += len(X) - 1 2428 else: 2429 Nf += len(X) 2430 if samples in ['anchors', 'all samples']: 2431 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2432 r = (chisq / Nf)**.5 if Nf > 0 else 0 2433 2434 else: # if key not in ['D47', 'D48'] 2435 chisq, Nf = 0, 0 2436 for sample in mysamples : 2437 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2438 if len(X) > 1 : 2439 Nf += len(X) - 1 2440 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2441 r = (chisq / Nf)**.5 if Nf > 0 else 0 2442 2443 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2444 return r 2445 2446 def sample_average(self, samples, weights = 'equal', normalize = True): 2447 ''' 2448 Weighted average Δ4x value of a group of samples, accounting for covariance. 2449 2450 Returns the weighed average Δ4x value and associated SE 2451 of a group of samples. Weights are equal by default. If `normalize` is 2452 true, `weights` will be rescaled so that their sum equals 1. 2453 2454 **Examples** 2455 2456 ```python 2457 self.sample_average(['X','Y'], [1, 2]) 2458 ``` 2459 2460 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2461 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2462 values of samples X and Y, respectively. 2463 2464 ```python 2465 self.sample_average(['X','Y'], [1, -1], normalize = False) 2466 ``` 2467 2468 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2469 ''' 2470 if weights == 'equal': 2471 weights = [1/len(samples)] * len(samples) 2472 2473 if normalize: 2474 s = sum(weights) 2475 if s: 2476 weights = [w/s for w in weights] 2477 2478 try: 2479# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2480# C = self.standardization.covar[indices,:][:,indices] 2481 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2482 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2483 return correlated_sum(X, C, weights) 2484 except ValueError: 2485 return (0., 0.) 2486 2487 2488 def sample_D4x_covar(self, sample1, sample2 = None): 2489 ''' 2490 Covariance between Δ4x values of samples 2491 2492 Returns the error covariance between the average Δ4x values of two 2493 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2494 returns the Δ4x variance for that sample. 2495 ''' 2496 if sample2 is None: 2497 sample2 = sample1 2498 if self.standardization_method == 'pooled': 2499 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2500 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2501 return self.standardization.covar[i, j] 2502 elif self.standardization_method == 'indep_sessions': 2503 if sample1 == sample2: 2504 return self.samples[sample1][f'SE_D{self._4x}']**2 2505 else: 2506 c = 0 2507 for session in self.sessions: 2508 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2509 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2510 if sdata1 and sdata2: 2511 a = self.sessions[session]['a'] 2512 # !! TODO: CM below does not account for temporal changes in standardization parameters 2513 CM = self.sessions[session]['CM'][:3,:3] 2514 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2515 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2516 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2517 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2518 c += ( 2519 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2520 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2521 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2522 @ CM 2523 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2524 ) / a**2 2525 return float(c) 2526 2527 def sample_D4x_correl(self, sample1, sample2 = None): 2528 ''' 2529 Correlation between Δ4x errors of samples 2530 2531 Returns the error correlation between the average Δ4x values of two samples. 2532 ''' 2533 if sample2 is None or sample2 == sample1: 2534 return 1. 2535 return ( 2536 self.sample_D4x_covar(sample1, sample2) 2537 / self.unknowns[sample1][f'SE_D{self._4x}'] 2538 / self.unknowns[sample2][f'SE_D{self._4x}'] 2539 ) 2540 2541 def plot_single_session(self, 2542 session, 2543 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2544 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2545 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2546 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2547 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2548 xylimits = 'free', # | 'constant' 2549 x_label = None, 2550 y_label = None, 2551 error_contour_interval = 'auto', 2552 fig = 'new', 2553 ): 2554 ''' 2555 Generate plot for a single session 2556 ''' 2557 if x_label is None: 2558 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2559 if y_label is None: 2560 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2561 2562 out = _SessionPlot() 2563 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2564 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2565 2566 if fig == 'new': 2567 out.fig = ppl.figure(figsize = (6,6)) 2568 ppl.subplots_adjust(.1,.1,.9,.9) 2569 2570 out.anchor_analyses, = ppl.plot( 2571 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2572 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2573 **kw_plot_anchors) 2574 out.unknown_analyses, = ppl.plot( 2575 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2576 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2577 **kw_plot_unknowns) 2578 out.anchor_avg = ppl.plot( 2579 np.array([ np.array([ 2580 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2581 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2582 ]) for sample in anchors]).T, 2583 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2584 **kw_plot_anchor_avg) 2585 out.unknown_avg = ppl.plot( 2586 np.array([ np.array([ 2587 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2588 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2589 ]) for sample in unknowns]).T, 2590 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2591 **kw_plot_unknown_avg) 2592 if xylimits == 'constant': 2593 x = [r[f'd{self._4x}'] for r in self] 2594 y = [r[f'D{self._4x}'] for r in self] 2595 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2596 w, h = x2-x1, y2-y1 2597 x1 -= w/20 2598 x2 += w/20 2599 y1 -= h/20 2600 y2 += h/20 2601 ppl.axis([x1, x2, y1, y2]) 2602 elif xylimits == 'free': 2603 x1, x2, y1, y2 = ppl.axis() 2604 else: 2605 x1, x2, y1, y2 = ppl.axis(xylimits) 2606 2607 if error_contour_interval != 'none': 2608 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2609 XI,YI = np.meshgrid(xi, yi) 2610 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2611 if error_contour_interval == 'auto': 2612 rng = np.max(SI) - np.min(SI) 2613 if rng <= 0.01: 2614 cinterval = 0.001 2615 elif rng <= 0.03: 2616 cinterval = 0.004 2617 elif rng <= 0.1: 2618 cinterval = 0.01 2619 elif rng <= 0.3: 2620 cinterval = 0.03 2621 elif rng <= 1.: 2622 cinterval = 0.1 2623 else: 2624 cinterval = 0.5 2625 else: 2626 cinterval = error_contour_interval 2627 2628 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2629 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2630 out.clabel = ppl.clabel(out.contour) 2631 2632 ppl.xlabel(x_label) 2633 ppl.ylabel(y_label) 2634 ppl.title(session, weight = 'bold') 2635 ppl.grid(alpha = .2) 2636 out.ax = ppl.gca() 2637 2638 return out 2639 2640 def plot_residuals( 2641 self, 2642 hist = False, 2643 binwidth = 2/3, 2644 dir = 'output', 2645 filename = None, 2646 highlight = [], 2647 colors = None, 2648 figsize = None, 2649 ): 2650 ''' 2651 Plot residuals of each analysis as a function of time (actually, as a function of 2652 the order of analyses in the `D4xdata` object) 2653 2654 + `hist`: whether to add a histogram of residuals 2655 + `histbins`: specify bin edges for the histogram 2656 + `dir`: the directory in which to save the plot 2657 + `highlight`: a list of samples to highlight 2658 + `colors`: a dict of `{<sample>: <color>}` for all samples 2659 + `figsize`: (width, height) of figure 2660 ''' 2661 # Layout 2662 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2663 if hist: 2664 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2665 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2666 else: 2667 ppl.subplots_adjust(.08,.05,.78,.8) 2668 ax1 = ppl.subplot(111) 2669 2670 # Colors 2671 N = len(self.anchors) 2672 if colors is None: 2673 if len(highlight) > 0: 2674 Nh = len(highlight) 2675 if Nh == 1: 2676 colors = {highlight[0]: (0,0,0)} 2677 elif Nh == 3: 2678 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2679 elif Nh == 4: 2680 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2681 else: 2682 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2683 else: 2684 if N == 3: 2685 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2686 elif N == 4: 2687 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2688 else: 2689 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2690 2691 ppl.sca(ax1) 2692 2693 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2694 2695 session = self[0]['Session'] 2696 x1 = 0 2697# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2698 x_sessions = {} 2699 one_or_more_singlets = False 2700 one_or_more_multiplets = False 2701 multiplets = set() 2702 for k,r in enumerate(self): 2703 if r['Session'] != session: 2704 x2 = k-1 2705 x_sessions[session] = (x1+x2)/2 2706 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2707 session = r['Session'] 2708 x1 = k 2709 singlet = len(self.samples[r['Sample']]['data']) == 1 2710 if not singlet: 2711 multiplets.add(r['Sample']) 2712 if r['Sample'] in self.unknowns: 2713 if singlet: 2714 one_or_more_singlets = True 2715 else: 2716 one_or_more_multiplets = True 2717 kw = dict( 2718 marker = 'x' if singlet else '+', 2719 ms = 4 if singlet else 5, 2720 ls = 'None', 2721 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2722 mew = 1, 2723 alpha = 0.2 if singlet else 1, 2724 ) 2725 if highlight and r['Sample'] not in highlight: 2726 kw['alpha'] = 0.2 2727 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2728 x2 = k 2729 x_sessions[session] = (x1+x2)/2 2730 2731 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2732 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2733 if not hist: 2734 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2735 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') 2736 2737 xmin, xmax, ymin, ymax = ppl.axis() 2738 for s in x_sessions: 2739 ppl.text( 2740 x_sessions[s], 2741 ymax +1, 2742 s, 2743 va = 'bottom', 2744 **( 2745 dict(ha = 'center') 2746 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2747 else dict(ha = 'left', rotation = 45) 2748 ) 2749 ) 2750 2751 if hist: 2752 ppl.sca(ax2) 2753 2754 for s in colors: 2755 kw['marker'] = '+' 2756 kw['ms'] = 5 2757 kw['mec'] = colors[s] 2758 kw['label'] = s 2759 kw['alpha'] = 1 2760 ppl.plot([], [], **kw) 2761 2762 kw['mec'] = (0,0,0) 2763 2764 if one_or_more_singlets: 2765 kw['marker'] = 'x' 2766 kw['ms'] = 4 2767 kw['alpha'] = .2 2768 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2769 ppl.plot([], [], **kw) 2770 2771 if one_or_more_multiplets: 2772 kw['marker'] = '+' 2773 kw['ms'] = 4 2774 kw['alpha'] = 1 2775 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2776 ppl.plot([], [], **kw) 2777 2778 if hist: 2779 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2780 else: 2781 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2782 leg.set_zorder(-1000) 2783 2784 ppl.sca(ax1) 2785 2786 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2787 ppl.xticks([]) 2788 ppl.axis([-1, len(self), None, None]) 2789 2790 if hist: 2791 ppl.sca(ax2) 2792 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2793 ppl.hist( 2794 X, 2795 orientation = 'horizontal', 2796 histtype = 'stepfilled', 2797 ec = [.4]*3, 2798 fc = [.25]*3, 2799 alpha = .25, 2800 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2801 ) 2802 ppl.axis([None, None, ymin, ymax]) 2803 ppl.text(0, 0, 2804 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2805 size = 8, 2806 alpha = 1, 2807 va = 'center', 2808 ha = 'left', 2809 ) 2810 2811 ppl.xticks([]) 2812 ppl.yticks([]) 2813# ax2.spines['left'].set_visible(False) 2814 ax2.spines['right'].set_visible(False) 2815 ax2.spines['top'].set_visible(False) 2816 ax2.spines['bottom'].set_visible(False) 2817 2818 2819 if not os.path.exists(dir): 2820 os.makedirs(dir) 2821 if filename is None: 2822 return fig 2823 elif filename == '': 2824 filename = f'D{self._4x}_residuals.pdf' 2825 ppl.savefig(f'{dir}/{filename}') 2826 ppl.close(fig) 2827 2828 2829 def simulate(self, *args, **kwargs): 2830 ''' 2831 Legacy function with warning message pointing to `virtual_data()` 2832 ''' 2833 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()') 2834 2835 def plot_distribution_of_analyses( 2836 self, 2837 dir = 'output', 2838 filename = None, 2839 vs_time = False, 2840 figsize = (6,4), 2841 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2842 output = None, 2843 ): 2844 ''' 2845 Plot temporal distribution of all analyses in the data set. 2846 2847 **Parameters** 2848 2849 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2850 ''' 2851 2852 asamples = [s for s in self.anchors] 2853 usamples = [s for s in self.unknowns] 2854 if output is None or output == 'fig': 2855 fig = ppl.figure(figsize = figsize) 2856 ppl.subplots_adjust(*subplots_adjust) 2857 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2858 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2859 Xmax += (Xmax-Xmin)/40 2860 Xmin -= (Xmax-Xmin)/41 2861 for k, s in enumerate(asamples + usamples): 2862 if vs_time: 2863 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2864 else: 2865 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2866 Y = [-k for x in X] 2867 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2868 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2869 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2870 ppl.axis([Xmin, Xmax, -k-1, 1]) 2871 ppl.xlabel('\ntime') 2872 ppl.gca().annotate('', 2873 xy = (0.6, -0.02), 2874 xycoords = 'axes fraction', 2875 xytext = (.4, -0.02), 2876 arrowprops = dict(arrowstyle = "->", color = 'k'), 2877 ) 2878 2879 2880 x2 = -1 2881 for session in self.sessions: 2882 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2883 if vs_time: 2884 ppl.axvline(x1, color = 'k', lw = .75) 2885 if x2 > -1: 2886 if not vs_time: 2887 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2888 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2889# from xlrd import xldate_as_datetime 2890# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2891 if vs_time: 2892 ppl.axvline(x2, color = 'k', lw = .75) 2893 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2894 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2895 2896 ppl.xticks([]) 2897 ppl.yticks([]) 2898 2899 if output is None: 2900 if not os.path.exists(dir): 2901 os.makedirs(dir) 2902 if filename == None: 2903 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2904 ppl.savefig(f'{dir}/{filename}') 2905 ppl.close(fig) 2906 elif output == 'ax': 2907 return ppl.gca() 2908 elif output == 'fig': 2909 return fig 2910 2911 2912class D47data(D4xdata): 2913 ''' 2914 Store and process data for a large set of Δ47 analyses, 2915 usually comprising more than one analytical session. 2916 ''' 2917 2918 Nominal_D4x = { 2919 'ETH-1': 0.2052, 2920 'ETH-2': 0.2085, 2921 'ETH-3': 0.6132, 2922 'ETH-4': 0.4511, 2923 'IAEA-C1': 0.3018, 2924 'IAEA-C2': 0.6409, 2925 'MERCK': 0.5135, 2926 } # I-CDES (Bernasconi et al., 2021) 2927 ''' 2928 Nominal Δ47 values assigned to the Δ47 anchor samples, used by 2929 `D47data.standardize()` to normalize unknown samples to an absolute Δ47 2930 reference frame. 2931 2932 By default equal to (after [Bernasconi et al. (2021)](https://doi.org/10.1029/2020GC009588)): 2933 ```py 2934 { 2935 'ETH-1' : 0.2052, 2936 'ETH-2' : 0.2085, 2937 'ETH-3' : 0.6132, 2938 'ETH-4' : 0.4511, 2939 'IAEA-C1' : 0.3018, 2940 'IAEA-C2' : 0.6409, 2941 'MERCK' : 0.5135, 2942 } 2943 ``` 2944 ''' 2945 2946 2947 @property 2948 def Nominal_D47(self): 2949 return self.Nominal_D4x 2950 2951 2952 @Nominal_D47.setter 2953 def Nominal_D47(self, new): 2954 self.Nominal_D4x = dict(**new) 2955 self.refresh() 2956 2957 2958 def __init__(self, l = [], **kwargs): 2959 ''' 2960 **Parameters:** same as `D4xdata.__init__()` 2961 ''' 2962 D4xdata.__init__(self, l = l, mass = '47', **kwargs) 2963 2964 2965 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 2966 ''' 2967 Find all samples for which `Teq` is specified, compute equilibrium Δ47 2968 value for that temperature, and add treat these samples as additional anchors. 2969 2970 **Parameters** 2971 2972 + `fCo2eqD47`: Which CO2 equilibrium law to use 2973 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 2974 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 2975 + `priority`: if `replace`: forget old anchors and only use the new ones; 2976 if `new`: keep pre-existing anchors but update them in case of conflict 2977 between old and new Δ47 values; 2978 if `old`: keep pre-existing anchors but preserve their original Δ47 2979 values in case of conflict. 2980 ''' 2981 f = { 2982 'petersen': fCO2eqD47_Petersen, 2983 'wang': fCO2eqD47_Wang, 2984 }[fCo2eqD47] 2985 foo = {} 2986 for r in self: 2987 if 'Teq' in r: 2988 if r['Sample'] in foo: 2989 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 2990 else: 2991 foo[r['Sample']] = f(r['Teq']) 2992 else: 2993 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 2994 2995 if priority == 'replace': 2996 self.Nominal_D47 = {} 2997 for s in foo: 2998 if priority != 'old' or s not in self.Nominal_D47: 2999 self.Nominal_D47[s] = foo[s] 3000 3001 3002 3003 3004class D48data(D4xdata): 3005 ''' 3006 Store and process data for a large set of Δ48 analyses, 3007 usually comprising more than one analytical session. 3008 ''' 3009 3010 Nominal_D4x = { 3011 'ETH-1': 0.138, 3012 'ETH-2': 0.138, 3013 'ETH-3': 0.270, 3014 'ETH-4': 0.223, 3015 'GU-1': -0.419, 3016 } # (Fiebig et al., 2019, 2021) 3017 ''' 3018 Nominal Δ48 values assigned to the Δ48 anchor samples, used by 3019 `D48data.standardize()` to normalize unknown samples to an absolute Δ48 3020 reference frame. 3021 3022 By default equal to (after [Fiebig et al. (2019)](https://doi.org/10.1016/j.chemgeo.2019.05.019), 3023 Fiebig et al. (in press)): 3024 3025 ```py 3026 { 3027 'ETH-1' : 0.138, 3028 'ETH-2' : 0.138, 3029 'ETH-3' : 0.270, 3030 'ETH-4' : 0.223, 3031 'GU-1' : -0.419, 3032 } 3033 ``` 3034 ''' 3035 3036 3037 @property 3038 def Nominal_D48(self): 3039 return self.Nominal_D4x 3040 3041 3042 @Nominal_D48.setter 3043 def Nominal_D48(self, new): 3044 self.Nominal_D4x = dict(**new) 3045 self.refresh() 3046 3047 3048 def __init__(self, l = [], **kwargs): 3049 ''' 3050 **Parameters:** same as `D4xdata.__init__()` 3051 ''' 3052 D4xdata.__init__(self, l = l, mass = '48', **kwargs) 3053 3054 3055class _SessionPlot(): 3056 ''' 3057 Simple placeholder class 3058 ''' 3059 def __init__(self): 3060 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']]
)
850class D4xdata(list): 851 ''' 852 Store and process data for a large set of Δ47 and/or Δ48 853 analyses, usually comprising more than one analytical session. 854 ''' 855 856 ### 17O CORRECTION PARAMETERS 857 R13_VPDB = 0.01118 # (Chang & Li, 1990) 858 ''' 859 Absolute (13C/12C) ratio of VPDB. 860 By default equal to 0.01118 ([Chang & Li, 1990](http://www.cnki.com.cn/Article/CJFDTotal-JXTW199004006.htm)) 861 ''' 862 863 R18_VSMOW = 0.0020052 # (Baertschi, 1976) 864 ''' 865 Absolute (18O/16C) ratio of VSMOW. 866 By default equal to 0.0020052 ([Baertschi, 1976](https://doi.org/10.1016/0012-821X(76)90115-1)) 867 ''' 868 869 LAMBDA_17 = 0.528 # (Barkan & Luz, 2005) 870 ''' 871 Mass-dependent exponent for triple oxygen isotopes. 872 By default equal to 0.528 ([Barkan & Luz, 2005](https://doi.org/10.1002/rcm.2250)) 873 ''' 874 875 R17_VSMOW = 0.00038475 # (Assonov & Brenninkmeijer, 2003, rescaled to R13_VPDB) 876 ''' 877 Absolute (17O/16C) ratio of VSMOW. 878 By default equal to 0.00038475 879 ([Assonov & Brenninkmeijer, 2003](https://dx.doi.org/10.1002/rcm.1011), 880 rescaled to `R13_VPDB`) 881 ''' 882 883 R18_VPDB = R18_VSMOW * 1.03092 884 ''' 885 Absolute (18O/16C) ratio of VPDB. 886 By definition equal to `R18_VSMOW * 1.03092`. 887 ''' 888 889 R17_VPDB = R17_VSMOW * 1.03092 ** LAMBDA_17 890 ''' 891 Absolute (17O/16C) ratio of VPDB. 892 By definition equal to `R17_VSMOW * 1.03092 ** LAMBDA_17`. 893 ''' 894 895 LEVENE_REF_SAMPLE = 'ETH-3' 896 ''' 897 After the Δ4x standardization step, each sample is tested to 898 assess whether the Δ4x variance within all analyses for that 899 sample differs significantly from that observed for a given reference 900 sample (using [Levene's test](https://en.wikipedia.org/wiki/Levene%27s_test), 901 which yields a p-value corresponding to the null hypothesis that the 902 underlying variances are equal). 903 904 `LEVENE_REF_SAMPLE` (by default equal to `'ETH-3'`) specifies which 905 sample should be used as a reference for this test. 906 ''' 907 908 ALPHA_18O_ACID_REACTION = round(np.exp(3.59 / (90 + 273.15) - 1.79e-3), 6) # (Kim et al., 2007, calcite) 909 ''' 910 Specifies the 18O/16O fractionation factor generally applicable 911 to acid reactions in the dataset. Currently used by `D4xdata.wg()`, 912 `D4xdata.standardize_d13C`, and `D4xdata.standardize_d18O`. 913 914 By default equal to 1.008129 (calcite reacted at 90 °C, 915 [Kim et al., 2007](https://dx.doi.org/10.1016/j.chemgeo.2007.08.005)). 916 ''' 917 918 Nominal_d13C_VPDB = { 919 'ETH-1': 2.02, 920 'ETH-2': -10.17, 921 'ETH-3': 1.71, 922 } # (Bernasconi et al., 2018) 923 ''' 924 Nominal δ13C_VPDB values assigned to carbonate standards, used by 925 `D4xdata.standardize_d13C()`. 926 927 By default equal to `{'ETH-1': 2.02, 'ETH-2': -10.17, 'ETH-3': 1.71}` after 928 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 929 ''' 930 931 Nominal_d18O_VPDB = { 932 'ETH-1': -2.19, 933 'ETH-2': -18.69, 934 'ETH-3': -1.78, 935 } # (Bernasconi et al., 2018) 936 ''' 937 Nominal δ18O_VPDB values assigned to carbonate standards, used by 938 `D4xdata.standardize_d18O()`. 939 940 By default equal to `{'ETH-1': -2.19, 'ETH-2': -18.69, 'ETH-3': -1.78}` after 941 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 942 ''' 943 944 d13C_STANDARDIZATION_METHOD = '2pt' 945 ''' 946 Method by which to standardize δ13C values: 947 948 + `none`: do not apply any δ13C standardization. 949 + `'1pt'`: within each session, offset all initial δ13C values so as to 950 minimize the difference between final δ13C_VPDB values and 951 `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` is defined). 952 + `'2pt'`: within each session, apply a affine trasformation to all δ13C 953 values so as to minimize the difference between final δ13C_VPDB 954 values and `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` 955 is defined). 956 ''' 957 958 d18O_STANDARDIZATION_METHOD = '2pt' 959 ''' 960 Method by which to standardize δ18O values: 961 962 + `none`: do not apply any δ18O standardization. 963 + `'1pt'`: within each session, offset all initial δ18O values so as to 964 minimize the difference between final δ18O_VPDB values and 965 `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` is defined). 966 + `'2pt'`: within each session, apply a affine trasformation to all δ18O 967 values so as to minimize the difference between final δ18O_VPDB 968 values and `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` 969 is defined). 970 ''' 971 972 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 973 ''' 974 **Parameters** 975 976 + `l`: a list of dictionaries, with each dictionary including at least the keys 977 `Sample`, `d45`, `d46`, and `d47` or `d48`. 978 + `mass`: `'47'` or `'48'` 979 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 980 + `session`: define session name for analyses without a `Session` key 981 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 982 983 Returns a `D4xdata` object derived from `list`. 984 ''' 985 self._4x = mass 986 self.verbose = verbose 987 self.prefix = 'D4xdata' 988 self.logfile = logfile 989 list.__init__(self, l) 990 self.Nf = None 991 self.repeatability = {} 992 self.refresh(session = session) 993 994 995 def make_verbal(oldfun): 996 ''' 997 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 998 ''' 999 @wraps(oldfun) 1000 def newfun(*args, verbose = '', **kwargs): 1001 myself = args[0] 1002 oldprefix = myself.prefix 1003 myself.prefix = oldfun.__name__ 1004 if verbose != '': 1005 oldverbose = myself.verbose 1006 myself.verbose = verbose 1007 out = oldfun(*args, **kwargs) 1008 myself.prefix = oldprefix 1009 if verbose != '': 1010 myself.verbose = oldverbose 1011 return out 1012 return newfun 1013 1014 1015 def msg(self, txt): 1016 ''' 1017 Log a message to `self.logfile`, and print it out if `verbose = True` 1018 ''' 1019 self.log(txt) 1020 if self.verbose: 1021 print(f'{f"[{self.prefix}]":<16} {txt}') 1022 1023 1024 def vmsg(self, txt): 1025 ''' 1026 Log a message to `self.logfile` and print it out 1027 ''' 1028 self.log(txt) 1029 print(txt) 1030 1031 1032 def log(self, *txts): 1033 ''' 1034 Log a message to `self.logfile` 1035 ''' 1036 if self.logfile: 1037 with open(self.logfile, 'a') as fid: 1038 for txt in txts: 1039 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}') 1040 1041 1042 def refresh(self, session = 'mySession'): 1043 ''' 1044 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1045 ''' 1046 self.fill_in_missing_info(session = session) 1047 self.refresh_sessions() 1048 self.refresh_samples() 1049 1050 1051 def refresh_sessions(self): 1052 ''' 1053 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1054 to `False` for all sessions. 1055 ''' 1056 self.sessions = { 1057 s: {'data': [r for r in self if r['Session'] == s]} 1058 for s in sorted({r['Session'] for r in self}) 1059 } 1060 for s in self.sessions: 1061 self.sessions[s]['scrambling_drift'] = False 1062 self.sessions[s]['slope_drift'] = False 1063 self.sessions[s]['wg_drift'] = False 1064 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1065 self.sessions[s]['d18O_standardization_method'] = self.d18O_STANDARDIZATION_METHOD 1066 1067 1068 def refresh_samples(self): 1069 ''' 1070 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1071 ''' 1072 self.samples = { 1073 s: {'data': [r for r in self if r['Sample'] == s]} 1074 for s in sorted({r['Sample'] for r in self}) 1075 } 1076 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1077 self.unknowns = {s: self.samples[s] for s in self.samples if s not in self.Nominal_D4x} 1078 1079 1080 def read(self, filename, sep = '', session = ''): 1081 ''' 1082 Read file in csv format to load data into a `D47data` object. 1083 1084 In the csv file, spaces before and after field separators (`','` by default) 1085 are optional. Each line corresponds to a single analysis. 1086 1087 The required fields are: 1088 1089 + `UID`: a unique identifier 1090 + `Session`: an identifier for the analytical session 1091 + `Sample`: a sample identifier 1092 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1093 1094 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1095 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1096 and `d49` are optional, and set to NaN by default. 1097 1098 **Parameters** 1099 1100 + `fileneme`: the path of the file to read 1101 + `sep`: csv separator delimiting the fields 1102 + `session`: set `Session` field to this string for all analyses 1103 ''' 1104 with open(filename) as fid: 1105 self.input(fid.read(), sep = sep, session = session) 1106 1107 1108 def input(self, txt, sep = '', session = ''): 1109 ''' 1110 Read `txt` string in csv format to load analysis data into a `D47data` object. 1111 1112 In the csv string, spaces before and after field separators (`','` by default) 1113 are optional. Each line corresponds to a single analysis. 1114 1115 The required fields are: 1116 1117 + `UID`: a unique identifier 1118 + `Session`: an identifier for the analytical session 1119 + `Sample`: a sample identifier 1120 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1121 1122 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1123 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1124 and `d49` are optional, and set to NaN by default. 1125 1126 **Parameters** 1127 1128 + `txt`: the csv string to read 1129 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1130 whichever appers most often in `txt`. 1131 + `session`: set `Session` field to this string for all analyses 1132 ''' 1133 if sep == '': 1134 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1135 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1136 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:]] 1137 1138 if session != '': 1139 for r in data: 1140 r['Session'] = session 1141 1142 self += data 1143 self.refresh() 1144 1145 1146 @make_verbal 1147 def wg(self, samples = None, a18_acid = None): 1148 ''' 1149 Compute bulk composition of the working gas for each session based on 1150 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1151 `self.Nominal_d18O_VPDB`. 1152 ''' 1153 1154 self.msg('Computing WG composition:') 1155 1156 if a18_acid is None: 1157 a18_acid = self.ALPHA_18O_ACID_REACTION 1158 if samples is None: 1159 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1160 1161 assert a18_acid, f'Acid fractionation factor should not be zero.' 1162 1163 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1164 R45R46_standards = {} 1165 for sample in samples: 1166 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1167 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1168 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1169 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1170 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1171 1172 C12_s = 1 / (1 + R13_s) 1173 C13_s = R13_s / (1 + R13_s) 1174 C16_s = 1 / (1 + R17_s + R18_s) 1175 C17_s = R17_s / (1 + R17_s + R18_s) 1176 C18_s = R18_s / (1 + R17_s + R18_s) 1177 1178 C626_s = C12_s * C16_s ** 2 1179 C627_s = 2 * C12_s * C16_s * C17_s 1180 C628_s = 2 * C12_s * C16_s * C18_s 1181 C636_s = C13_s * C16_s ** 2 1182 C637_s = 2 * C13_s * C16_s * C17_s 1183 C727_s = C12_s * C17_s ** 2 1184 1185 R45_s = (C627_s + C636_s) / C626_s 1186 R46_s = (C628_s + C637_s + C727_s) / C626_s 1187 R45R46_standards[sample] = (R45_s, R46_s) 1188 1189 for s in self.sessions: 1190 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1191 assert db, f'No sample from {samples} found in session "{s}".' 1192# dbsamples = sorted({r['Sample'] for r in db}) 1193 1194 X = [r['d45'] for r in db] 1195 Y = [R45R46_standards[r['Sample']][0] for r in db] 1196 x1, x2 = np.min(X), np.max(X) 1197 1198 if x1 < x2: 1199 wgcoord = x1/(x1-x2) 1200 else: 1201 wgcoord = 999 1202 1203 if wgcoord < -.5 or wgcoord > 1.5: 1204 # unreasonable to extrapolate to d45 = 0 1205 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1206 else : 1207 # d45 = 0 is reasonably well bracketed 1208 R45_wg = np.polyfit(X, Y, 1)[1] 1209 1210 X = [r['d46'] for r in db] 1211 Y = [R45R46_standards[r['Sample']][1] for r in db] 1212 x1, x2 = np.min(X), np.max(X) 1213 1214 if x1 < x2: 1215 wgcoord = x1/(x1-x2) 1216 else: 1217 wgcoord = 999 1218 1219 if wgcoord < -.5 or wgcoord > 1.5: 1220 # unreasonable to extrapolate to d46 = 0 1221 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1222 else : 1223 # d46 = 0 is reasonably well bracketed 1224 R46_wg = np.polyfit(X, Y, 1)[1] 1225 1226 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1227 1228 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1229 1230 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1231 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1232 for r in self.sessions[s]['data']: 1233 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1234 r['d18Owg_VSMOW'] = d18Owg_VSMOW 1235 1236 1237 def compute_bulk_delta(self, R45, R46, D17O = 0): 1238 ''' 1239 Compute δ13C_VPDB and δ18O_VSMOW, 1240 by solving the generalized form of equation (17) from 1241 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1242 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1243 solving the corresponding second-order Taylor polynomial. 1244 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1245 ''' 1246 1247 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1248 1249 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1250 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1251 C = 2 * self.R18_VSMOW 1252 D = -R46 1253 1254 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1255 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1256 cc = A + B + C + D 1257 1258 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1259 1260 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1261 R17 = K * R18 ** self.LAMBDA_17 1262 R13 = R45 - 2 * R17 1263 1264 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1265 1266 return d13C_VPDB, d18O_VSMOW 1267 1268 1269 @make_verbal 1270 def crunch(self, verbose = ''): 1271 ''' 1272 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1273 ''' 1274 for r in self: 1275 self.compute_bulk_and_clumping_deltas(r) 1276 self.standardize_d13C() 1277 self.standardize_d18O() 1278 self.msg(f"Crunched {len(self)} analyses.") 1279 1280 1281 def fill_in_missing_info(self, session = 'mySession'): 1282 ''' 1283 Fill in optional fields with default values 1284 ''' 1285 for i,r in enumerate(self): 1286 if 'D17O' not in r: 1287 r['D17O'] = 0. 1288 if 'UID' not in r: 1289 r['UID'] = f'{i+1}' 1290 if 'Session' not in r: 1291 r['Session'] = session 1292 for k in ['d47', 'd48', 'd49']: 1293 if k not in r: 1294 r[k] = np.nan 1295 1296 1297 def standardize_d13C(self): 1298 ''' 1299 Perform δ13C standadization within each session `s` according to 1300 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1301 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1302 may be redefined abitrarily at a later stage. 1303 ''' 1304 for s in self.sessions: 1305 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1306 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] 1307 X,Y = zip(*XY) 1308 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1309 offset = np.mean(Y) - np.mean(X) 1310 for r in self.sessions[s]['data']: 1311 r['d13C_VPDB'] += offset 1312 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1313 a,b = np.polyfit(X,Y,1) 1314 for r in self.sessions[s]['data']: 1315 r['d13C_VPDB'] = a * r['d13C_VPDB'] + b 1316 1317 def standardize_d18O(self): 1318 ''' 1319 Perform δ18O standadization within each session `s` according to 1320 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1321 which is defined by default by `D47data.refresh_sessions()`as equal to 1322 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1323 ''' 1324 for s in self.sessions: 1325 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1326 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] 1327 X,Y = zip(*XY) 1328 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1329 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1330 offset = np.mean(Y) - np.mean(X) 1331 for r in self.sessions[s]['data']: 1332 r['d18O_VSMOW'] += offset 1333 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1334 a,b = np.polyfit(X,Y,1) 1335 for r in self.sessions[s]['data']: 1336 r['d18O_VSMOW'] = a * r['d18O_VSMOW'] + b 1337 1338 1339 def compute_bulk_and_clumping_deltas(self, r): 1340 ''' 1341 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1342 ''' 1343 1344 # Compute working gas R13, R18, and isobar ratios 1345 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1346 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1347 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1348 1349 # Compute analyte isobar ratios 1350 R45 = (1 + r['d45'] / 1000) * R45_wg 1351 R46 = (1 + r['d46'] / 1000) * R46_wg 1352 R47 = (1 + r['d47'] / 1000) * R47_wg 1353 R48 = (1 + r['d48'] / 1000) * R48_wg 1354 R49 = (1 + r['d49'] / 1000) * R49_wg 1355 1356 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1357 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1358 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1359 1360 # Compute stochastic isobar ratios of the analyte 1361 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1362 R13, R18, D17O = r['D17O'] 1363 ) 1364 1365 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1366 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1367 if (R45 / R45stoch - 1) > 5e-8: 1368 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1369 if (R46 / R46stoch - 1) > 5e-8: 1370 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1371 1372 # Compute raw clumped isotope anomalies 1373 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1374 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1375 r['D49raw'] = 1000 * (R49 / R49stoch - 1) 1376 1377 1378 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1379 ''' 1380 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1381 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1382 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1383 ''' 1384 1385 # Compute R17 1386 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1387 1388 # Compute isotope concentrations 1389 C12 = (1 + R13) ** -1 1390 C13 = C12 * R13 1391 C16 = (1 + R17 + R18) ** -1 1392 C17 = C16 * R17 1393 C18 = C16 * R18 1394 1395 # Compute stochastic isotopologue concentrations 1396 C626 = C16 * C12 * C16 1397 C627 = C16 * C12 * C17 * 2 1398 C628 = C16 * C12 * C18 * 2 1399 C636 = C16 * C13 * C16 1400 C637 = C16 * C13 * C17 * 2 1401 C638 = C16 * C13 * C18 * 2 1402 C727 = C17 * C12 * C17 1403 C728 = C17 * C12 * C18 * 2 1404 C737 = C17 * C13 * C17 1405 C738 = C17 * C13 * C18 * 2 1406 C828 = C18 * C12 * C18 1407 C838 = C18 * C13 * C18 1408 1409 # Compute stochastic isobar ratios 1410 R45 = (C636 + C627) / C626 1411 R46 = (C628 + C637 + C727) / C626 1412 R47 = (C638 + C728 + C737) / C626 1413 R48 = (C738 + C828) / C626 1414 R49 = C838 / C626 1415 1416 # Account for stochastic anomalies 1417 R47 *= 1 + D47 / 1000 1418 R48 *= 1 + D48 / 1000 1419 R49 *= 1 + D49 / 1000 1420 1421 # Return isobar ratios 1422 return R45, R46, R47, R48, R49 1423 1424 1425 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1426 ''' 1427 Split unknown samples by UID (treat all analyses as different samples) 1428 or by session (treat analyses of a given sample in different sessions as 1429 different samples). 1430 1431 **Parameters** 1432 1433 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1434 + `grouping`: `by_uid` | `by_session` 1435 ''' 1436 if samples_to_split == 'all': 1437 samples_to_split = [s for s in self.unknowns] 1438 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1439 self.grouping = grouping.lower() 1440 if self.grouping in gkeys: 1441 gkey = gkeys[self.grouping] 1442 for r in self: 1443 if r['Sample'] in samples_to_split: 1444 r['Sample_original'] = r['Sample'] 1445 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1446 elif r['Sample'] in self.unknowns: 1447 r['Sample_original'] = r['Sample'] 1448 self.refresh_samples() 1449 1450 1451 def unsplit_samples(self, tables = False): 1452 ''' 1453 Reverse the effects of `D47data.split_samples()`. 1454 1455 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1456 1457 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1458 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1459 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1460 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1461 that case session-averaged Δ4x values are statistically independent). 1462 ''' 1463 unknowns_old = sorted({s for s in self.unknowns}) 1464 CM_old = self.standardization.covar[:,:] 1465 VD_old = self.standardization.params.valuesdict().copy() 1466 vars_old = self.standardization.var_names 1467 1468 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1469 1470 Ns = len(vars_old) - len(unknowns_old) 1471 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1472 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1473 1474 W = np.zeros((len(vars_new), len(vars_old))) 1475 W[:Ns,:Ns] = np.eye(Ns) 1476 for u in unknowns_new: 1477 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1478 if self.grouping == 'by_session': 1479 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1480 elif self.grouping == 'by_uid': 1481 weights = [1 for s in splits] 1482 sw = sum(weights) 1483 weights = [w/sw for w in weights] 1484 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1485 1486 CM_new = W @ CM_old @ W.T 1487 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1488 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1489 1490 self.standardization.covar = CM_new 1491 self.standardization.params.valuesdict = lambda : VD_new 1492 self.standardization.var_names = vars_new 1493 1494 for r in self: 1495 if r['Sample'] in self.unknowns: 1496 r['Sample_split'] = r['Sample'] 1497 r['Sample'] = r['Sample_original'] 1498 1499 self.refresh_samples() 1500 self.consolidate_samples() 1501 self.repeatabilities() 1502 1503 if tables: 1504 self.table_of_analyses() 1505 self.table_of_samples() 1506 1507 def assign_timestamps(self): 1508 ''' 1509 Assign a time field `t` of type `float` to each analysis. 1510 1511 If `TimeTag` is one of the data fields, `t` is equal within a given session 1512 to `TimeTag` minus the mean value of `TimeTag` for that session. 1513 Otherwise, `TimeTag` is by default equal to the index of each analysis 1514 in the dataset and `t` is defined as above. 1515 ''' 1516 for session in self.sessions: 1517 sdata = self.sessions[session]['data'] 1518 try: 1519 t0 = np.mean([r['TimeTag'] for r in sdata]) 1520 for r in sdata: 1521 r['t'] = r['TimeTag'] - t0 1522 except KeyError: 1523 t0 = (len(sdata)-1)/2 1524 for t,r in enumerate(sdata): 1525 r['t'] = t - t0 1526 1527 1528 def report(self): 1529 ''' 1530 Prints a report on the standardization fit. 1531 Only applicable after `D4xdata.standardize(method='pooled')`. 1532 ''' 1533 report_fit(self.standardization) 1534 1535 1536 def combine_samples(self, sample_groups): 1537 ''' 1538 Combine analyses of different samples to compute weighted average Δ4x 1539 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1540 dictionary. 1541 1542 Caution: samples are weighted by number of replicate analyses, which is a 1543 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1544 correlated analytical errors for one or more samples). 1545 1546 Returns a tuplet of: 1547 1548 + the list of group names 1549 + an array of the corresponding Δ4x values 1550 + the corresponding (co)variance matrix 1551 1552 **Parameters** 1553 1554 + `sample_groups`: a dictionary of the form: 1555 ```py 1556 {'group1': ['sample_1', 'sample_2'], 1557 'group2': ['sample_3', 'sample_4', 'sample_5']} 1558 ``` 1559 ''' 1560 1561 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1562 groups = sorted(sample_groups.keys()) 1563 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1564 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1565 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1566 W = np.array([ 1567 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1568 for j in groups]) 1569 D4x_new = W @ D4x_old 1570 CM_new = W @ CM_old @ W.T 1571 1572 return groups, D4x_new[:,0], CM_new 1573 1574 1575 @make_verbal 1576 def standardize(self, 1577 method = 'pooled', 1578 weighted_sessions = [], 1579 consolidate = True, 1580 consolidate_tables = False, 1581 consolidate_plots = False, 1582 constraints = {}, 1583 ): 1584 ''' 1585 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1586 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1587 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1588 i.e. that their true Δ4x value does not change between sessions, 1589 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1590 `'indep_sessions'`, the standardization processes each session independently, based only 1591 on anchors analyses. 1592 ''' 1593 1594 self.standardization_method = method 1595 self.assign_timestamps() 1596 1597 if method == 'pooled': 1598 if weighted_sessions: 1599 for session_group in weighted_sessions: 1600 if self._4x == '47': 1601 X = D47data([r for r in self if r['Session'] in session_group]) 1602 elif self._4x == '48': 1603 X = D48data([r for r in self if r['Session'] in session_group]) 1604 X.Nominal_D4x = self.Nominal_D4x.copy() 1605 X.refresh() 1606 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1607 w = np.sqrt(result.redchi) 1608 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1609 for r in X: 1610 r[f'wD{self._4x}raw'] *= w 1611 else: 1612 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1613 for r in self: 1614 r[f'wD{self._4x}raw'] = 1. 1615 1616 params = Parameters() 1617 for k,session in enumerate(self.sessions): 1618 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1619 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1620 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1621 s = pf(session) 1622 params.add(f'a_{s}', value = 0.9) 1623 params.add(f'b_{s}', value = 0.) 1624 params.add(f'c_{s}', value = -0.9) 1625 params.add(f'a2_{s}', value = 0., vary = self.sessions[session]['scrambling_drift']) 1626 params.add(f'b2_{s}', value = 0., vary = self.sessions[session]['slope_drift']) 1627 params.add(f'c2_{s}', value = 0., vary = self.sessions[session]['wg_drift']) 1628 for sample in self.unknowns: 1629 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1630 1631 for k in constraints: 1632 params[k].expr = constraints[k] 1633 1634 def residuals(p): 1635 R = [] 1636 for r in self: 1637 session = pf(r['Session']) 1638 sample = pf(r['Sample']) 1639 if r['Sample'] in self.Nominal_D4x: 1640 R += [ ( 1641 r[f'D{self._4x}raw'] - ( 1642 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1643 + p[f'b_{session}'] * r[f'd{self._4x}'] 1644 + p[f'c_{session}'] 1645 + r['t'] * ( 1646 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1647 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1648 + p[f'c2_{session}'] 1649 ) 1650 ) 1651 ) / r[f'wD{self._4x}raw'] ] 1652 else: 1653 R += [ ( 1654 r[f'D{self._4x}raw'] - ( 1655 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1656 + p[f'b_{session}'] * r[f'd{self._4x}'] 1657 + p[f'c_{session}'] 1658 + r['t'] * ( 1659 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1660 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1661 + p[f'c2_{session}'] 1662 ) 1663 ) 1664 ) / r[f'wD{self._4x}raw'] ] 1665 return R 1666 1667 M = Minimizer(residuals, params) 1668 result = M.least_squares() 1669 self.Nf = result.nfree 1670 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1671# if self.verbose: 1672# report_fit(result) 1673 1674 for r in self: 1675 s = pf(r["Session"]) 1676 a = result.params.valuesdict()[f'a_{s}'] 1677 b = result.params.valuesdict()[f'b_{s}'] 1678 c = result.params.valuesdict()[f'c_{s}'] 1679 a2 = result.params.valuesdict()[f'a2_{s}'] 1680 b2 = result.params.valuesdict()[f'b2_{s}'] 1681 c2 = result.params.valuesdict()[f'c2_{s}'] 1682 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']) 1683 1684 self.standardization = result 1685 1686 for session in self.sessions: 1687 self.sessions[session]['Np'] = 3 1688 for k in ['scrambling', 'slope', 'wg']: 1689 if self.sessions[session][f'{k}_drift']: 1690 self.sessions[session]['Np'] += 1 1691 1692 if consolidate: 1693 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1694 return result 1695 1696 1697 elif method == 'indep_sessions': 1698 1699 if weighted_sessions: 1700 for session_group in weighted_sessions: 1701 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1702 X.Nominal_D4x = self.Nominal_D4x.copy() 1703 X.refresh() 1704 # This is only done to assign r['wD47raw'] for r in X: 1705 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1706 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}') 1707 else: 1708 self.msg('All weights set to 1 ‰') 1709 for r in self: 1710 r[f'wD{self._4x}raw'] = 1 1711 1712 for session in self.sessions: 1713 s = self.sessions[session] 1714 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1715 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1716 s['Np'] = sum(p_active) 1717 sdata = s['data'] 1718 1719 A = np.array([ 1720 [ 1721 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1722 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1723 1 / r[f'wD{self._4x}raw'], 1724 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1725 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1726 r['t'] / r[f'wD{self._4x}raw'] 1727 ] 1728 for r in sdata if r['Sample'] in self.anchors 1729 ])[:,p_active] # only keep columns for the active parameters 1730 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]) 1731 s['Na'] = Y.size 1732 CM = linalg.inv(A.T @ A) 1733 bf = (CM @ A.T @ Y).T[0,:] 1734 k = 0 1735 for n,a in zip(p_names, p_active): 1736 if a: 1737 s[n] = bf[k] 1738# self.msg(f'{n} = {bf[k]}') 1739 k += 1 1740 else: 1741 s[n] = 0. 1742# self.msg(f'{n} = 0.0') 1743 1744 for r in sdata : 1745 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1746 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']) 1747 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1748 1749 s['CM'] = np.zeros((6,6)) 1750 i = 0 1751 k_active = [j for j,a in enumerate(p_active) if a] 1752 for j,a in enumerate(p_active): 1753 if a: 1754 s['CM'][j,k_active] = CM[i,:] 1755 i += 1 1756 1757 if not weighted_sessions: 1758 w = self.rmswd()['rmswd'] 1759 for r in self: 1760 r[f'wD{self._4x}'] *= w 1761 r[f'wD{self._4x}raw'] *= w 1762 for session in self.sessions: 1763 self.sessions[session]['CM'] *= w**2 1764 1765 for session in self.sessions: 1766 s = self.sessions[session] 1767 s['SE_a'] = s['CM'][0,0]**.5 1768 s['SE_b'] = s['CM'][1,1]**.5 1769 s['SE_c'] = s['CM'][2,2]**.5 1770 s['SE_a2'] = s['CM'][3,3]**.5 1771 s['SE_b2'] = s['CM'][4,4]**.5 1772 s['SE_c2'] = s['CM'][5,5]**.5 1773 1774 if not weighted_sessions: 1775 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1776 else: 1777 self.Nf = 0 1778 for sg in weighted_sessions: 1779 self.Nf += self.rmswd(sessions = sg)['Nf'] 1780 1781 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1782 1783 avgD4x = { 1784 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1785 for sample in self.samples 1786 } 1787 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1788 rD4x = (chi2/self.Nf)**.5 1789 self.repeatability[f'sigma_{self._4x}'] = rD4x 1790 1791 if consolidate: 1792 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1793 1794 1795 def standardization_error(self, session, d4x, D4x, t = 0): 1796 ''' 1797 Compute standardization error for a given session and 1798 (δ47, Δ47) composition. 1799 ''' 1800 a = self.sessions[session]['a'] 1801 b = self.sessions[session]['b'] 1802 c = self.sessions[session]['c'] 1803 a2 = self.sessions[session]['a2'] 1804 b2 = self.sessions[session]['b2'] 1805 c2 = self.sessions[session]['c2'] 1806 CM = self.sessions[session]['CM'] 1807 1808 x, y = D4x, d4x 1809 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1810# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1811 dxdy = -(b+b2*t) / (a+a2*t) 1812 dxdz = 1. / (a+a2*t) 1813 dxda = -x / (a+a2*t) 1814 dxdb = -y / (a+a2*t) 1815 dxdc = -1. / (a+a2*t) 1816 dxda2 = -x * a2 / (a+a2*t) 1817 dxdb2 = -y * t / (a+a2*t) 1818 dxdc2 = -t / (a+a2*t) 1819 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1820 sx = (V @ CM @ V.T) ** .5 1821 return sx 1822 1823 1824 @make_verbal 1825 def summary(self, 1826 dir = 'output', 1827 filename = None, 1828 save_to_file = True, 1829 print_out = True, 1830 ): 1831 ''' 1832 Print out an/or save to disk a summary of the standardization results. 1833 1834 **Parameters** 1835 1836 + `dir`: the directory in which to save the table 1837 + `filename`: the name to the csv file to write to 1838 + `save_to_file`: whether to save the table to disk 1839 + `print_out`: whether to print out the table 1840 ''' 1841 1842 out = [] 1843 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1844 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])})"]] 1845 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1846 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1847 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1848 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1849 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1850 out += [['Model degrees of freedom', f"{self.Nf}"]] 1851 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1852 out += [['Standardization method', self.standardization_method]] 1853 1854 if save_to_file: 1855 if not os.path.exists(dir): 1856 os.makedirs(dir) 1857 if filename is None: 1858 filename = f'D{self._4x}_summary.csv' 1859 with open(f'{dir}/{filename}', 'w') as fid: 1860 fid.write(make_csv(out)) 1861 if print_out: 1862 self.msg('\n' + pretty_table(out, header = 0)) 1863 1864 1865 @make_verbal 1866 def table_of_sessions(self, 1867 dir = 'output', 1868 filename = None, 1869 save_to_file = True, 1870 print_out = True, 1871 output = None, 1872 ): 1873 ''' 1874 Print out an/or save to disk a table of sessions. 1875 1876 **Parameters** 1877 1878 + `dir`: the directory in which to save the table 1879 + `filename`: the name to the csv file to write to 1880 + `save_to_file`: whether to save the table to disk 1881 + `print_out`: whether to print out the table 1882 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1883 if set to `'raw'`: return a list of list of strings 1884 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1885 ''' 1886 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1887 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1888 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1889 1890 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']] 1891 if include_a2: 1892 out[-1] += ['a2 ± SE'] 1893 if include_b2: 1894 out[-1] += ['b2 ± SE'] 1895 if include_c2: 1896 out[-1] += ['c2 ± SE'] 1897 for session in self.sessions: 1898 out += [[ 1899 session, 1900 f"{self.sessions[session]['Na']}", 1901 f"{self.sessions[session]['Nu']}", 1902 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1903 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1904 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1905 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1906 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1907 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1908 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1909 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1910 ]] 1911 if include_a2: 1912 if self.sessions[session]['scrambling_drift']: 1913 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1914 else: 1915 out[-1] += [''] 1916 if include_b2: 1917 if self.sessions[session]['slope_drift']: 1918 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1919 else: 1920 out[-1] += [''] 1921 if include_c2: 1922 if self.sessions[session]['wg_drift']: 1923 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1924 else: 1925 out[-1] += [''] 1926 1927 if save_to_file: 1928 if not os.path.exists(dir): 1929 os.makedirs(dir) 1930 if filename is None: 1931 filename = f'D{self._4x}_sessions.csv' 1932 with open(f'{dir}/{filename}', 'w') as fid: 1933 fid.write(make_csv(out)) 1934 if print_out: 1935 self.msg('\n' + pretty_table(out)) 1936 if output == 'raw': 1937 return out 1938 elif output == 'pretty': 1939 return pretty_table(out) 1940 1941 1942 @make_verbal 1943 def table_of_analyses( 1944 self, 1945 dir = 'output', 1946 filename = None, 1947 save_to_file = True, 1948 print_out = True, 1949 output = None, 1950 ): 1951 ''' 1952 Print out an/or save to disk a table of analyses. 1953 1954 **Parameters** 1955 1956 + `dir`: the directory in which to save the table 1957 + `filename`: the name to the csv file to write to 1958 + `save_to_file`: whether to save the table to disk 1959 + `print_out`: whether to print out the table 1960 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1961 if set to `'raw'`: return a list of list of strings 1962 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1963 ''' 1964 1965 out = [['UID','Session','Sample']] 1966 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}] 1967 for f in extra_fields: 1968 out[-1] += [f[0]] 1969 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 1970 for r in self: 1971 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 1972 for f in extra_fields: 1973 out[-1] += [f"{r[f[0]]:{f[1]}}"] 1974 out[-1] += [ 1975 f"{r['d13Cwg_VPDB']:.3f}", 1976 f"{r['d18Owg_VSMOW']:.3f}", 1977 f"{r['d45']:.6f}", 1978 f"{r['d46']:.6f}", 1979 f"{r['d47']:.6f}", 1980 f"{r['d48']:.6f}", 1981 f"{r['d49']:.6f}", 1982 f"{r['d13C_VPDB']:.6f}", 1983 f"{r['d18O_VSMOW']:.6f}", 1984 f"{r['D47raw']:.6f}", 1985 f"{r['D48raw']:.6f}", 1986 f"{r['D49raw']:.6f}", 1987 f"{r[f'D{self._4x}']:.6f}" 1988 ] 1989 if save_to_file: 1990 if not os.path.exists(dir): 1991 os.makedirs(dir) 1992 if filename is None: 1993 filename = f'D{self._4x}_analyses.csv' 1994 with open(f'{dir}/{filename}', 'w') as fid: 1995 fid.write(make_csv(out)) 1996 if print_out: 1997 self.msg('\n' + pretty_table(out)) 1998 return out 1999 2000 @make_verbal 2001 def covar_table( 2002 self, 2003 correl = False, 2004 dir = 'output', 2005 filename = None, 2006 save_to_file = True, 2007 print_out = True, 2008 output = None, 2009 ): 2010 ''' 2011 Print out, save to disk and/or return the variance-covariance matrix of D4x 2012 for all unknown samples. 2013 2014 **Parameters** 2015 2016 + `dir`: the directory in which to save the csv 2017 + `filename`: the name of the csv file to write to 2018 + `save_to_file`: whether to save the csv 2019 + `print_out`: whether to print out the matrix 2020 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2021 if set to `'raw'`: return a list of list of strings 2022 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2023 ''' 2024 samples = sorted([u for u in self.unknowns]) 2025 out = [[''] + samples] 2026 for s1 in samples: 2027 out.append([s1]) 2028 for s2 in samples: 2029 if correl: 2030 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2031 else: 2032 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2033 2034 if save_to_file: 2035 if not os.path.exists(dir): 2036 os.makedirs(dir) 2037 if filename is None: 2038 if correl: 2039 filename = f'D{self._4x}_correl.csv' 2040 else: 2041 filename = f'D{self._4x}_covar.csv' 2042 with open(f'{dir}/{filename}', 'w') as fid: 2043 fid.write(make_csv(out)) 2044 if print_out: 2045 self.msg('\n'+pretty_table(out)) 2046 if output == 'raw': 2047 return out 2048 elif output == 'pretty': 2049 return pretty_table(out) 2050 2051 @make_verbal 2052 def table_of_samples( 2053 self, 2054 dir = 'output', 2055 filename = None, 2056 save_to_file = True, 2057 print_out = True, 2058 output = None, 2059 ): 2060 ''' 2061 Print out, save to disk and/or return a table of samples. 2062 2063 **Parameters** 2064 2065 + `dir`: the directory in which to save the csv 2066 + `filename`: the name of the csv file to write to 2067 + `save_to_file`: whether to save the csv 2068 + `print_out`: whether to print out the table 2069 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2070 if set to `'raw'`: return a list of list of strings 2071 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2072 ''' 2073 2074 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2075 for sample in self.anchors: 2076 out += [[ 2077 f"{sample}", 2078 f"{self.samples[sample]['N']}", 2079 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2080 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2081 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2082 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2083 ]] 2084 for sample in self.unknowns: 2085 out += [[ 2086 f"{sample}", 2087 f"{self.samples[sample]['N']}", 2088 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2089 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2090 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2091 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2092 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2093 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2094 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2095 ]] 2096 if save_to_file: 2097 if not os.path.exists(dir): 2098 os.makedirs(dir) 2099 if filename is None: 2100 filename = f'D{self._4x}_samples.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 2111 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2112 ''' 2113 Generate session plots and save them to disk. 2114 2115 **Parameters** 2116 2117 + `dir`: the directory in which to save the plots 2118 + `figsize`: the width and height (in inches) of each plot 2119 ''' 2120 if not os.path.exists(dir): 2121 os.makedirs(dir) 2122 2123 for session in self.sessions: 2124 sp = self.plot_single_session(session, xylimits = 'constant') 2125 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2126 ppl.close(sp.fig) 2127 2128 2129 @make_verbal 2130 def consolidate_samples(self): 2131 ''' 2132 Compile various statistics for each sample. 2133 2134 For each anchor sample: 2135 2136 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2137 + `SE_D47` or `SE_D48`: set to zero by definition 2138 2139 For each unknown sample: 2140 2141 + `D47` or `D48`: the standardized Δ4x value for this unknown 2142 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2143 2144 For each anchor and unknown: 2145 2146 + `N`: the total number of analyses of this sample 2147 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2148 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2149 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2150 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2151 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2152 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2153 ''' 2154 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2155 for sample in self.samples: 2156 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2157 if self.samples[sample]['N'] > 1: 2158 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2159 2160 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2161 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2162 2163 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2164 if len(D4x_pop) > 2: 2165 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2166 2167 if self.standardization_method == 'pooled': 2168 for sample in self.anchors: 2169 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2170 self.samples[sample][f'SE_D{self._4x}'] = 0. 2171 for sample in self.unknowns: 2172 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2173 try: 2174 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2175 except ValueError: 2176 # when `sample` is constrained by self.standardize(constraints = {...}), 2177 # it is no longer listed in self.standardization.var_names. 2178 # Temporary fix: define SE as zero for now 2179 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2180 2181 elif self.standardization_method == 'indep_sessions': 2182 for sample in self.anchors: 2183 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2184 self.samples[sample][f'SE_D{self._4x}'] = 0. 2185 for sample in self.unknowns: 2186 self.msg(f'Consolidating sample {sample}') 2187 self.unknowns[sample][f'session_D{self._4x}'] = {} 2188 session_avg = [] 2189 for session in self.sessions: 2190 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2191 if sdata: 2192 self.msg(f'{sample} found in session {session}') 2193 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2194 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2195 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2196 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2197 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2198 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2199 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2200 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2201 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2202 wsum = sum([weights[s] for s in weights]) 2203 for s in weights: 2204 self.unknowns[sample][f'session_D{self._4x}'][s] += [self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 / wsum] 2205 2206 2207 def consolidate_sessions(self): 2208 ''' 2209 Compute various statistics for each session. 2210 2211 + `Na`: Number of anchor analyses in the session 2212 + `Nu`: Number of unknown analyses in the session 2213 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2214 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2215 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2216 + `a`: scrambling factor 2217 + `b`: compositional slope 2218 + `c`: WG offset 2219 + `SE_a`: Model stadard erorr of `a` 2220 + `SE_b`: Model stadard erorr of `b` 2221 + `SE_c`: Model stadard erorr of `c` 2222 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2223 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2224 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2225 + `a2`: scrambling factor drift 2226 + `b2`: compositional slope drift 2227 + `c2`: WG offset drift 2228 + `Np`: Number of standardization parameters to fit 2229 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2230 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2231 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2232 ''' 2233 for session in self.sessions: 2234 if 'd13Cwg_VPDB' not in self.sessions[session]: 2235 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2236 if 'd18Owg_VSMOW' not in self.sessions[session]: 2237 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2238 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2239 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2240 2241 self.msg(f'Computing repeatabilities for session {session}') 2242 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2243 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2244 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2245 2246 if self.standardization_method == 'pooled': 2247 for session in self.sessions: 2248 2249 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2250 i = self.standardization.var_names.index(f'a_{pf(session)}') 2251 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2252 2253 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2254 i = self.standardization.var_names.index(f'b_{pf(session)}') 2255 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2256 2257 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2258 i = self.standardization.var_names.index(f'c_{pf(session)}') 2259 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2260 2261 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2262 if self.sessions[session]['scrambling_drift']: 2263 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2264 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2265 else: 2266 self.sessions[session]['SE_a2'] = 0. 2267 2268 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2269 if self.sessions[session]['slope_drift']: 2270 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2271 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2272 else: 2273 self.sessions[session]['SE_b2'] = 0. 2274 2275 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2276 if self.sessions[session]['wg_drift']: 2277 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2278 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2279 else: 2280 self.sessions[session]['SE_c2'] = 0. 2281 2282 i = self.standardization.var_names.index(f'a_{pf(session)}') 2283 j = self.standardization.var_names.index(f'b_{pf(session)}') 2284 k = self.standardization.var_names.index(f'c_{pf(session)}') 2285 CM = np.zeros((6,6)) 2286 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2287 try: 2288 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2289 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2290 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2291 try: 2292 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2293 CM[3,4] = self.standardization.covar[i2,j2] 2294 CM[4,3] = self.standardization.covar[j2,i2] 2295 except ValueError: 2296 pass 2297 try: 2298 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2299 CM[3,5] = self.standardization.covar[i2,k2] 2300 CM[5,3] = self.standardization.covar[k2,i2] 2301 except ValueError: 2302 pass 2303 except ValueError: 2304 pass 2305 try: 2306 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2307 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2308 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2309 try: 2310 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2311 CM[4,5] = self.standardization.covar[j2,k2] 2312 CM[5,4] = self.standardization.covar[k2,j2] 2313 except ValueError: 2314 pass 2315 except ValueError: 2316 pass 2317 try: 2318 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2319 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2320 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2321 except ValueError: 2322 pass 2323 2324 self.sessions[session]['CM'] = CM 2325 2326 elif self.standardization_method == 'indep_sessions': 2327 pass # Not implemented yet 2328 2329 2330 @make_verbal 2331 def repeatabilities(self): 2332 ''' 2333 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2334 (for all samples, for anchors, and for unknowns). 2335 ''' 2336 self.msg('Computing reproducibilities for all sessions') 2337 2338 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2339 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2340 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2341 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2342 self.repeatability[f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', samples = 'all samples') 2343 2344 2345 @make_verbal 2346 def consolidate(self, tables = True, plots = True): 2347 ''' 2348 Collect information about samples, sessions and repeatabilities. 2349 ''' 2350 self.consolidate_samples() 2351 self.consolidate_sessions() 2352 self.repeatabilities() 2353 2354 if tables: 2355 self.summary() 2356 self.table_of_sessions() 2357 self.table_of_analyses() 2358 self.table_of_samples() 2359 2360 if plots: 2361 self.plot_sessions() 2362 2363 2364 @make_verbal 2365 def rmswd(self, 2366 samples = 'all samples', 2367 sessions = 'all sessions', 2368 ): 2369 ''' 2370 Compute the χ2, root mean squared weighted deviation 2371 (i.e. reduced χ2), and corresponding degrees of freedom of the 2372 Δ4x values for samples in `samples` and sessions in `sessions`. 2373 2374 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2375 ''' 2376 if samples == 'all samples': 2377 mysamples = [k for k in self.samples] 2378 elif samples == 'anchors': 2379 mysamples = [k for k in self.anchors] 2380 elif samples == 'unknowns': 2381 mysamples = [k for k in self.unknowns] 2382 else: 2383 mysamples = samples 2384 2385 if sessions == 'all sessions': 2386 sessions = [k for k in self.sessions] 2387 2388 chisq, Nf = 0, 0 2389 for sample in mysamples : 2390 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2391 if len(G) > 1 : 2392 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2393 Nf += (len(G) - 1) 2394 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2395 r = (chisq / Nf)**.5 if Nf > 0 else 0 2396 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2397 return {'rmswd': r, 'chisq': chisq, 'Nf': Nf} 2398 2399 2400 @make_verbal 2401 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2402 ''' 2403 Compute the repeatability of `[r[key] for r in self]` 2404 ''' 2405 # NB: it's debatable whether rD47 should be computed 2406 # with Nf = len(self)-len(self.samples) instead of 2407 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2408 2409 if samples == 'all samples': 2410 mysamples = [k for k in self.samples] 2411 elif samples == 'anchors': 2412 mysamples = [k for k in self.anchors] 2413 elif samples == 'unknowns': 2414 mysamples = [k for k in self.unknowns] 2415 else: 2416 mysamples = samples 2417 2418 if sessions == 'all sessions': 2419 sessions = [k for k in self.sessions] 2420 2421 if key in ['D47', 'D48']: 2422 chisq, Nf = 0, 0 2423 for sample in mysamples : 2424 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2425 if len(X) > 1 : 2426 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2427 if sample in self.unknowns: 2428 Nf += len(X) - 1 2429 else: 2430 Nf += len(X) 2431 if samples in ['anchors', 'all samples']: 2432 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2433 r = (chisq / Nf)**.5 if Nf > 0 else 0 2434 2435 else: # if key not in ['D47', 'D48'] 2436 chisq, Nf = 0, 0 2437 for sample in mysamples : 2438 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2439 if len(X) > 1 : 2440 Nf += len(X) - 1 2441 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2442 r = (chisq / Nf)**.5 if Nf > 0 else 0 2443 2444 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2445 return r 2446 2447 def sample_average(self, samples, weights = 'equal', normalize = True): 2448 ''' 2449 Weighted average Δ4x value of a group of samples, accounting for covariance. 2450 2451 Returns the weighed average Δ4x value and associated SE 2452 of a group of samples. Weights are equal by default. If `normalize` is 2453 true, `weights` will be rescaled so that their sum equals 1. 2454 2455 **Examples** 2456 2457 ```python 2458 self.sample_average(['X','Y'], [1, 2]) 2459 ``` 2460 2461 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2462 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2463 values of samples X and Y, respectively. 2464 2465 ```python 2466 self.sample_average(['X','Y'], [1, -1], normalize = False) 2467 ``` 2468 2469 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2470 ''' 2471 if weights == 'equal': 2472 weights = [1/len(samples)] * len(samples) 2473 2474 if normalize: 2475 s = sum(weights) 2476 if s: 2477 weights = [w/s for w in weights] 2478 2479 try: 2480# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2481# C = self.standardization.covar[indices,:][:,indices] 2482 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2483 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2484 return correlated_sum(X, C, weights) 2485 except ValueError: 2486 return (0., 0.) 2487 2488 2489 def sample_D4x_covar(self, sample1, sample2 = None): 2490 ''' 2491 Covariance between Δ4x values of samples 2492 2493 Returns the error covariance between the average Δ4x values of two 2494 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2495 returns the Δ4x variance for that sample. 2496 ''' 2497 if sample2 is None: 2498 sample2 = sample1 2499 if self.standardization_method == 'pooled': 2500 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2501 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2502 return self.standardization.covar[i, j] 2503 elif self.standardization_method == 'indep_sessions': 2504 if sample1 == sample2: 2505 return self.samples[sample1][f'SE_D{self._4x}']**2 2506 else: 2507 c = 0 2508 for session in self.sessions: 2509 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2510 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2511 if sdata1 and sdata2: 2512 a = self.sessions[session]['a'] 2513 # !! TODO: CM below does not account for temporal changes in standardization parameters 2514 CM = self.sessions[session]['CM'][:3,:3] 2515 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2516 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2517 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2518 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2519 c += ( 2520 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2521 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2522 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2523 @ CM 2524 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2525 ) / a**2 2526 return float(c) 2527 2528 def sample_D4x_correl(self, sample1, sample2 = None): 2529 ''' 2530 Correlation between Δ4x errors of samples 2531 2532 Returns the error correlation between the average Δ4x values of two samples. 2533 ''' 2534 if sample2 is None or sample2 == sample1: 2535 return 1. 2536 return ( 2537 self.sample_D4x_covar(sample1, sample2) 2538 / self.unknowns[sample1][f'SE_D{self._4x}'] 2539 / self.unknowns[sample2][f'SE_D{self._4x}'] 2540 ) 2541 2542 def plot_single_session(self, 2543 session, 2544 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2545 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2546 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2547 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2548 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2549 xylimits = 'free', # | 'constant' 2550 x_label = None, 2551 y_label = None, 2552 error_contour_interval = 'auto', 2553 fig = 'new', 2554 ): 2555 ''' 2556 Generate plot for a single session 2557 ''' 2558 if x_label is None: 2559 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2560 if y_label is None: 2561 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2562 2563 out = _SessionPlot() 2564 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2565 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2566 2567 if fig == 'new': 2568 out.fig = ppl.figure(figsize = (6,6)) 2569 ppl.subplots_adjust(.1,.1,.9,.9) 2570 2571 out.anchor_analyses, = ppl.plot( 2572 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2573 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2574 **kw_plot_anchors) 2575 out.unknown_analyses, = ppl.plot( 2576 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2577 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2578 **kw_plot_unknowns) 2579 out.anchor_avg = ppl.plot( 2580 np.array([ np.array([ 2581 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2582 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2583 ]) for sample in anchors]).T, 2584 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2585 **kw_plot_anchor_avg) 2586 out.unknown_avg = ppl.plot( 2587 np.array([ np.array([ 2588 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2589 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2590 ]) for sample in unknowns]).T, 2591 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2592 **kw_plot_unknown_avg) 2593 if xylimits == 'constant': 2594 x = [r[f'd{self._4x}'] for r in self] 2595 y = [r[f'D{self._4x}'] for r in self] 2596 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2597 w, h = x2-x1, y2-y1 2598 x1 -= w/20 2599 x2 += w/20 2600 y1 -= h/20 2601 y2 += h/20 2602 ppl.axis([x1, x2, y1, y2]) 2603 elif xylimits == 'free': 2604 x1, x2, y1, y2 = ppl.axis() 2605 else: 2606 x1, x2, y1, y2 = ppl.axis(xylimits) 2607 2608 if error_contour_interval != 'none': 2609 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2610 XI,YI = np.meshgrid(xi, yi) 2611 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2612 if error_contour_interval == 'auto': 2613 rng = np.max(SI) - np.min(SI) 2614 if rng <= 0.01: 2615 cinterval = 0.001 2616 elif rng <= 0.03: 2617 cinterval = 0.004 2618 elif rng <= 0.1: 2619 cinterval = 0.01 2620 elif rng <= 0.3: 2621 cinterval = 0.03 2622 elif rng <= 1.: 2623 cinterval = 0.1 2624 else: 2625 cinterval = 0.5 2626 else: 2627 cinterval = error_contour_interval 2628 2629 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2630 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2631 out.clabel = ppl.clabel(out.contour) 2632 2633 ppl.xlabel(x_label) 2634 ppl.ylabel(y_label) 2635 ppl.title(session, weight = 'bold') 2636 ppl.grid(alpha = .2) 2637 out.ax = ppl.gca() 2638 2639 return out 2640 2641 def plot_residuals( 2642 self, 2643 hist = False, 2644 binwidth = 2/3, 2645 dir = 'output', 2646 filename = None, 2647 highlight = [], 2648 colors = None, 2649 figsize = None, 2650 ): 2651 ''' 2652 Plot residuals of each analysis as a function of time (actually, as a function of 2653 the order of analyses in the `D4xdata` object) 2654 2655 + `hist`: whether to add a histogram of residuals 2656 + `histbins`: specify bin edges for the histogram 2657 + `dir`: the directory in which to save the plot 2658 + `highlight`: a list of samples to highlight 2659 + `colors`: a dict of `{<sample>: <color>}` for all samples 2660 + `figsize`: (width, height) of figure 2661 ''' 2662 # Layout 2663 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2664 if hist: 2665 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2666 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2667 else: 2668 ppl.subplots_adjust(.08,.05,.78,.8) 2669 ax1 = ppl.subplot(111) 2670 2671 # Colors 2672 N = len(self.anchors) 2673 if colors is None: 2674 if len(highlight) > 0: 2675 Nh = len(highlight) 2676 if Nh == 1: 2677 colors = {highlight[0]: (0,0,0)} 2678 elif Nh == 3: 2679 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2680 elif Nh == 4: 2681 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2682 else: 2683 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2684 else: 2685 if N == 3: 2686 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2687 elif N == 4: 2688 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2689 else: 2690 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2691 2692 ppl.sca(ax1) 2693 2694 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2695 2696 session = self[0]['Session'] 2697 x1 = 0 2698# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2699 x_sessions = {} 2700 one_or_more_singlets = False 2701 one_or_more_multiplets = False 2702 multiplets = set() 2703 for k,r in enumerate(self): 2704 if r['Session'] != session: 2705 x2 = k-1 2706 x_sessions[session] = (x1+x2)/2 2707 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2708 session = r['Session'] 2709 x1 = k 2710 singlet = len(self.samples[r['Sample']]['data']) == 1 2711 if not singlet: 2712 multiplets.add(r['Sample']) 2713 if r['Sample'] in self.unknowns: 2714 if singlet: 2715 one_or_more_singlets = True 2716 else: 2717 one_or_more_multiplets = True 2718 kw = dict( 2719 marker = 'x' if singlet else '+', 2720 ms = 4 if singlet else 5, 2721 ls = 'None', 2722 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2723 mew = 1, 2724 alpha = 0.2 if singlet else 1, 2725 ) 2726 if highlight and r['Sample'] not in highlight: 2727 kw['alpha'] = 0.2 2728 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2729 x2 = k 2730 x_sessions[session] = (x1+x2)/2 2731 2732 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2733 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2734 if not hist: 2735 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2736 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') 2737 2738 xmin, xmax, ymin, ymax = ppl.axis() 2739 for s in x_sessions: 2740 ppl.text( 2741 x_sessions[s], 2742 ymax +1, 2743 s, 2744 va = 'bottom', 2745 **( 2746 dict(ha = 'center') 2747 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2748 else dict(ha = 'left', rotation = 45) 2749 ) 2750 ) 2751 2752 if hist: 2753 ppl.sca(ax2) 2754 2755 for s in colors: 2756 kw['marker'] = '+' 2757 kw['ms'] = 5 2758 kw['mec'] = colors[s] 2759 kw['label'] = s 2760 kw['alpha'] = 1 2761 ppl.plot([], [], **kw) 2762 2763 kw['mec'] = (0,0,0) 2764 2765 if one_or_more_singlets: 2766 kw['marker'] = 'x' 2767 kw['ms'] = 4 2768 kw['alpha'] = .2 2769 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2770 ppl.plot([], [], **kw) 2771 2772 if one_or_more_multiplets: 2773 kw['marker'] = '+' 2774 kw['ms'] = 4 2775 kw['alpha'] = 1 2776 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2777 ppl.plot([], [], **kw) 2778 2779 if hist: 2780 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2781 else: 2782 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2783 leg.set_zorder(-1000) 2784 2785 ppl.sca(ax1) 2786 2787 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2788 ppl.xticks([]) 2789 ppl.axis([-1, len(self), None, None]) 2790 2791 if hist: 2792 ppl.sca(ax2) 2793 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2794 ppl.hist( 2795 X, 2796 orientation = 'horizontal', 2797 histtype = 'stepfilled', 2798 ec = [.4]*3, 2799 fc = [.25]*3, 2800 alpha = .25, 2801 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2802 ) 2803 ppl.axis([None, None, ymin, ymax]) 2804 ppl.text(0, 0, 2805 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2806 size = 8, 2807 alpha = 1, 2808 va = 'center', 2809 ha = 'left', 2810 ) 2811 2812 ppl.xticks([]) 2813 ppl.yticks([]) 2814# ax2.spines['left'].set_visible(False) 2815 ax2.spines['right'].set_visible(False) 2816 ax2.spines['top'].set_visible(False) 2817 ax2.spines['bottom'].set_visible(False) 2818 2819 2820 if not os.path.exists(dir): 2821 os.makedirs(dir) 2822 if filename is None: 2823 return fig 2824 elif filename == '': 2825 filename = f'D{self._4x}_residuals.pdf' 2826 ppl.savefig(f'{dir}/{filename}') 2827 ppl.close(fig) 2828 2829 2830 def simulate(self, *args, **kwargs): 2831 ''' 2832 Legacy function with warning message pointing to `virtual_data()` 2833 ''' 2834 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()') 2835 2836 def plot_distribution_of_analyses( 2837 self, 2838 dir = 'output', 2839 filename = None, 2840 vs_time = False, 2841 figsize = (6,4), 2842 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2843 output = None, 2844 ): 2845 ''' 2846 Plot temporal distribution of all analyses in the data set. 2847 2848 **Parameters** 2849 2850 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2851 ''' 2852 2853 asamples = [s for s in self.anchors] 2854 usamples = [s for s in self.unknowns] 2855 if output is None or output == 'fig': 2856 fig = ppl.figure(figsize = figsize) 2857 ppl.subplots_adjust(*subplots_adjust) 2858 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2859 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2860 Xmax += (Xmax-Xmin)/40 2861 Xmin -= (Xmax-Xmin)/41 2862 for k, s in enumerate(asamples + usamples): 2863 if vs_time: 2864 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2865 else: 2866 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2867 Y = [-k for x in X] 2868 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2869 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2870 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2871 ppl.axis([Xmin, Xmax, -k-1, 1]) 2872 ppl.xlabel('\ntime') 2873 ppl.gca().annotate('', 2874 xy = (0.6, -0.02), 2875 xycoords = 'axes fraction', 2876 xytext = (.4, -0.02), 2877 arrowprops = dict(arrowstyle = "->", color = 'k'), 2878 ) 2879 2880 2881 x2 = -1 2882 for session in self.sessions: 2883 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2884 if vs_time: 2885 ppl.axvline(x1, color = 'k', lw = .75) 2886 if x2 > -1: 2887 if not vs_time: 2888 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2889 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2890# from xlrd import xldate_as_datetime 2891# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2892 if vs_time: 2893 ppl.axvline(x2, color = 'k', lw = .75) 2894 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2895 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2896 2897 ppl.xticks([]) 2898 ppl.yticks([]) 2899 2900 if output is None: 2901 if not os.path.exists(dir): 2902 os.makedirs(dir) 2903 if filename == None: 2904 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2905 ppl.savefig(f'{dir}/{filename}') 2906 ppl.close(fig) 2907 elif output == 'ax': 2908 return ppl.gca() 2909 elif output == 'fig': 2910 return fig
Store and process data for a large set of Δ47 and/or Δ48 analyses, usually comprising more than one analytical session.
972 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 973 ''' 974 **Parameters** 975 976 + `l`: a list of dictionaries, with each dictionary including at least the keys 977 `Sample`, `d45`, `d46`, and `d47` or `d48`. 978 + `mass`: `'47'` or `'48'` 979 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 980 + `session`: define session name for analyses without a `Session` key 981 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 982 983 Returns a `D4xdata` object derived from `list`. 984 ''' 985 self._4x = mass 986 self.verbose = verbose 987 self.prefix = 'D4xdata' 988 self.logfile = logfile 989 list.__init__(self, l) 990 self.Nf = None 991 self.repeatability = {} 992 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).
995 def make_verbal(oldfun): 996 ''' 997 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 998 ''' 999 @wraps(oldfun) 1000 def newfun(*args, verbose = '', **kwargs): 1001 myself = args[0] 1002 oldprefix = myself.prefix 1003 myself.prefix = oldfun.__name__ 1004 if verbose != '': 1005 oldverbose = myself.verbose 1006 myself.verbose = verbose 1007 out = oldfun(*args, **kwargs) 1008 myself.prefix = oldprefix 1009 if verbose != '': 1010 myself.verbose = oldverbose 1011 return out 1012 return newfun
Decorator: allow temporarily changing self.prefix
and overriding self.verbose
.
1015 def msg(self, txt): 1016 ''' 1017 Log a message to `self.logfile`, and print it out if `verbose = True` 1018 ''' 1019 self.log(txt) 1020 if self.verbose: 1021 print(f'{f"[{self.prefix}]":<16} {txt}')
Log a message to self.logfile
, and print it out if verbose = True
1024 def vmsg(self, txt): 1025 ''' 1026 Log a message to `self.logfile` and print it out 1027 ''' 1028 self.log(txt) 1029 print(txt)
Log a message to self.logfile
and print it out
1032 def log(self, *txts): 1033 ''' 1034 Log a message to `self.logfile` 1035 ''' 1036 if self.logfile: 1037 with open(self.logfile, 'a') as fid: 1038 for txt in txts: 1039 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}')
Log a message to self.logfile
1042 def refresh(self, session = 'mySession'): 1043 ''' 1044 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1045 ''' 1046 self.fill_in_missing_info(session = session) 1047 self.refresh_sessions() 1048 self.refresh_samples()
Update self.sessions
, self.samples
, self.anchors
, and self.unknowns
.
1051 def refresh_sessions(self): 1052 ''' 1053 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1054 to `False` for all sessions. 1055 ''' 1056 self.sessions = { 1057 s: {'data': [r for r in self if r['Session'] == s]} 1058 for s in sorted({r['Session'] for r in self}) 1059 } 1060 for s in self.sessions: 1061 self.sessions[s]['scrambling_drift'] = False 1062 self.sessions[s]['slope_drift'] = False 1063 self.sessions[s]['wg_drift'] = False 1064 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1065 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.
1068 def refresh_samples(self): 1069 ''' 1070 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1071 ''' 1072 self.samples = { 1073 s: {'data': [r for r in self if r['Sample'] == s]} 1074 for s in sorted({r['Sample'] for r in self}) 1075 } 1076 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1077 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
.
1080 def read(self, filename, sep = '', session = ''): 1081 ''' 1082 Read file in csv format to load data into a `D47data` object. 1083 1084 In the csv file, spaces before and after field separators (`','` by default) 1085 are optional. Each line corresponds to a single analysis. 1086 1087 The required fields are: 1088 1089 + `UID`: a unique identifier 1090 + `Session`: an identifier for the analytical session 1091 + `Sample`: a sample identifier 1092 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1093 1094 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1095 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1096 and `d49` are optional, and set to NaN by default. 1097 1098 **Parameters** 1099 1100 + `fileneme`: the path of the file to read 1101 + `sep`: csv separator delimiting the fields 1102 + `session`: set `Session` field to this string for all analyses 1103 ''' 1104 with open(filename) as fid: 1105 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
1108 def input(self, txt, sep = '', session = ''): 1109 ''' 1110 Read `txt` string in csv format to load analysis data into a `D47data` object. 1111 1112 In the csv string, spaces before and after field separators (`','` by default) 1113 are optional. Each line corresponds to a single analysis. 1114 1115 The required fields are: 1116 1117 + `UID`: a unique identifier 1118 + `Session`: an identifier for the analytical session 1119 + `Sample`: a sample identifier 1120 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1121 1122 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1123 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1124 and `d49` are optional, and set to NaN by default. 1125 1126 **Parameters** 1127 1128 + `txt`: the csv string to read 1129 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1130 whichever appers most often in `txt`. 1131 + `session`: set `Session` field to this string for all analyses 1132 ''' 1133 if sep == '': 1134 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1135 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1136 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:]] 1137 1138 if session != '': 1139 for r in data: 1140 r['Session'] = session 1141 1142 self += data 1143 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
1146 @make_verbal 1147 def wg(self, samples = None, a18_acid = None): 1148 ''' 1149 Compute bulk composition of the working gas for each session based on 1150 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1151 `self.Nominal_d18O_VPDB`. 1152 ''' 1153 1154 self.msg('Computing WG composition:') 1155 1156 if a18_acid is None: 1157 a18_acid = self.ALPHA_18O_ACID_REACTION 1158 if samples is None: 1159 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1160 1161 assert a18_acid, f'Acid fractionation factor should not be zero.' 1162 1163 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1164 R45R46_standards = {} 1165 for sample in samples: 1166 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1167 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1168 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1169 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1170 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1171 1172 C12_s = 1 / (1 + R13_s) 1173 C13_s = R13_s / (1 + R13_s) 1174 C16_s = 1 / (1 + R17_s + R18_s) 1175 C17_s = R17_s / (1 + R17_s + R18_s) 1176 C18_s = R18_s / (1 + R17_s + R18_s) 1177 1178 C626_s = C12_s * C16_s ** 2 1179 C627_s = 2 * C12_s * C16_s * C17_s 1180 C628_s = 2 * C12_s * C16_s * C18_s 1181 C636_s = C13_s * C16_s ** 2 1182 C637_s = 2 * C13_s * C16_s * C17_s 1183 C727_s = C12_s * C17_s ** 2 1184 1185 R45_s = (C627_s + C636_s) / C626_s 1186 R46_s = (C628_s + C637_s + C727_s) / C626_s 1187 R45R46_standards[sample] = (R45_s, R46_s) 1188 1189 for s in self.sessions: 1190 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1191 assert db, f'No sample from {samples} found in session "{s}".' 1192# dbsamples = sorted({r['Sample'] for r in db}) 1193 1194 X = [r['d45'] for r in db] 1195 Y = [R45R46_standards[r['Sample']][0] for r in db] 1196 x1, x2 = np.min(X), np.max(X) 1197 1198 if x1 < x2: 1199 wgcoord = x1/(x1-x2) 1200 else: 1201 wgcoord = 999 1202 1203 if wgcoord < -.5 or wgcoord > 1.5: 1204 # unreasonable to extrapolate to d45 = 0 1205 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1206 else : 1207 # d45 = 0 is reasonably well bracketed 1208 R45_wg = np.polyfit(X, Y, 1)[1] 1209 1210 X = [r['d46'] for r in db] 1211 Y = [R45R46_standards[r['Sample']][1] for r in db] 1212 x1, x2 = np.min(X), np.max(X) 1213 1214 if x1 < x2: 1215 wgcoord = x1/(x1-x2) 1216 else: 1217 wgcoord = 999 1218 1219 if wgcoord < -.5 or wgcoord > 1.5: 1220 # unreasonable to extrapolate to d46 = 0 1221 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1222 else : 1223 # d46 = 0 is reasonably well bracketed 1224 R46_wg = np.polyfit(X, Y, 1)[1] 1225 1226 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1227 1228 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1229 1230 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1231 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1232 for r in self.sessions[s]['data']: 1233 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1234 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
.
1237 def compute_bulk_delta(self, R45, R46, D17O = 0): 1238 ''' 1239 Compute δ13C_VPDB and δ18O_VSMOW, 1240 by solving the generalized form of equation (17) from 1241 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1242 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1243 solving the corresponding second-order Taylor polynomial. 1244 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1245 ''' 1246 1247 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1248 1249 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1250 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1251 C = 2 * self.R18_VSMOW 1252 D = -R46 1253 1254 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1255 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1256 cc = A + B + C + D 1257 1258 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1259 1260 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1261 R17 = K * R18 ** self.LAMBDA_17 1262 R13 = R45 - 2 * R17 1263 1264 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1265 1266 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)
1269 @make_verbal 1270 def crunch(self, verbose = ''): 1271 ''' 1272 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1273 ''' 1274 for r in self: 1275 self.compute_bulk_and_clumping_deltas(r) 1276 self.standardize_d13C() 1277 self.standardize_d18O() 1278 self.msg(f"Crunched {len(self)} analyses.")
Compute bulk composition and raw clumped isotope anomalies for all analyses.
1281 def fill_in_missing_info(self, session = 'mySession'): 1282 ''' 1283 Fill in optional fields with default values 1284 ''' 1285 for i,r in enumerate(self): 1286 if 'D17O' not in r: 1287 r['D17O'] = 0. 1288 if 'UID' not in r: 1289 r['UID'] = f'{i+1}' 1290 if 'Session' not in r: 1291 r['Session'] = session 1292 for k in ['d47', 'd48', 'd49']: 1293 if k not in r: 1294 r[k] = np.nan
Fill in optional fields with default values
1297 def standardize_d13C(self): 1298 ''' 1299 Perform δ13C standadization within each session `s` according to 1300 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1301 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1302 may be redefined abitrarily at a later stage. 1303 ''' 1304 for s in self.sessions: 1305 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1306 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] 1307 X,Y = zip(*XY) 1308 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1309 offset = np.mean(Y) - np.mean(X) 1310 for r in self.sessions[s]['data']: 1311 r['d13C_VPDB'] += offset 1312 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1313 a,b = np.polyfit(X,Y,1) 1314 for r in self.sessions[s]['data']: 1315 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.
1317 def standardize_d18O(self): 1318 ''' 1319 Perform δ18O standadization within each session `s` according to 1320 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1321 which is defined by default by `D47data.refresh_sessions()`as equal to 1322 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1323 ''' 1324 for s in self.sessions: 1325 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1326 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] 1327 X,Y = zip(*XY) 1328 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1329 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1330 offset = np.mean(Y) - np.mean(X) 1331 for r in self.sessions[s]['data']: 1332 r['d18O_VSMOW'] += offset 1333 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1334 a,b = np.polyfit(X,Y,1) 1335 for r in self.sessions[s]['data']: 1336 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.
1339 def compute_bulk_and_clumping_deltas(self, r): 1340 ''' 1341 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1342 ''' 1343 1344 # Compute working gas R13, R18, and isobar ratios 1345 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1346 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1347 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1348 1349 # Compute analyte isobar ratios 1350 R45 = (1 + r['d45'] / 1000) * R45_wg 1351 R46 = (1 + r['d46'] / 1000) * R46_wg 1352 R47 = (1 + r['d47'] / 1000) * R47_wg 1353 R48 = (1 + r['d48'] / 1000) * R48_wg 1354 R49 = (1 + r['d49'] / 1000) * R49_wg 1355 1356 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1357 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1358 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1359 1360 # Compute stochastic isobar ratios of the analyte 1361 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1362 R13, R18, D17O = r['D17O'] 1363 ) 1364 1365 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1366 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1367 if (R45 / R45stoch - 1) > 5e-8: 1368 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1369 if (R46 / R46stoch - 1) > 5e-8: 1370 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1371 1372 # Compute raw clumped isotope anomalies 1373 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1374 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1375 r['D49raw'] = 1000 * (R49 / R49stoch - 1)
Compute δ13CVPDB, δ18OVSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis r
.
1378 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1379 ''' 1380 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1381 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1382 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1383 ''' 1384 1385 # Compute R17 1386 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1387 1388 # Compute isotope concentrations 1389 C12 = (1 + R13) ** -1 1390 C13 = C12 * R13 1391 C16 = (1 + R17 + R18) ** -1 1392 C17 = C16 * R17 1393 C18 = C16 * R18 1394 1395 # Compute stochastic isotopologue concentrations 1396 C626 = C16 * C12 * C16 1397 C627 = C16 * C12 * C17 * 2 1398 C628 = C16 * C12 * C18 * 2 1399 C636 = C16 * C13 * C16 1400 C637 = C16 * C13 * C17 * 2 1401 C638 = C16 * C13 * C18 * 2 1402 C727 = C17 * C12 * C17 1403 C728 = C17 * C12 * C18 * 2 1404 C737 = C17 * C13 * C17 1405 C738 = C17 * C13 * C18 * 2 1406 C828 = C18 * C12 * C18 1407 C838 = C18 * C13 * C18 1408 1409 # Compute stochastic isobar ratios 1410 R45 = (C636 + C627) / C626 1411 R46 = (C628 + C637 + C727) / C626 1412 R47 = (C638 + C728 + C737) / C626 1413 R48 = (C738 + C828) / C626 1414 R49 = C838 / C626 1415 1416 # Account for stochastic anomalies 1417 R47 *= 1 + D47 / 1000 1418 R48 *= 1 + D48 / 1000 1419 R49 *= 1 + D49 / 1000 1420 1421 # Return isobar ratios 1422 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.
1425 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1426 ''' 1427 Split unknown samples by UID (treat all analyses as different samples) 1428 or by session (treat analyses of a given sample in different sessions as 1429 different samples). 1430 1431 **Parameters** 1432 1433 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1434 + `grouping`: `by_uid` | `by_session` 1435 ''' 1436 if samples_to_split == 'all': 1437 samples_to_split = [s for s in self.unknowns] 1438 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1439 self.grouping = grouping.lower() 1440 if self.grouping in gkeys: 1441 gkey = gkeys[self.grouping] 1442 for r in self: 1443 if r['Sample'] in samples_to_split: 1444 r['Sample_original'] = r['Sample'] 1445 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1446 elif r['Sample'] in self.unknowns: 1447 r['Sample_original'] = r['Sample'] 1448 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
1451 def unsplit_samples(self, tables = False): 1452 ''' 1453 Reverse the effects of `D47data.split_samples()`. 1454 1455 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1456 1457 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1458 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1459 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1460 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1461 that case session-averaged Δ4x values are statistically independent). 1462 ''' 1463 unknowns_old = sorted({s for s in self.unknowns}) 1464 CM_old = self.standardization.covar[:,:] 1465 VD_old = self.standardization.params.valuesdict().copy() 1466 vars_old = self.standardization.var_names 1467 1468 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1469 1470 Ns = len(vars_old) - len(unknowns_old) 1471 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1472 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1473 1474 W = np.zeros((len(vars_new), len(vars_old))) 1475 W[:Ns,:Ns] = np.eye(Ns) 1476 for u in unknowns_new: 1477 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1478 if self.grouping == 'by_session': 1479 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1480 elif self.grouping == 'by_uid': 1481 weights = [1 for s in splits] 1482 sw = sum(weights) 1483 weights = [w/sw for w in weights] 1484 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1485 1486 CM_new = W @ CM_old @ W.T 1487 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1488 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1489 1490 self.standardization.covar = CM_new 1491 self.standardization.params.valuesdict = lambda : VD_new 1492 self.standardization.var_names = vars_new 1493 1494 for r in self: 1495 if r['Sample'] in self.unknowns: 1496 r['Sample_split'] = r['Sample'] 1497 r['Sample'] = r['Sample_original'] 1498 1499 self.refresh_samples() 1500 self.consolidate_samples() 1501 self.repeatabilities() 1502 1503 if tables: 1504 self.table_of_analyses() 1505 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).
1507 def assign_timestamps(self): 1508 ''' 1509 Assign a time field `t` of type `float` to each analysis. 1510 1511 If `TimeTag` is one of the data fields, `t` is equal within a given session 1512 to `TimeTag` minus the mean value of `TimeTag` for that session. 1513 Otherwise, `TimeTag` is by default equal to the index of each analysis 1514 in the dataset and `t` is defined as above. 1515 ''' 1516 for session in self.sessions: 1517 sdata = self.sessions[session]['data'] 1518 try: 1519 t0 = np.mean([r['TimeTag'] for r in sdata]) 1520 for r in sdata: 1521 r['t'] = r['TimeTag'] - t0 1522 except KeyError: 1523 t0 = (len(sdata)-1)/2 1524 for t,r in enumerate(sdata): 1525 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.
1528 def report(self): 1529 ''' 1530 Prints a report on the standardization fit. 1531 Only applicable after `D4xdata.standardize(method='pooled')`. 1532 ''' 1533 report_fit(self.standardization)
Prints a report on the standardization fit.
Only applicable after D4xdata.standardize(method='pooled')
.
1536 def combine_samples(self, sample_groups): 1537 ''' 1538 Combine analyses of different samples to compute weighted average Δ4x 1539 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1540 dictionary. 1541 1542 Caution: samples are weighted by number of replicate analyses, which is a 1543 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1544 correlated analytical errors for one or more samples). 1545 1546 Returns a tuplet of: 1547 1548 + the list of group names 1549 + an array of the corresponding Δ4x values 1550 + the corresponding (co)variance matrix 1551 1552 **Parameters** 1553 1554 + `sample_groups`: a dictionary of the form: 1555 ```py 1556 {'group1': ['sample_1', 'sample_2'], 1557 'group2': ['sample_3', 'sample_4', 'sample_5']} 1558 ``` 1559 ''' 1560 1561 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1562 groups = sorted(sample_groups.keys()) 1563 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1564 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1565 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1566 W = np.array([ 1567 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1568 for j in groups]) 1569 D4x_new = W @ D4x_old 1570 CM_new = W @ CM_old @ W.T 1571 1572 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']}
1575 @make_verbal 1576 def standardize(self, 1577 method = 'pooled', 1578 weighted_sessions = [], 1579 consolidate = True, 1580 consolidate_tables = False, 1581 consolidate_plots = False, 1582 constraints = {}, 1583 ): 1584 ''' 1585 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1586 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1587 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1588 i.e. that their true Δ4x value does not change between sessions, 1589 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1590 `'indep_sessions'`, the standardization processes each session independently, based only 1591 on anchors analyses. 1592 ''' 1593 1594 self.standardization_method = method 1595 self.assign_timestamps() 1596 1597 if method == 'pooled': 1598 if weighted_sessions: 1599 for session_group in weighted_sessions: 1600 if self._4x == '47': 1601 X = D47data([r for r in self if r['Session'] in session_group]) 1602 elif self._4x == '48': 1603 X = D48data([r for r in self if r['Session'] in session_group]) 1604 X.Nominal_D4x = self.Nominal_D4x.copy() 1605 X.refresh() 1606 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1607 w = np.sqrt(result.redchi) 1608 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1609 for r in X: 1610 r[f'wD{self._4x}raw'] *= w 1611 else: 1612 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1613 for r in self: 1614 r[f'wD{self._4x}raw'] = 1. 1615 1616 params = Parameters() 1617 for k,session in enumerate(self.sessions): 1618 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1619 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1620 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1621 s = pf(session) 1622 params.add(f'a_{s}', value = 0.9) 1623 params.add(f'b_{s}', value = 0.) 1624 params.add(f'c_{s}', value = -0.9) 1625 params.add(f'a2_{s}', value = 0., vary = self.sessions[session]['scrambling_drift']) 1626 params.add(f'b2_{s}', value = 0., vary = self.sessions[session]['slope_drift']) 1627 params.add(f'c2_{s}', value = 0., vary = self.sessions[session]['wg_drift']) 1628 for sample in self.unknowns: 1629 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1630 1631 for k in constraints: 1632 params[k].expr = constraints[k] 1633 1634 def residuals(p): 1635 R = [] 1636 for r in self: 1637 session = pf(r['Session']) 1638 sample = pf(r['Sample']) 1639 if r['Sample'] in self.Nominal_D4x: 1640 R += [ ( 1641 r[f'D{self._4x}raw'] - ( 1642 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1643 + p[f'b_{session}'] * r[f'd{self._4x}'] 1644 + p[f'c_{session}'] 1645 + r['t'] * ( 1646 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1647 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1648 + p[f'c2_{session}'] 1649 ) 1650 ) 1651 ) / r[f'wD{self._4x}raw'] ] 1652 else: 1653 R += [ ( 1654 r[f'D{self._4x}raw'] - ( 1655 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1656 + p[f'b_{session}'] * r[f'd{self._4x}'] 1657 + p[f'c_{session}'] 1658 + r['t'] * ( 1659 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1660 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1661 + p[f'c2_{session}'] 1662 ) 1663 ) 1664 ) / r[f'wD{self._4x}raw'] ] 1665 return R 1666 1667 M = Minimizer(residuals, params) 1668 result = M.least_squares() 1669 self.Nf = result.nfree 1670 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1671# if self.verbose: 1672# report_fit(result) 1673 1674 for r in self: 1675 s = pf(r["Session"]) 1676 a = result.params.valuesdict()[f'a_{s}'] 1677 b = result.params.valuesdict()[f'b_{s}'] 1678 c = result.params.valuesdict()[f'c_{s}'] 1679 a2 = result.params.valuesdict()[f'a2_{s}'] 1680 b2 = result.params.valuesdict()[f'b2_{s}'] 1681 c2 = result.params.valuesdict()[f'c2_{s}'] 1682 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']) 1683 1684 self.standardization = result 1685 1686 for session in self.sessions: 1687 self.sessions[session]['Np'] = 3 1688 for k in ['scrambling', 'slope', 'wg']: 1689 if self.sessions[session][f'{k}_drift']: 1690 self.sessions[session]['Np'] += 1 1691 1692 if consolidate: 1693 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1694 return result 1695 1696 1697 elif method == 'indep_sessions': 1698 1699 if weighted_sessions: 1700 for session_group in weighted_sessions: 1701 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1702 X.Nominal_D4x = self.Nominal_D4x.copy() 1703 X.refresh() 1704 # This is only done to assign r['wD47raw'] for r in X: 1705 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1706 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}') 1707 else: 1708 self.msg('All weights set to 1 ‰') 1709 for r in self: 1710 r[f'wD{self._4x}raw'] = 1 1711 1712 for session in self.sessions: 1713 s = self.sessions[session] 1714 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1715 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1716 s['Np'] = sum(p_active) 1717 sdata = s['data'] 1718 1719 A = np.array([ 1720 [ 1721 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1722 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1723 1 / r[f'wD{self._4x}raw'], 1724 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1725 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1726 r['t'] / r[f'wD{self._4x}raw'] 1727 ] 1728 for r in sdata if r['Sample'] in self.anchors 1729 ])[:,p_active] # only keep columns for the active parameters 1730 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]) 1731 s['Na'] = Y.size 1732 CM = linalg.inv(A.T @ A) 1733 bf = (CM @ A.T @ Y).T[0,:] 1734 k = 0 1735 for n,a in zip(p_names, p_active): 1736 if a: 1737 s[n] = bf[k] 1738# self.msg(f'{n} = {bf[k]}') 1739 k += 1 1740 else: 1741 s[n] = 0. 1742# self.msg(f'{n} = 0.0') 1743 1744 for r in sdata : 1745 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1746 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']) 1747 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1748 1749 s['CM'] = np.zeros((6,6)) 1750 i = 0 1751 k_active = [j for j,a in enumerate(p_active) if a] 1752 for j,a in enumerate(p_active): 1753 if a: 1754 s['CM'][j,k_active] = CM[i,:] 1755 i += 1 1756 1757 if not weighted_sessions: 1758 w = self.rmswd()['rmswd'] 1759 for r in self: 1760 r[f'wD{self._4x}'] *= w 1761 r[f'wD{self._4x}raw'] *= w 1762 for session in self.sessions: 1763 self.sessions[session]['CM'] *= w**2 1764 1765 for session in self.sessions: 1766 s = self.sessions[session] 1767 s['SE_a'] = s['CM'][0,0]**.5 1768 s['SE_b'] = s['CM'][1,1]**.5 1769 s['SE_c'] = s['CM'][2,2]**.5 1770 s['SE_a2'] = s['CM'][3,3]**.5 1771 s['SE_b2'] = s['CM'][4,4]**.5 1772 s['SE_c2'] = s['CM'][5,5]**.5 1773 1774 if not weighted_sessions: 1775 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1776 else: 1777 self.Nf = 0 1778 for sg in weighted_sessions: 1779 self.Nf += self.rmswd(sessions = sg)['Nf'] 1780 1781 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1782 1783 avgD4x = { 1784 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1785 for sample in self.samples 1786 } 1787 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1788 rD4x = (chi2/self.Nf)**.5 1789 self.repeatability[f'sigma_{self._4x}'] = rD4x 1790 1791 if consolidate: 1792 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.
1795 def standardization_error(self, session, d4x, D4x, t = 0): 1796 ''' 1797 Compute standardization error for a given session and 1798 (δ47, Δ47) composition. 1799 ''' 1800 a = self.sessions[session]['a'] 1801 b = self.sessions[session]['b'] 1802 c = self.sessions[session]['c'] 1803 a2 = self.sessions[session]['a2'] 1804 b2 = self.sessions[session]['b2'] 1805 c2 = self.sessions[session]['c2'] 1806 CM = self.sessions[session]['CM'] 1807 1808 x, y = D4x, d4x 1809 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1810# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1811 dxdy = -(b+b2*t) / (a+a2*t) 1812 dxdz = 1. / (a+a2*t) 1813 dxda = -x / (a+a2*t) 1814 dxdb = -y / (a+a2*t) 1815 dxdc = -1. / (a+a2*t) 1816 dxda2 = -x * a2 / (a+a2*t) 1817 dxdb2 = -y * t / (a+a2*t) 1818 dxdc2 = -t / (a+a2*t) 1819 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1820 sx = (V @ CM @ V.T) ** .5 1821 return sx
Compute standardization error for a given session and (δ47, Δ47) composition.
1824 @make_verbal 1825 def summary(self, 1826 dir = 'output', 1827 filename = None, 1828 save_to_file = True, 1829 print_out = True, 1830 ): 1831 ''' 1832 Print out an/or save to disk a summary of the standardization results. 1833 1834 **Parameters** 1835 1836 + `dir`: the directory in which to save the table 1837 + `filename`: the name to the csv file to write to 1838 + `save_to_file`: whether to save the table to disk 1839 + `print_out`: whether to print out the table 1840 ''' 1841 1842 out = [] 1843 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1844 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])})"]] 1845 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1846 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1847 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1848 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1849 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1850 out += [['Model degrees of freedom', f"{self.Nf}"]] 1851 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1852 out += [['Standardization method', self.standardization_method]] 1853 1854 if save_to_file: 1855 if not os.path.exists(dir): 1856 os.makedirs(dir) 1857 if filename is None: 1858 filename = f'D{self._4x}_summary.csv' 1859 with open(f'{dir}/{filename}', 'w') as fid: 1860 fid.write(make_csv(out)) 1861 if print_out: 1862 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
1865 @make_verbal 1866 def table_of_sessions(self, 1867 dir = 'output', 1868 filename = None, 1869 save_to_file = True, 1870 print_out = True, 1871 output = None, 1872 ): 1873 ''' 1874 Print out an/or save to disk a table of sessions. 1875 1876 **Parameters** 1877 1878 + `dir`: the directory in which to save the table 1879 + `filename`: the name to the csv file to write to 1880 + `save_to_file`: whether to save the table to disk 1881 + `print_out`: whether to print out the table 1882 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1883 if set to `'raw'`: return a list of list of strings 1884 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1885 ''' 1886 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1887 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1888 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1889 1890 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']] 1891 if include_a2: 1892 out[-1] += ['a2 ± SE'] 1893 if include_b2: 1894 out[-1] += ['b2 ± SE'] 1895 if include_c2: 1896 out[-1] += ['c2 ± SE'] 1897 for session in self.sessions: 1898 out += [[ 1899 session, 1900 f"{self.sessions[session]['Na']}", 1901 f"{self.sessions[session]['Nu']}", 1902 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1903 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1904 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1905 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1906 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1907 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1908 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1909 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1910 ]] 1911 if include_a2: 1912 if self.sessions[session]['scrambling_drift']: 1913 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1914 else: 1915 out[-1] += [''] 1916 if include_b2: 1917 if self.sessions[session]['slope_drift']: 1918 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1919 else: 1920 out[-1] += [''] 1921 if include_c2: 1922 if self.sessions[session]['wg_drift']: 1923 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1924 else: 1925 out[-1] += [''] 1926 1927 if save_to_file: 1928 if not os.path.exists(dir): 1929 os.makedirs(dir) 1930 if filename is None: 1931 filename = f'D{self._4x}_sessions.csv' 1932 with open(f'{dir}/{filename}', 'w') as fid: 1933 fid.write(make_csv(out)) 1934 if print_out: 1935 self.msg('\n' + pretty_table(out)) 1936 if output == 'raw': 1937 return out 1938 elif output == 'pretty': 1939 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']]
)
1942 @make_verbal 1943 def table_of_analyses( 1944 self, 1945 dir = 'output', 1946 filename = None, 1947 save_to_file = True, 1948 print_out = True, 1949 output = None, 1950 ): 1951 ''' 1952 Print out an/or save to disk a table of analyses. 1953 1954 **Parameters** 1955 1956 + `dir`: the directory in which to save the table 1957 + `filename`: the name to the csv file to write to 1958 + `save_to_file`: whether to save the table to disk 1959 + `print_out`: whether to print out the table 1960 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1961 if set to `'raw'`: return a list of list of strings 1962 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1963 ''' 1964 1965 out = [['UID','Session','Sample']] 1966 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}] 1967 for f in extra_fields: 1968 out[-1] += [f[0]] 1969 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 1970 for r in self: 1971 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 1972 for f in extra_fields: 1973 out[-1] += [f"{r[f[0]]:{f[1]}}"] 1974 out[-1] += [ 1975 f"{r['d13Cwg_VPDB']:.3f}", 1976 f"{r['d18Owg_VSMOW']:.3f}", 1977 f"{r['d45']:.6f}", 1978 f"{r['d46']:.6f}", 1979 f"{r['d47']:.6f}", 1980 f"{r['d48']:.6f}", 1981 f"{r['d49']:.6f}", 1982 f"{r['d13C_VPDB']:.6f}", 1983 f"{r['d18O_VSMOW']:.6f}", 1984 f"{r['D47raw']:.6f}", 1985 f"{r['D48raw']:.6f}", 1986 f"{r['D49raw']:.6f}", 1987 f"{r[f'D{self._4x}']:.6f}" 1988 ] 1989 if save_to_file: 1990 if not os.path.exists(dir): 1991 os.makedirs(dir) 1992 if filename is None: 1993 filename = f'D{self._4x}_analyses.csv' 1994 with open(f'{dir}/{filename}', 'w') as fid: 1995 fid.write(make_csv(out)) 1996 if print_out: 1997 self.msg('\n' + pretty_table(out)) 1998 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']]
)
2000 @make_verbal 2001 def covar_table( 2002 self, 2003 correl = False, 2004 dir = 'output', 2005 filename = None, 2006 save_to_file = True, 2007 print_out = True, 2008 output = None, 2009 ): 2010 ''' 2011 Print out, save to disk and/or return the variance-covariance matrix of D4x 2012 for all unknown samples. 2013 2014 **Parameters** 2015 2016 + `dir`: the directory in which to save the csv 2017 + `filename`: the name of the csv file to write to 2018 + `save_to_file`: whether to save the csv 2019 + `print_out`: whether to print out the matrix 2020 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2021 if set to `'raw'`: return a list of list of strings 2022 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2023 ''' 2024 samples = sorted([u for u in self.unknowns]) 2025 out = [[''] + samples] 2026 for s1 in samples: 2027 out.append([s1]) 2028 for s2 in samples: 2029 if correl: 2030 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2031 else: 2032 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2033 2034 if save_to_file: 2035 if not os.path.exists(dir): 2036 os.makedirs(dir) 2037 if filename is None: 2038 if correl: 2039 filename = f'D{self._4x}_correl.csv' 2040 else: 2041 filename = f'D{self._4x}_covar.csv' 2042 with open(f'{dir}/{filename}', 'w') as fid: 2043 fid.write(make_csv(out)) 2044 if print_out: 2045 self.msg('\n'+pretty_table(out)) 2046 if output == 'raw': 2047 return out 2048 elif output == 'pretty': 2049 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']]
)
2051 @make_verbal 2052 def table_of_samples( 2053 self, 2054 dir = 'output', 2055 filename = None, 2056 save_to_file = True, 2057 print_out = True, 2058 output = None, 2059 ): 2060 ''' 2061 Print out, save to disk and/or return a table of samples. 2062 2063 **Parameters** 2064 2065 + `dir`: the directory in which to save the csv 2066 + `filename`: the name of the csv file to write to 2067 + `save_to_file`: whether to save the csv 2068 + `print_out`: whether to print out the table 2069 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2070 if set to `'raw'`: return a list of list of strings 2071 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2072 ''' 2073 2074 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2075 for sample in self.anchors: 2076 out += [[ 2077 f"{sample}", 2078 f"{self.samples[sample]['N']}", 2079 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2080 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2081 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2082 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2083 ]] 2084 for sample in self.unknowns: 2085 out += [[ 2086 f"{sample}", 2087 f"{self.samples[sample]['N']}", 2088 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2089 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2090 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2091 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2092 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2093 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2094 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2095 ]] 2096 if save_to_file: 2097 if not os.path.exists(dir): 2098 os.makedirs(dir) 2099 if filename is None: 2100 filename = f'D{self._4x}_samples.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 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']]
)
2111 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2112 ''' 2113 Generate session plots and save them to disk. 2114 2115 **Parameters** 2116 2117 + `dir`: the directory in which to save the plots 2118 + `figsize`: the width and height (in inches) of each plot 2119 ''' 2120 if not os.path.exists(dir): 2121 os.makedirs(dir) 2122 2123 for session in self.sessions: 2124 sp = self.plot_single_session(session, xylimits = 'constant') 2125 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2126 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
2129 @make_verbal 2130 def consolidate_samples(self): 2131 ''' 2132 Compile various statistics for each sample. 2133 2134 For each anchor sample: 2135 2136 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2137 + `SE_D47` or `SE_D48`: set to zero by definition 2138 2139 For each unknown sample: 2140 2141 + `D47` or `D48`: the standardized Δ4x value for this unknown 2142 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2143 2144 For each anchor and unknown: 2145 2146 + `N`: the total number of analyses of this sample 2147 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2148 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2149 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2150 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2151 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2152 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2153 ''' 2154 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2155 for sample in self.samples: 2156 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2157 if self.samples[sample]['N'] > 1: 2158 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2159 2160 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2161 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2162 2163 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2164 if len(D4x_pop) > 2: 2165 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2166 2167 if self.standardization_method == 'pooled': 2168 for sample in self.anchors: 2169 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2170 self.samples[sample][f'SE_D{self._4x}'] = 0. 2171 for sample in self.unknowns: 2172 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2173 try: 2174 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2175 except ValueError: 2176 # when `sample` is constrained by self.standardize(constraints = {...}), 2177 # it is no longer listed in self.standardization.var_names. 2178 # Temporary fix: define SE as zero for now 2179 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2180 2181 elif self.standardization_method == 'indep_sessions': 2182 for sample in self.anchors: 2183 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2184 self.samples[sample][f'SE_D{self._4x}'] = 0. 2185 for sample in self.unknowns: 2186 self.msg(f'Consolidating sample {sample}') 2187 self.unknowns[sample][f'session_D{self._4x}'] = {} 2188 session_avg = [] 2189 for session in self.sessions: 2190 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2191 if sdata: 2192 self.msg(f'{sample} found in session {session}') 2193 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2194 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2195 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2196 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2197 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2198 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2199 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2200 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2201 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2202 wsum = sum([weights[s] for s in weights]) 2203 for s in weights: 2204 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
.
2207 def consolidate_sessions(self): 2208 ''' 2209 Compute various statistics for each session. 2210 2211 + `Na`: Number of anchor analyses in the session 2212 + `Nu`: Number of unknown analyses in the session 2213 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2214 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2215 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2216 + `a`: scrambling factor 2217 + `b`: compositional slope 2218 + `c`: WG offset 2219 + `SE_a`: Model stadard erorr of `a` 2220 + `SE_b`: Model stadard erorr of `b` 2221 + `SE_c`: Model stadard erorr of `c` 2222 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2223 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2224 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2225 + `a2`: scrambling factor drift 2226 + `b2`: compositional slope drift 2227 + `c2`: WG offset drift 2228 + `Np`: Number of standardization parameters to fit 2229 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2230 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2231 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2232 ''' 2233 for session in self.sessions: 2234 if 'd13Cwg_VPDB' not in self.sessions[session]: 2235 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2236 if 'd18Owg_VSMOW' not in self.sessions[session]: 2237 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2238 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2239 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2240 2241 self.msg(f'Computing repeatabilities for session {session}') 2242 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2243 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2244 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2245 2246 if self.standardization_method == 'pooled': 2247 for session in self.sessions: 2248 2249 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2250 i = self.standardization.var_names.index(f'a_{pf(session)}') 2251 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2252 2253 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2254 i = self.standardization.var_names.index(f'b_{pf(session)}') 2255 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2256 2257 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2258 i = self.standardization.var_names.index(f'c_{pf(session)}') 2259 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2260 2261 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2262 if self.sessions[session]['scrambling_drift']: 2263 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2264 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2265 else: 2266 self.sessions[session]['SE_a2'] = 0. 2267 2268 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2269 if self.sessions[session]['slope_drift']: 2270 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2271 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2272 else: 2273 self.sessions[session]['SE_b2'] = 0. 2274 2275 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2276 if self.sessions[session]['wg_drift']: 2277 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2278 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2279 else: 2280 self.sessions[session]['SE_c2'] = 0. 2281 2282 i = self.standardization.var_names.index(f'a_{pf(session)}') 2283 j = self.standardization.var_names.index(f'b_{pf(session)}') 2284 k = self.standardization.var_names.index(f'c_{pf(session)}') 2285 CM = np.zeros((6,6)) 2286 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2287 try: 2288 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2289 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2290 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2291 try: 2292 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2293 CM[3,4] = self.standardization.covar[i2,j2] 2294 CM[4,3] = self.standardization.covar[j2,i2] 2295 except ValueError: 2296 pass 2297 try: 2298 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2299 CM[3,5] = self.standardization.covar[i2,k2] 2300 CM[5,3] = self.standardization.covar[k2,i2] 2301 except ValueError: 2302 pass 2303 except ValueError: 2304 pass 2305 try: 2306 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2307 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2308 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2309 try: 2310 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2311 CM[4,5] = self.standardization.covar[j2,k2] 2312 CM[5,4] = self.standardization.covar[k2,j2] 2313 except ValueError: 2314 pass 2315 except ValueError: 2316 pass 2317 try: 2318 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2319 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2320 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2321 except ValueError: 2322 pass 2323 2324 self.sessions[session]['CM'] = CM 2325 2326 elif self.standardization_method == 'indep_sessions': 2327 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
2330 @make_verbal 2331 def repeatabilities(self): 2332 ''' 2333 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2334 (for all samples, for anchors, and for unknowns). 2335 ''' 2336 self.msg('Computing reproducibilities for all sessions') 2337 2338 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2339 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2340 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2341 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2342 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).
2345 @make_verbal 2346 def consolidate(self, tables = True, plots = True): 2347 ''' 2348 Collect information about samples, sessions and repeatabilities. 2349 ''' 2350 self.consolidate_samples() 2351 self.consolidate_sessions() 2352 self.repeatabilities() 2353 2354 if tables: 2355 self.summary() 2356 self.table_of_sessions() 2357 self.table_of_analyses() 2358 self.table_of_samples() 2359 2360 if plots: 2361 self.plot_sessions()
Collect information about samples, sessions and repeatabilities.
2364 @make_verbal 2365 def rmswd(self, 2366 samples = 'all samples', 2367 sessions = 'all sessions', 2368 ): 2369 ''' 2370 Compute the χ2, root mean squared weighted deviation 2371 (i.e. reduced χ2), and corresponding degrees of freedom of the 2372 Δ4x values for samples in `samples` and sessions in `sessions`. 2373 2374 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2375 ''' 2376 if samples == 'all samples': 2377 mysamples = [k for k in self.samples] 2378 elif samples == 'anchors': 2379 mysamples = [k for k in self.anchors] 2380 elif samples == 'unknowns': 2381 mysamples = [k for k in self.unknowns] 2382 else: 2383 mysamples = samples 2384 2385 if sessions == 'all sessions': 2386 sessions = [k for k in self.sessions] 2387 2388 chisq, Nf = 0, 0 2389 for sample in mysamples : 2390 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2391 if len(G) > 1 : 2392 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2393 Nf += (len(G) - 1) 2394 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2395 r = (chisq / Nf)**.5 if Nf > 0 else 0 2396 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2397 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'
.
2400 @make_verbal 2401 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2402 ''' 2403 Compute the repeatability of `[r[key] for r in self]` 2404 ''' 2405 # NB: it's debatable whether rD47 should be computed 2406 # with Nf = len(self)-len(self.samples) instead of 2407 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2408 2409 if samples == 'all samples': 2410 mysamples = [k for k in self.samples] 2411 elif samples == 'anchors': 2412 mysamples = [k for k in self.anchors] 2413 elif samples == 'unknowns': 2414 mysamples = [k for k in self.unknowns] 2415 else: 2416 mysamples = samples 2417 2418 if sessions == 'all sessions': 2419 sessions = [k for k in self.sessions] 2420 2421 if key in ['D47', 'D48']: 2422 chisq, Nf = 0, 0 2423 for sample in mysamples : 2424 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2425 if len(X) > 1 : 2426 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2427 if sample in self.unknowns: 2428 Nf += len(X) - 1 2429 else: 2430 Nf += len(X) 2431 if samples in ['anchors', 'all samples']: 2432 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2433 r = (chisq / Nf)**.5 if Nf > 0 else 0 2434 2435 else: # if key not in ['D47', 'D48'] 2436 chisq, Nf = 0, 0 2437 for sample in mysamples : 2438 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2439 if len(X) > 1 : 2440 Nf += len(X) - 1 2441 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2442 r = (chisq / Nf)**.5 if Nf > 0 else 0 2443 2444 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2445 return r
Compute the repeatability of [r[key] for r in self]
2447 def sample_average(self, samples, weights = 'equal', normalize = True): 2448 ''' 2449 Weighted average Δ4x value of a group of samples, accounting for covariance. 2450 2451 Returns the weighed average Δ4x value and associated SE 2452 of a group of samples. Weights are equal by default. If `normalize` is 2453 true, `weights` will be rescaled so that their sum equals 1. 2454 2455 **Examples** 2456 2457 ```python 2458 self.sample_average(['X','Y'], [1, 2]) 2459 ``` 2460 2461 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2462 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2463 values of samples X and Y, respectively. 2464 2465 ```python 2466 self.sample_average(['X','Y'], [1, -1], normalize = False) 2467 ``` 2468 2469 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2470 ''' 2471 if weights == 'equal': 2472 weights = [1/len(samples)] * len(samples) 2473 2474 if normalize: 2475 s = sum(weights) 2476 if s: 2477 weights = [w/s for w in weights] 2478 2479 try: 2480# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2481# C = self.standardization.covar[indices,:][:,indices] 2482 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2483 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2484 return correlated_sum(X, C, weights) 2485 except ValueError: 2486 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).
2489 def sample_D4x_covar(self, sample1, sample2 = None): 2490 ''' 2491 Covariance between Δ4x values of samples 2492 2493 Returns the error covariance between the average Δ4x values of two 2494 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2495 returns the Δ4x variance for that sample. 2496 ''' 2497 if sample2 is None: 2498 sample2 = sample1 2499 if self.standardization_method == 'pooled': 2500 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2501 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2502 return self.standardization.covar[i, j] 2503 elif self.standardization_method == 'indep_sessions': 2504 if sample1 == sample2: 2505 return self.samples[sample1][f'SE_D{self._4x}']**2 2506 else: 2507 c = 0 2508 for session in self.sessions: 2509 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2510 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2511 if sdata1 and sdata2: 2512 a = self.sessions[session]['a'] 2513 # !! TODO: CM below does not account for temporal changes in standardization parameters 2514 CM = self.sessions[session]['CM'][:3,:3] 2515 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2516 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2517 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2518 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2519 c += ( 2520 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2521 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2522 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2523 @ CM 2524 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2525 ) / a**2 2526 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.
2528 def sample_D4x_correl(self, sample1, sample2 = None): 2529 ''' 2530 Correlation between Δ4x errors of samples 2531 2532 Returns the error correlation between the average Δ4x values of two samples. 2533 ''' 2534 if sample2 is None or sample2 == sample1: 2535 return 1. 2536 return ( 2537 self.sample_D4x_covar(sample1, sample2) 2538 / self.unknowns[sample1][f'SE_D{self._4x}'] 2539 / self.unknowns[sample2][f'SE_D{self._4x}'] 2540 )
Correlation between Δ4x errors of samples
Returns the error correlation between the average Δ4x values of two samples.
2542 def plot_single_session(self, 2543 session, 2544 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2545 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2546 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2547 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2548 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2549 xylimits = 'free', # | 'constant' 2550 x_label = None, 2551 y_label = None, 2552 error_contour_interval = 'auto', 2553 fig = 'new', 2554 ): 2555 ''' 2556 Generate plot for a single session 2557 ''' 2558 if x_label is None: 2559 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2560 if y_label is None: 2561 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2562 2563 out = _SessionPlot() 2564 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2565 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2566 2567 if fig == 'new': 2568 out.fig = ppl.figure(figsize = (6,6)) 2569 ppl.subplots_adjust(.1,.1,.9,.9) 2570 2571 out.anchor_analyses, = ppl.plot( 2572 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2573 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2574 **kw_plot_anchors) 2575 out.unknown_analyses, = ppl.plot( 2576 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2577 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2578 **kw_plot_unknowns) 2579 out.anchor_avg = ppl.plot( 2580 np.array([ np.array([ 2581 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2582 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2583 ]) for sample in anchors]).T, 2584 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2585 **kw_plot_anchor_avg) 2586 out.unknown_avg = ppl.plot( 2587 np.array([ np.array([ 2588 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2589 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2590 ]) for sample in unknowns]).T, 2591 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2592 **kw_plot_unknown_avg) 2593 if xylimits == 'constant': 2594 x = [r[f'd{self._4x}'] for r in self] 2595 y = [r[f'D{self._4x}'] for r in self] 2596 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2597 w, h = x2-x1, y2-y1 2598 x1 -= w/20 2599 x2 += w/20 2600 y1 -= h/20 2601 y2 += h/20 2602 ppl.axis([x1, x2, y1, y2]) 2603 elif xylimits == 'free': 2604 x1, x2, y1, y2 = ppl.axis() 2605 else: 2606 x1, x2, y1, y2 = ppl.axis(xylimits) 2607 2608 if error_contour_interval != 'none': 2609 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2610 XI,YI = np.meshgrid(xi, yi) 2611 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2612 if error_contour_interval == 'auto': 2613 rng = np.max(SI) - np.min(SI) 2614 if rng <= 0.01: 2615 cinterval = 0.001 2616 elif rng <= 0.03: 2617 cinterval = 0.004 2618 elif rng <= 0.1: 2619 cinterval = 0.01 2620 elif rng <= 0.3: 2621 cinterval = 0.03 2622 elif rng <= 1.: 2623 cinterval = 0.1 2624 else: 2625 cinterval = 0.5 2626 else: 2627 cinterval = error_contour_interval 2628 2629 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2630 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2631 out.clabel = ppl.clabel(out.contour) 2632 2633 ppl.xlabel(x_label) 2634 ppl.ylabel(y_label) 2635 ppl.title(session, weight = 'bold') 2636 ppl.grid(alpha = .2) 2637 out.ax = ppl.gca() 2638 2639 return out
Generate plot for a single session
2641 def plot_residuals( 2642 self, 2643 hist = False, 2644 binwidth = 2/3, 2645 dir = 'output', 2646 filename = None, 2647 highlight = [], 2648 colors = None, 2649 figsize = None, 2650 ): 2651 ''' 2652 Plot residuals of each analysis as a function of time (actually, as a function of 2653 the order of analyses in the `D4xdata` object) 2654 2655 + `hist`: whether to add a histogram of residuals 2656 + `histbins`: specify bin edges for the histogram 2657 + `dir`: the directory in which to save the plot 2658 + `highlight`: a list of samples to highlight 2659 + `colors`: a dict of `{<sample>: <color>}` for all samples 2660 + `figsize`: (width, height) of figure 2661 ''' 2662 # Layout 2663 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2664 if hist: 2665 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2666 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2667 else: 2668 ppl.subplots_adjust(.08,.05,.78,.8) 2669 ax1 = ppl.subplot(111) 2670 2671 # Colors 2672 N = len(self.anchors) 2673 if colors is None: 2674 if len(highlight) > 0: 2675 Nh = len(highlight) 2676 if Nh == 1: 2677 colors = {highlight[0]: (0,0,0)} 2678 elif Nh == 3: 2679 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2680 elif Nh == 4: 2681 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2682 else: 2683 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2684 else: 2685 if N == 3: 2686 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2687 elif N == 4: 2688 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2689 else: 2690 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2691 2692 ppl.sca(ax1) 2693 2694 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2695 2696 session = self[0]['Session'] 2697 x1 = 0 2698# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2699 x_sessions = {} 2700 one_or_more_singlets = False 2701 one_or_more_multiplets = False 2702 multiplets = set() 2703 for k,r in enumerate(self): 2704 if r['Session'] != session: 2705 x2 = k-1 2706 x_sessions[session] = (x1+x2)/2 2707 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2708 session = r['Session'] 2709 x1 = k 2710 singlet = len(self.samples[r['Sample']]['data']) == 1 2711 if not singlet: 2712 multiplets.add(r['Sample']) 2713 if r['Sample'] in self.unknowns: 2714 if singlet: 2715 one_or_more_singlets = True 2716 else: 2717 one_or_more_multiplets = True 2718 kw = dict( 2719 marker = 'x' if singlet else '+', 2720 ms = 4 if singlet else 5, 2721 ls = 'None', 2722 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2723 mew = 1, 2724 alpha = 0.2 if singlet else 1, 2725 ) 2726 if highlight and r['Sample'] not in highlight: 2727 kw['alpha'] = 0.2 2728 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2729 x2 = k 2730 x_sessions[session] = (x1+x2)/2 2731 2732 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2733 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2734 if not hist: 2735 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2736 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') 2737 2738 xmin, xmax, ymin, ymax = ppl.axis() 2739 for s in x_sessions: 2740 ppl.text( 2741 x_sessions[s], 2742 ymax +1, 2743 s, 2744 va = 'bottom', 2745 **( 2746 dict(ha = 'center') 2747 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2748 else dict(ha = 'left', rotation = 45) 2749 ) 2750 ) 2751 2752 if hist: 2753 ppl.sca(ax2) 2754 2755 for s in colors: 2756 kw['marker'] = '+' 2757 kw['ms'] = 5 2758 kw['mec'] = colors[s] 2759 kw['label'] = s 2760 kw['alpha'] = 1 2761 ppl.plot([], [], **kw) 2762 2763 kw['mec'] = (0,0,0) 2764 2765 if one_or_more_singlets: 2766 kw['marker'] = 'x' 2767 kw['ms'] = 4 2768 kw['alpha'] = .2 2769 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2770 ppl.plot([], [], **kw) 2771 2772 if one_or_more_multiplets: 2773 kw['marker'] = '+' 2774 kw['ms'] = 4 2775 kw['alpha'] = 1 2776 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2777 ppl.plot([], [], **kw) 2778 2779 if hist: 2780 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2781 else: 2782 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2783 leg.set_zorder(-1000) 2784 2785 ppl.sca(ax1) 2786 2787 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2788 ppl.xticks([]) 2789 ppl.axis([-1, len(self), None, None]) 2790 2791 if hist: 2792 ppl.sca(ax2) 2793 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2794 ppl.hist( 2795 X, 2796 orientation = 'horizontal', 2797 histtype = 'stepfilled', 2798 ec = [.4]*3, 2799 fc = [.25]*3, 2800 alpha = .25, 2801 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2802 ) 2803 ppl.axis([None, None, ymin, ymax]) 2804 ppl.text(0, 0, 2805 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2806 size = 8, 2807 alpha = 1, 2808 va = 'center', 2809 ha = 'left', 2810 ) 2811 2812 ppl.xticks([]) 2813 ppl.yticks([]) 2814# ax2.spines['left'].set_visible(False) 2815 ax2.spines['right'].set_visible(False) 2816 ax2.spines['top'].set_visible(False) 2817 ax2.spines['bottom'].set_visible(False) 2818 2819 2820 if not os.path.exists(dir): 2821 os.makedirs(dir) 2822 if filename is None: 2823 return fig 2824 elif filename == '': 2825 filename = f'D{self._4x}_residuals.pdf' 2826 ppl.savefig(f'{dir}/{filename}') 2827 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
2830 def simulate(self, *args, **kwargs): 2831 ''' 2832 Legacy function with warning message pointing to `virtual_data()` 2833 ''' 2834 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()')
Legacy function with warning message pointing to virtual_data()
2836 def plot_distribution_of_analyses( 2837 self, 2838 dir = 'output', 2839 filename = None, 2840 vs_time = False, 2841 figsize = (6,4), 2842 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2843 output = None, 2844 ): 2845 ''' 2846 Plot temporal distribution of all analyses in the data set. 2847 2848 **Parameters** 2849 2850 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2851 ''' 2852 2853 asamples = [s for s in self.anchors] 2854 usamples = [s for s in self.unknowns] 2855 if output is None or output == 'fig': 2856 fig = ppl.figure(figsize = figsize) 2857 ppl.subplots_adjust(*subplots_adjust) 2858 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2859 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2860 Xmax += (Xmax-Xmin)/40 2861 Xmin -= (Xmax-Xmin)/41 2862 for k, s in enumerate(asamples + usamples): 2863 if vs_time: 2864 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2865 else: 2866 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2867 Y = [-k for x in X] 2868 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2869 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2870 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2871 ppl.axis([Xmin, Xmax, -k-1, 1]) 2872 ppl.xlabel('\ntime') 2873 ppl.gca().annotate('', 2874 xy = (0.6, -0.02), 2875 xycoords = 'axes fraction', 2876 xytext = (.4, -0.02), 2877 arrowprops = dict(arrowstyle = "->", color = 'k'), 2878 ) 2879 2880 2881 x2 = -1 2882 for session in self.sessions: 2883 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2884 if vs_time: 2885 ppl.axvline(x1, color = 'k', lw = .75) 2886 if x2 > -1: 2887 if not vs_time: 2888 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2889 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2890# from xlrd import xldate_as_datetime 2891# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2892 if vs_time: 2893 ppl.axvline(x2, color = 'k', lw = .75) 2894 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2895 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2896 2897 ppl.xticks([]) 2898 ppl.yticks([]) 2899 2900 if output is None: 2901 if not os.path.exists(dir): 2902 os.makedirs(dir) 2903 if filename == None: 2904 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2905 ppl.savefig(f'{dir}/{filename}') 2906 ppl.close(fig) 2907 elif output == 'ax': 2908 return ppl.gca() 2909 elif output == 'fig': 2910 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
2913class D47data(D4xdata): 2914 ''' 2915 Store and process data for a large set of Δ47 analyses, 2916 usually comprising more than one analytical session. 2917 ''' 2918 2919 Nominal_D4x = { 2920 'ETH-1': 0.2052, 2921 'ETH-2': 0.2085, 2922 'ETH-3': 0.6132, 2923 'ETH-4': 0.4511, 2924 'IAEA-C1': 0.3018, 2925 'IAEA-C2': 0.6409, 2926 'MERCK': 0.5135, 2927 } # I-CDES (Bernasconi et al., 2021) 2928 ''' 2929 Nominal Δ47 values assigned to the Δ47 anchor samples, used by 2930 `D47data.standardize()` to normalize unknown samples to an absolute Δ47 2931 reference frame. 2932 2933 By default equal to (after [Bernasconi et al. (2021)](https://doi.org/10.1029/2020GC009588)): 2934 ```py 2935 { 2936 'ETH-1' : 0.2052, 2937 'ETH-2' : 0.2085, 2938 'ETH-3' : 0.6132, 2939 'ETH-4' : 0.4511, 2940 'IAEA-C1' : 0.3018, 2941 'IAEA-C2' : 0.6409, 2942 'MERCK' : 0.5135, 2943 } 2944 ``` 2945 ''' 2946 2947 2948 @property 2949 def Nominal_D47(self): 2950 return self.Nominal_D4x 2951 2952 2953 @Nominal_D47.setter 2954 def Nominal_D47(self, new): 2955 self.Nominal_D4x = dict(**new) 2956 self.refresh() 2957 2958 2959 def __init__(self, l = [], **kwargs): 2960 ''' 2961 **Parameters:** same as `D4xdata.__init__()` 2962 ''' 2963 D4xdata.__init__(self, l = l, mass = '47', **kwargs) 2964 2965 2966 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 2967 ''' 2968 Find all samples for which `Teq` is specified, compute equilibrium Δ47 2969 value for that temperature, and add treat these samples as additional anchors. 2970 2971 **Parameters** 2972 2973 + `fCo2eqD47`: Which CO2 equilibrium law to use 2974 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 2975 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 2976 + `priority`: if `replace`: forget old anchors and only use the new ones; 2977 if `new`: keep pre-existing anchors but update them in case of conflict 2978 between old and new Δ47 values; 2979 if `old`: keep pre-existing anchors but preserve their original Δ47 2980 values in case of conflict. 2981 ''' 2982 f = { 2983 'petersen': fCO2eqD47_Petersen, 2984 'wang': fCO2eqD47_Wang, 2985 }[fCo2eqD47] 2986 foo = {} 2987 for r in self: 2988 if 'Teq' in r: 2989 if r['Sample'] in foo: 2990 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 2991 else: 2992 foo[r['Sample']] = f(r['Teq']) 2993 else: 2994 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 2995 2996 if priority == 'replace': 2997 self.Nominal_D47 = {} 2998 for s in foo: 2999 if priority != 'old' or s not in self.Nominal_D47: 3000 self.Nominal_D47[s] = foo[s]
Store and process data for a large set of Δ47 analyses, usually comprising more than one analytical session.
2959 def __init__(self, l = [], **kwargs): 2960 ''' 2961 **Parameters:** same as `D4xdata.__init__()` 2962 ''' 2963 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,
}
2966 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 2967 ''' 2968 Find all samples for which `Teq` is specified, compute equilibrium Δ47 2969 value for that temperature, and add treat these samples as additional anchors. 2970 2971 **Parameters** 2972 2973 + `fCo2eqD47`: Which CO2 equilibrium law to use 2974 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 2975 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 2976 + `priority`: if `replace`: forget old anchors and only use the new ones; 2977 if `new`: keep pre-existing anchors but update them in case of conflict 2978 between old and new Δ47 values; 2979 if `old`: keep pre-existing anchors but preserve their original Δ47 2980 values in case of conflict. 2981 ''' 2982 f = { 2983 'petersen': fCO2eqD47_Petersen, 2984 'wang': fCO2eqD47_Wang, 2985 }[fCo2eqD47] 2986 foo = {} 2987 for r in self: 2988 if 'Teq' in r: 2989 if r['Sample'] in foo: 2990 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 2991 else: 2992 foo[r['Sample']] = f(r['Teq']) 2993 else: 2994 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 2995 2996 if priority == 'replace': 2997 self.Nominal_D47 = {} 2998 for s in foo: 2999 if priority != 'old' or s not in self.Nominal_D47: 3000 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
3005class D48data(D4xdata): 3006 ''' 3007 Store and process data for a large set of Δ48 analyses, 3008 usually comprising more than one analytical session. 3009 ''' 3010 3011 Nominal_D4x = { 3012 'ETH-1': 0.138, 3013 'ETH-2': 0.138, 3014 'ETH-3': 0.270, 3015 'ETH-4': 0.223, 3016 'GU-1': -0.419, 3017 } # (Fiebig et al., 2019, 2021) 3018 ''' 3019 Nominal Δ48 values assigned to the Δ48 anchor samples, used by 3020 `D48data.standardize()` to normalize unknown samples to an absolute Δ48 3021 reference frame. 3022 3023 By default equal to (after [Fiebig et al. (2019)](https://doi.org/10.1016/j.chemgeo.2019.05.019), 3024 Fiebig et al. (in press)): 3025 3026 ```py 3027 { 3028 'ETH-1' : 0.138, 3029 'ETH-2' : 0.138, 3030 'ETH-3' : 0.270, 3031 'ETH-4' : 0.223, 3032 'GU-1' : -0.419, 3033 } 3034 ``` 3035 ''' 3036 3037 3038 @property 3039 def Nominal_D48(self): 3040 return self.Nominal_D4x 3041 3042 3043 @Nominal_D48.setter 3044 def Nominal_D48(self, new): 3045 self.Nominal_D4x = dict(**new) 3046 self.refresh() 3047 3048 3049 def __init__(self, l = [], **kwargs): 3050 ''' 3051 **Parameters:** same as `D4xdata.__init__()` 3052 ''' 3053 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.
3049 def __init__(self, l = [], **kwargs): 3050 ''' 3051 **Parameters:** same as `D4xdata.__init__()` 3052 ''' 3053 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