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.5' 25 26import os 27import numpy as np 28from statistics import stdev 29from scipy.stats import t as tstudent 30from scipy.stats import levene 31from scipy.interpolate import interp1d 32from numpy import linalg 33from lmfit import Minimizer, Parameters, report_fit 34from matplotlib import pyplot as ppl 35from datetime import datetime as dt 36from functools import wraps 37from colorsys import hls_to_rgb 38from matplotlib import rcParams 39 40rcParams['font.family'] = 'sans-serif' 41rcParams['font.sans-serif'] = 'Helvetica' 42rcParams['font.size'] = 10 43rcParams['mathtext.fontset'] = 'custom' 44rcParams['mathtext.rm'] = 'sans' 45rcParams['mathtext.bf'] = 'sans:bold' 46rcParams['mathtext.it'] = 'sans:italic' 47rcParams['mathtext.cal'] = 'sans:italic' 48rcParams['mathtext.default'] = 'rm' 49rcParams['xtick.major.size'] = 4 50rcParams['xtick.major.width'] = 1 51rcParams['ytick.major.size'] = 4 52rcParams['ytick.major.width'] = 1 53rcParams['axes.grid'] = False 54rcParams['axes.linewidth'] = 1 55rcParams['grid.linewidth'] = .75 56rcParams['grid.linestyle'] = '-' 57rcParams['grid.alpha'] = .15 58rcParams['savefig.dpi'] = 150 59 60Petersen_etal_CO2eqD47 = np.array([[-12, 1.147113572], [-11, 1.139961218], [-10, 1.132872856], [-9, 1.125847677], [-8, 1.118884889], [-7, 1.111983708], [-6, 1.105143366], [-5, 1.098363105], [-4, 1.091642182], [-3, 1.084979862], [-2, 1.078375423], [-1, 1.071828156], [0, 1.065337360], [1, 1.058902349], [2, 1.052522443], [3, 1.046196976], [4, 1.039925291], [5, 1.033706741], [6, 1.027540690], [7, 1.021426510], [8, 1.015363585], [9, 1.009351306], [10, 1.003389075], [11, 0.997476303], [12, 0.991612409], [13, 0.985796821], [14, 0.980028975], [15, 0.974308318], [16, 0.968634304], [17, 0.963006392], [18, 0.957424055], [19, 0.951886769], [20, 0.946394020], [21, 0.940945302], [22, 0.935540114], [23, 0.930177964], [24, 0.924858369], [25, 0.919580851], [26, 0.914344938], [27, 0.909150167], [28, 0.903996080], [29, 0.898882228], [30, 0.893808167], [31, 0.888773459], [32, 0.883777672], [33, 0.878820382], [34, 0.873901170], [35, 0.869019623], [36, 0.864175334], [37, 0.859367901], [38, 0.854596929], [39, 0.849862028], [40, 0.845162813], [41, 0.840498905], [42, 0.835869931], [43, 0.831275522], [44, 0.826715314], [45, 0.822188950], [46, 0.817696075], [47, 0.813236341], [48, 0.808809404], [49, 0.804414926], [50, 0.800052572], [51, 0.795722012], [52, 0.791422922], [53, 0.787154979], [54, 0.782917869], [55, 0.778711277], [56, 0.774534898], [57, 0.770388426], [58, 0.766271562], [59, 0.762184010], [60, 0.758125479], [61, 0.754095680], [62, 0.750094329], [63, 0.746121147], [64, 0.742175856], [65, 0.738258184], [66, 0.734367860], [67, 0.730504620], [68, 0.726668201], [69, 0.722858343], [70, 0.719074792], [71, 0.715317295], [72, 0.711585602], [73, 0.707879469], [74, 0.704198652], [75, 0.700542912], [76, 0.696912012], [77, 0.693305719], [78, 0.689723802], [79, 0.686166034], [80, 0.682632189], [81, 0.679122047], [82, 0.675635387], [83, 0.672171994], [84, 0.668731654], [85, 0.665314156], [86, 0.661919291], [87, 0.658546854], [88, 0.655196641], [89, 0.651868451], [90, 0.648562087], [91, 0.645277352], [92, 0.642014054], [93, 0.638771999], [94, 0.635551001], [95, 0.632350872], [96, 0.629171428], [97, 0.626012487], [98, 0.622873870], [99, 0.619755397], [100, 0.616656895], [102, 0.610519107], [104, 0.604459143], [106, 0.598475670], [108, 0.592567388], [110, 0.586733026], [112, 0.580971342], [114, 0.575281125], [116, 0.569661187], [118, 0.564110371], [120, 0.558627545], [122, 0.553211600], [124, 0.547861454], [126, 0.542576048], [128, 0.537354347], [130, 0.532195337], [132, 0.527098028], [134, 0.522061450], [136, 0.517084654], [138, 0.512166711], [140, 0.507306712], [142, 0.502503768], [144, 0.497757006], [146, 0.493065573], [148, 0.488428634], [150, 0.483845370], [152, 0.479314980], [154, 0.474836677], [156, 0.470409692], [158, 0.466033271], [160, 0.461706674], [162, 0.457429176], [164, 0.453200067], [166, 0.449018650], [168, 0.444884242], [170, 0.440796174], [172, 0.436753787], [174, 0.432756438], [176, 0.428803494], [178, 0.424894334], [180, 0.421028350], [182, 0.417204944], [184, 0.413423530], [186, 0.409683531], [188, 0.405984383], [190, 0.402325531], [192, 0.398706429], [194, 0.395126543], [196, 0.391585347], [198, 0.388082324], [200, 0.384616967], [202, 0.381188778], [204, 0.377797268], [206, 0.374441954], [208, 0.371122364], [210, 0.367838033], [212, 0.364588505], [214, 0.361373329], [216, 0.358192065], [218, 0.355044277], [220, 0.351929540], [222, 0.348847432], [224, 0.345797540], [226, 0.342779460], [228, 0.339792789], [230, 0.336837136], [232, 0.333912113], [234, 0.331017339], [236, 0.328152439], [238, 0.325317046], [240, 0.322510795], [242, 0.319733329], [244, 0.316984297], [246, 0.314263352], [248, 0.311570153], [250, 0.308904364], [252, 0.306265654], [254, 0.303653699], [256, 0.301068176], [258, 0.298508771], [260, 0.295975171], [262, 0.293467070], [264, 0.290984167], [266, 0.288526163], [268, 0.286092765], [270, 0.283683684], [272, 0.281298636], [274, 0.278937339], [276, 0.276599517], [278, 0.274284898], [280, 0.271993211], [282, 0.269724193], [284, 0.267477582], [286, 0.265253121], [288, 0.263050554], [290, 0.260869633], [292, 0.258710110], [294, 0.256571741], [296, 0.254454286], [298, 0.252357508], [300, 0.250281174], [302, 0.248225053], [304, 0.246188917], [306, 0.244172542], [308, 0.242175707], [310, 0.240198194], [312, 0.238239786], [314, 0.236300272], [316, 0.234379441], [318, 0.232477087], [320, 0.230593005], [322, 0.228726993], [324, 0.226878853], [326, 0.225048388], [328, 0.223235405], [330, 0.221439711], [332, 0.219661118], [334, 0.217899439], [336, 0.216154491], [338, 0.214426091], [340, 0.212714060], [342, 0.211018220], [344, 0.209338398], [346, 0.207674420], [348, 0.206026115], [350, 0.204393315], [355, 0.200378063], [360, 0.196456139], [365, 0.192625077], [370, 0.188882487], [375, 0.185226048], [380, 0.181653511], [385, 0.178162694], [390, 0.174751478], [395, 0.171417807], [400, 0.168159686], [405, 0.164975177], [410, 0.161862398], [415, 0.158819521], [420, 0.155844772], [425, 0.152936426], [430, 0.150092806], [435, 0.147312286], [440, 0.144593281], [445, 0.141934254], [450, 0.139333710], [455, 0.136790195], [460, 0.134302294], [465, 0.131868634], [470, 0.129487876], [475, 0.127158722], [480, 0.124879906], [485, 0.122650197], [490, 0.120468398], [495, 0.118333345], [500, 0.116243903], [505, 0.114198970], [510, 0.112197471], [515, 0.110238362], [520, 0.108320625], [525, 0.106443271], [530, 0.104605335], [535, 0.102805877], [540, 0.101043985], [545, 0.099318768], [550, 0.097629359], [555, 0.095974915], [560, 0.094354612], [565, 0.092767650], [570, 0.091213248], [575, 0.089690648], [580, 0.088199108], [585, 0.086737906], [590, 0.085306341], [595, 0.083903726], [600, 0.082529395], [605, 0.081182697], [610, 0.079862998], [615, 0.078569680], [620, 0.077302141], [625, 0.076059794], [630, 0.074842066], [635, 0.073648400], [640, 0.072478251], [645, 0.071331090], [650, 0.070206399], [655, 0.069103674], [660, 0.068022424], [665, 0.066962168], [670, 0.065922439], [675, 0.064902780], [680, 0.063902748], [685, 0.062921909], [690, 0.061959837], [695, 0.061016122], [700, 0.060090360], [705, 0.059182157], [710, 0.058291131], [715, 0.057416907], [720, 0.056559120], [725, 0.055717414], [730, 0.054891440], [735, 0.054080860], [740, 0.053285343], [745, 0.052504565], [750, 0.051738210], [755, 0.050985971], [760, 0.050247546], [765, 0.049522643], [770, 0.048810974], [775, 0.048112260], [780, 0.047426227], [785, 0.046752609], [790, 0.046091145], [795, 0.045441581], [800, 0.044803668], [805, 0.044177164], [810, 0.043561831], [815, 0.042957438], [820, 0.042363759], [825, 0.041780573], [830, 0.041207664], [835, 0.040644822], [840, 0.040091839], [845, 0.039548516], [850, 0.039014654], [855, 0.038490063], [860, 0.037974554], [865, 0.037467944], [870, 0.036970054], [875, 0.036480707], [880, 0.035999734], [885, 0.035526965], [890, 0.035062238], [895, 0.034605393], [900, 0.034156272], [905, 0.033714724], [910, 0.033280598], [915, 0.032853749], [920, 0.032434032], [925, 0.032021309], [930, 0.031615443], [935, 0.031216300], [940, 0.030823749], [945, 0.030437663], [950, 0.030057915], [955, 0.029684385], [960, 0.029316951], [965, 0.028955498], [970, 0.028599910], [975, 0.028250075], [980, 0.027905884], [985, 0.027567229], [990, 0.027234006], [995, 0.026906112], [1000, 0.026583445], [1005, 0.026265908], [1010, 0.025953405], [1015, 0.025645841], [1020, 0.025343124], [1025, 0.025045163], [1030, 0.024751871], [1035, 0.024463160], [1040, 0.024178947], [1045, 0.023899147], [1050, 0.023623680], [1055, 0.023352467], [1060, 0.023085429], [1065, 0.022822491], [1070, 0.022563577], [1075, 0.022308615], [1080, 0.022057533], [1085, 0.021810260], [1090, 0.021566729], [1095, 0.021326872], [1100, 0.021090622]]) 61_fCO2eqD47_Petersen = interp1d(Petersen_etal_CO2eqD47[:,0], Petersen_etal_CO2eqD47[:,1]) 62def fCO2eqD47_Petersen(T): 63 ''' 64 CO2 equilibrium Δ47 value as a function of T (in degrees C) 65 according to [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127). 66 67 ''' 68 return float(_fCO2eqD47_Petersen(T)) 69 70 71Wang_etal_CO2eqD47 = np.array([[-83., 1.8954], [-73., 1.7530], [-63., 1.6261], [-53., 1.5126], [-43., 1.4104], [-33., 1.3182], [-23., 1.2345], [-13., 1.1584], [-3., 1.0888], [7., 1.0251], [17., 0.9665], [27., 0.9125], [37., 0.8626], [47., 0.8164], [57., 0.7734], [67., 0.7334], [87., 0.6612], [97., 0.6286], [107., 0.5980], [117., 0.5693], [127., 0.5423], [137., 0.5169], [147., 0.4930], [157., 0.4704], [167., 0.4491], [177., 0.4289], [187., 0.4098], [197., 0.3918], [207., 0.3747], [217., 0.3585], [227., 0.3431], [237., 0.3285], [247., 0.3147], [257., 0.3015], [267., 0.2890], [277., 0.2771], [287., 0.2657], [297., 0.2550], [307., 0.2447], [317., 0.2349], [327., 0.2256], [337., 0.2167], [347., 0.2083], [357., 0.2002], [367., 0.1925], [377., 0.1851], [387., 0.1781], [397., 0.1714], [407., 0.1650], [417., 0.1589], [427., 0.1530], [437., 0.1474], [447., 0.1421], [457., 0.1370], [467., 0.1321], [477., 0.1274], [487., 0.1229], [497., 0.1186], [507., 0.1145], [517., 0.1105], [527., 0.1068], [537., 0.1031], [547., 0.0997], [557., 0.0963], [567., 0.0931], [577., 0.0901], [587., 0.0871], [597., 0.0843], [607., 0.0816], [617., 0.0790], [627., 0.0765], [637., 0.0741], [647., 0.0718], [657., 0.0695], [667., 0.0674], [677., 0.0654], [687., 0.0634], [697., 0.0615], [707., 0.0597], [717., 0.0579], [727., 0.0562], [737., 0.0546], [747., 0.0530], [757., 0.0515], [767., 0.0500], [777., 0.0486], [787., 0.0472], [797., 0.0459], [807., 0.0447], [817., 0.0435], [827., 0.0423], [837., 0.0411], [847., 0.0400], [857., 0.0390], [867., 0.0380], [877., 0.0370], [887., 0.0360], [897., 0.0351], [907., 0.0342], [917., 0.0333], [927., 0.0325], [937., 0.0317], [947., 0.0309], [957., 0.0302], [967., 0.0294], [977., 0.0287], [987., 0.0281], [997., 0.0274], [1007., 0.0268], [1017., 0.0261], [1027., 0.0255], [1037., 0.0249], [1047., 0.0244], [1057., 0.0238], [1067., 0.0233], [1077., 0.0228], [1087., 0.0223], [1097., 0.0218]]) 72_fCO2eqD47_Wang = interp1d(Wang_etal_CO2eqD47[:,0] - 0.15, Wang_etal_CO2eqD47[:,1]) 73def fCO2eqD47_Wang(T): 74 ''' 75 CO2 equilibrium Δ47 value as a function of `T` (in degrees C) 76 according to [Wang et al. (2004)](https://doi.org/10.1016/j.gca.2004.05.039) 77 (supplementary data of [Dennis et al., 2011](https://doi.org/10.1016/j.gca.2011.09.025)). 78 ''' 79 return float(_fCO2eqD47_Wang(T)) 80 81 82def correlated_sum(X, C, w = None): 83 ''' 84 Compute covariance-aware linear combinations 85 86 **Parameters** 87 88 + `X`: list or 1-D array of values to sum 89 + `C`: covariance matrix for the elements of `X` 90 + `w`: list or 1-D array of weights to apply to the elements of `X` 91 (all equal to 1 by default) 92 93 Return the sum (and its SE) of the elements of `X`, with optional weights equal 94 to the elements of `w`, accounting for covariances between the elements of `X`. 95 ''' 96 if w is None: 97 w = [1 for x in X] 98 return np.dot(w,X), (np.dot(w,np.dot(C,w)))**.5 99 100 101def make_csv(x, hsep = ',', vsep = '\n'): 102 ''' 103 Formats a list of lists of strings as a CSV 104 105 **Parameters** 106 107 + `x`: the list of lists of strings to format 108 + `hsep`: the field separator (`,` by default) 109 + `vsep`: the line-ending convention to use (`\\n` by default) 110 111 **Example** 112 113 ```py 114 print(make_csv([['a', 'b', 'c'], ['d', 'e', 'f']])) 115 ``` 116 117 outputs: 118 119 ```py 120 a,b,c 121 d,e,f 122 ``` 123 ''' 124 return vsep.join([hsep.join(l) for l in x]) 125 126 127def pf(txt): 128 ''' 129 Modify string `txt` to follow `lmfit.Parameter()` naming rules. 130 ''' 131 return txt.replace('-','_').replace('.','_').replace(' ','_') 132 133 134def smart_type(x): 135 ''' 136 Tries to convert string `x` to a float if it includes a decimal point, or 137 to an integer if it does not. If both attempts fail, return the original 138 string unchanged. 139 ''' 140 try: 141 y = float(x) 142 except ValueError: 143 return x 144 if '.' not in x: 145 return int(y) 146 return y 147 148 149def pretty_table(x, header = 1, hsep = ' ', vsep = '–', align = '<'): 150 ''' 151 Reads a list of lists of strings and outputs an ascii table 152 153 **Parameters** 154 155 + `x`: a list of lists of strings 156 + `header`: the number of lines to treat as header lines 157 + `hsep`: the horizontal separator between columns 158 + `vsep`: the character to use as vertical separator 159 + `align`: string of left (`<`) or right (`>`) alignment characters. 160 161 **Example** 162 163 ```py 164 x = [['A', 'B', 'C'], ['1', '1.9999', 'foo'], ['10', 'x', 'bar']] 165 print(pretty_table(x)) 166 ``` 167 yields: 168 ``` 169 -- ------ --- 170 A B C 171 -- ------ --- 172 1 1.9999 foo 173 10 x bar 174 -- ------ --- 175 ``` 176 177 ''' 178 txt = [] 179 widths = [np.max([len(e) for e in c]) for c in zip(*x)] 180 181 if len(widths) > len(align): 182 align += '>' * (len(widths)-len(align)) 183 sepline = hsep.join([vsep*w for w in widths]) 184 txt += [sepline] 185 for k,l in enumerate(x): 186 if k and k == header: 187 txt += [sepline] 188 txt += [hsep.join([f'{e:{a}{w}}' for e, w, a in zip(l, widths, align)])] 189 txt += [sepline] 190 txt += [''] 191 return '\n'.join(txt) 192 193 194def transpose_table(x): 195 ''' 196 Transpose a list if lists 197 198 **Parameters** 199 200 + `x`: a list of lists 201 202 **Example** 203 204 ```py 205 x = [[1, 2], [3, 4]] 206 print(transpose_table(x)) # yields: [[1, 3], [2, 4]] 207 ``` 208 ''' 209 return [[e for e in c] for c in zip(*x)] 210 211 212def w_avg(X, sX) : 213 ''' 214 Compute variance-weighted average 215 216 Returns the value and SE of the weighted average of the elements of `X`, 217 with relative weights equal to their inverse variances (`1/sX**2`). 218 219 **Parameters** 220 221 + `X`: array-like of elements to average 222 + `sX`: array-like of the corresponding SE values 223 224 **Tip** 225 226 If `X` and `sX` are initially arranged as a list of `(x, sx)` doublets, 227 they may be rearranged using `zip()`: 228 229 ```python 230 foo = [(0, 1), (1, 0.5), (2, 0.5)] 231 print(w_avg(*zip(*foo))) # yields: (1.3333333333333333, 0.3333333333333333) 232 ``` 233 ''' 234 X = [ x for x in X ] 235 sX = [ sx for sx in sX ] 236 W = [ sx**-2 for sx in sX ] 237 W = [ w/sum(W) for w in W ] 238 Xavg = sum([ w*x for w,x in zip(W,X) ]) 239 sXavg = sum([ w**2*sx**2 for w,sx in zip(W,sX) ])**.5 240 return Xavg, sXavg 241 242 243def read_csv(filename, sep = ''): 244 ''' 245 Read contents of `filename` in csv format and return a list of dictionaries. 246 247 In the csv string, spaces before and after field separators (`','` by default) 248 are optional. 249 250 **Parameters** 251 252 + `filename`: the csv file to read 253 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 254 whichever appers most often in the contents of `filename`. 255 ''' 256 with open(filename) as fid: 257 txt = fid.read() 258 259 if sep == '': 260 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 261 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 262 return [{k: smart_type(v) for k,v in zip(txt[0], l) if v} for l in txt[1:]] 263 264 265def simulate_single_analysis( 266 sample = 'MYSAMPLE', 267 d13Cwg_VPDB = -4., d18Owg_VSMOW = 26., 268 d13C_VPDB = None, d18O_VPDB = None, 269 D47 = None, D48 = None, D49 = 0., D17O = 0., 270 a47 = 1., b47 = 0., c47 = -0.9, 271 a48 = 1., b48 = 0., c48 = -0.45, 272 Nominal_D47 = None, 273 Nominal_D48 = None, 274 Nominal_d13C_VPDB = None, 275 Nominal_d18O_VPDB = None, 276 ALPHA_18O_ACID_REACTION = None, 277 R13_VPDB = None, 278 R17_VSMOW = None, 279 R18_VSMOW = None, 280 LAMBDA_17 = None, 281 R18_VPDB = None, 282 ): 283 ''' 284 Compute working-gas delta values for a single analysis, assuming a stochastic working 285 gas and a “perfect” measurement (i.e. raw Δ values are identical to absolute values). 286 287 **Parameters** 288 289 + `sample`: sample name 290 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 291 (respectively –4 and +26 ‰ by default) 292 + `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 293 + `D47`, `D48`, `D49`, `D17O`: clumped-isotope and oxygen-17 anomalies 294 of the carbonate sample 295 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and 296 Δ48 values if `D47` or `D48` are not specified 297 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 298 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 299 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 300 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 301 correction parameters (by default equal to the `D4xdata` default values) 302 303 Returns a dictionary with fields 304 `['Sample', 'D17O', 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'd45', 'd46', 'd47', 'd48', 'd49']`. 305 ''' 306 307 if Nominal_d13C_VPDB is None: 308 Nominal_d13C_VPDB = D4xdata().Nominal_d13C_VPDB 309 310 if Nominal_d18O_VPDB is None: 311 Nominal_d18O_VPDB = D4xdata().Nominal_d18O_VPDB 312 313 if ALPHA_18O_ACID_REACTION is None: 314 ALPHA_18O_ACID_REACTION = D4xdata().ALPHA_18O_ACID_REACTION 315 316 if R13_VPDB is None: 317 R13_VPDB = D4xdata().R13_VPDB 318 319 if R17_VSMOW is None: 320 R17_VSMOW = D4xdata().R17_VSMOW 321 322 if R18_VSMOW is None: 323 R18_VSMOW = D4xdata().R18_VSMOW 324 325 if LAMBDA_17 is None: 326 LAMBDA_17 = D4xdata().LAMBDA_17 327 328 if R18_VPDB is None: 329 R18_VPDB = D4xdata().R18_VPDB 330 331 R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW) ** LAMBDA_17 332 333 if Nominal_D47 is None: 334 Nominal_D47 = D47data().Nominal_D47 335 336 if Nominal_D48 is None: 337 Nominal_D48 = D48data().Nominal_D48 338 339 if d13C_VPDB is None: 340 if sample in Nominal_d13C_VPDB: 341 d13C_VPDB = Nominal_d13C_VPDB[sample] 342 else: 343 raise KeyError(f"Sample {sample} is missing d13C_VDP value, and it is not defined in Nominal_d13C_VDP.") 344 345 if d18O_VPDB is None: 346 if sample in Nominal_d18O_VPDB: 347 d18O_VPDB = Nominal_d18O_VPDB[sample] 348 else: 349 raise KeyError(f"Sample {sample} is missing d18O_VPDB value, and it is not defined in Nominal_d18O_VPDB.") 350 351 if D47 is None: 352 if sample in Nominal_D47: 353 D47 = Nominal_D47[sample] 354 else: 355 raise KeyError(f"Sample {sample} is missing D47 value, and it is not defined in Nominal_D47.") 356 357 if D48 is None: 358 if sample in Nominal_D48: 359 D48 = Nominal_D48[sample] 360 else: 361 raise KeyError(f"Sample {sample} is missing D48 value, and it is not defined in Nominal_D48.") 362 363 X = D4xdata() 364 X.R13_VPDB = R13_VPDB 365 X.R17_VSMOW = R17_VSMOW 366 X.R18_VSMOW = R18_VSMOW 367 X.LAMBDA_17 = LAMBDA_17 368 X.R18_VPDB = R18_VPDB 369 X.R17_VPDB = R17_VSMOW * (R18_VPDB / R18_VSMOW)**LAMBDA_17 370 371 R45wg, R46wg, R47wg, R48wg, R49wg = X.compute_isobar_ratios( 372 R13 = R13_VPDB * (1 + d13Cwg_VPDB/1000), 373 R18 = R18_VSMOW * (1 + d18Owg_VSMOW/1000), 374 ) 375 R45, R46, R47, R48, R49 = X.compute_isobar_ratios( 376 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 377 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 378 D17O=D17O, D47=D47, D48=D48, D49=D49, 379 ) 380 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = X.compute_isobar_ratios( 381 R13 = R13_VPDB * (1 + d13C_VPDB/1000), 382 R18 = R18_VPDB * (1 + d18O_VPDB/1000) * ALPHA_18O_ACID_REACTION, 383 D17O=D17O, 384 ) 385 386 d45 = 1000 * (R45/R45wg - 1) 387 d46 = 1000 * (R46/R46wg - 1) 388 d47 = 1000 * (R47/R47wg - 1) 389 d48 = 1000 * (R48/R48wg - 1) 390 d49 = 1000 * (R49/R49wg - 1) 391 392 for k in range(3): # dumb iteration to adjust for small changes in d47 393 R47raw = (1 + (a47 * D47 + b47 * d47 + c47)/1000) * R47stoch 394 R48raw = (1 + (a48 * D48 + b48 * d48 + c48)/1000) * R48stoch 395 d47 = 1000 * (R47raw/R47wg - 1) 396 d48 = 1000 * (R48raw/R48wg - 1) 397 398 return dict( 399 Sample = sample, 400 D17O = D17O, 401 d13Cwg_VPDB = d13Cwg_VPDB, 402 d18Owg_VSMOW = d18Owg_VSMOW, 403 d45 = d45, 404 d46 = d46, 405 d47 = d47, 406 d48 = d48, 407 d49 = d49, 408 ) 409 410 411def virtual_data( 412 samples = [], 413 a47 = 1., b47 = 0., c47 = -0.9, 414 a48 = 1., b48 = 0., c48 = -0.45, 415 rD47 = 0.015, rD48 = 0.045, 416 d13Cwg_VPDB = None, d18Owg_VSMOW = None, 417 session = None, 418 Nominal_D47 = None, Nominal_D48 = None, 419 Nominal_d13C_VPDB = None, Nominal_d18O_VPDB = None, 420 ALPHA_18O_ACID_REACTION = None, 421 R13_VPDB = None, 422 R17_VSMOW = None, 423 R18_VSMOW = None, 424 LAMBDA_17 = None, 425 R18_VPDB = None, 426 seed = 0, 427 ): 428 ''' 429 Return list with simulated analyses from a single session. 430 431 **Parameters** 432 433 + `samples`: a list of entries; each entry is a dictionary with the following fields: 434 * `Sample`: the name of the sample 435 * `d13C_VPDB`, `d18O_VPDB`: bulk composition of the carbonate sample 436 * `D47`, `D48`, `D49`, `D17O` (all optional): clumped-isotope and oxygen-17 anomalies of the carbonate sample 437 * `N`: how many analyses to generate for this sample 438 + `a47`: scrambling factor for Δ47 439 + `b47`: compositional nonlinearity for Δ47 440 + `c47`: working gas offset for Δ47 441 + `a48`: scrambling factor for Δ48 442 + `b48`: compositional nonlinearity for Δ48 443 + `c48`: working gas offset for Δ48 444 + `rD47`: analytical repeatability of Δ47 445 + `rD48`: analytical repeatability of Δ48 446 + `d13Cwg_VPDB`, `d18Owg_VSMOW`: bulk composition of the working gas 447 (by default equal to the `simulate_single_analysis` default values) 448 + `session`: name of the session (no name by default) 449 + `Nominal_D47`, `Nominal_D48`: where to lookup Δ47 and Δ48 values 450 if `D47` or `D48` are not specified (by default equal to the `simulate_single_analysis` defaults) 451 + `Nominal_d13C_VPDB`, `Nominal_d18O_VPDB`: where to lookup δ13C and 452 δ18O values if `d13C_VPDB` or `d18O_VPDB` are not specified 453 (by default equal to the `simulate_single_analysis` defaults) 454 + `ALPHA_18O_ACID_REACTION`: 18O/16O acid fractionation factor 455 (by default equal to the `simulate_single_analysis` defaults) 456 + `R13_VPDB`, `R17_VSMOW`, `R18_VSMOW`, `LAMBDA_17`, `R18_VPDB`: oxygen-17 457 correction parameters (by default equal to the `simulate_single_analysis` default) 458 + `seed`: explicitly set to a non-zero value to achieve random but repeatable simulations 459 460 461 Here is an example of using this method to generate an arbitrary combination of 462 anchors and unknowns for a bunch of sessions: 463 464 ```py 465 args = dict( 466 samples = [ 467 dict(Sample = 'ETH-1', N = 4), 468 dict(Sample = 'ETH-2', N = 5), 469 dict(Sample = 'ETH-3', N = 6), 470 dict(Sample = 'FOO', N = 2, 471 d13C_VPDB = -5., d18O_VPDB = -10., 472 D47 = 0.3, D48 = 0.15), 473 ], rD47 = 0.010, rD48 = 0.030) 474 475 session1 = virtual_data(session = 'Session_01', **args, seed = 123) 476 session2 = virtual_data(session = 'Session_02', **args, seed = 1234) 477 session3 = virtual_data(session = 'Session_03', **args, seed = 12345) 478 session4 = virtual_data(session = 'Session_04', **args, seed = 123456) 479 480 D = D47data(session1 + session2 + session3 + session4) 481 482 D.crunch() 483 D.standardize() 484 485 D.table_of_sessions(verbose = True, save_to_file = False) 486 D.table_of_samples(verbose = True, save_to_file = False) 487 D.table_of_analyses(verbose = True, save_to_file = False) 488 ``` 489 490 This should output something like: 491 492 ``` 493 [table_of_sessions] 494 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 495 Session Na Nu d13Cwg_VPDB d18Owg_VSMOW r_d13C r_d18O r_D47 a ± SE 1e3 x b ± SE c ± SE 496 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 497 Session_01 15 2 -4.000 26.000 0.0000 0.0000 0.0110 0.997 ± 0.017 -0.097 ± 0.244 -0.896 ± 0.006 498 Session_02 15 2 -4.000 26.000 0.0000 0.0000 0.0109 1.002 ± 0.017 -0.110 ± 0.244 -0.901 ± 0.006 499 Session_03 15 2 -4.000 26.000 0.0000 0.0000 0.0107 1.010 ± 0.017 -0.037 ± 0.244 -0.904 ± 0.006 500 Session_04 15 2 -4.000 26.000 0.0000 0.0000 0.0106 1.001 ± 0.017 -0.181 ± 0.244 -0.894 ± 0.006 501 –––––––––– –– –– ––––––––––– –––––––––––– –––––– –––––– –––––– ––––––––––––– –––––––––––––– –––––––––––––– 502 503 [table_of_samples] 504 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 505 Sample N d13C_VPDB d18O_VSMOW D47 SE 95% CL SD p_Levene 506 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 507 ETH-1 16 2.02 37.02 0.2052 0.0079 508 ETH-2 20 -10.17 19.88 0.2085 0.0100 509 ETH-3 24 1.71 37.45 0.6132 0.0105 510 FOO 8 -5.00 28.91 0.2989 0.0040 ± 0.0080 0.0101 0.638 511 –––––– –– ––––––––– –––––––––– –––––– –––––– –––––––– –––––– –––––––– 512 513 [table_of_analyses] 514 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 515 UID Session Sample d13Cwg_VPDB d18Owg_VSMOW d45 d46 d47 d48 d49 d13C_VPDB d18O_VSMOW D47raw D48raw D49raw D47 516 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 517 1 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.122986 21.273526 27.780042 2.020000 37.024281 -0.706013 -0.328878 -0.000013 0.192554 518 2 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.130144 21.282615 27.780042 2.020000 37.024281 -0.698974 -0.319981 -0.000013 0.199615 519 3 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.149219 21.299572 27.780042 2.020000 37.024281 -0.680215 -0.303383 -0.000013 0.218429 520 4 Session_01 ETH-1 -4.000 26.000 6.018962 10.747026 16.136616 21.233128 27.780042 2.020000 37.024281 -0.692609 -0.368421 -0.000013 0.205998 521 5 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.697171 -12.203054 -18.023381 -10.170000 19.875825 -0.680771 -0.290128 -0.000002 0.215054 522 6 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701124 -12.184422 -18.023381 -10.170000 19.875825 -0.684772 -0.271272 -0.000002 0.211041 523 7 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.715105 -12.195251 -18.023381 -10.170000 19.875825 -0.698923 -0.282232 -0.000002 0.196848 524 8 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701529 -12.204963 -18.023381 -10.170000 19.875825 -0.685182 -0.292061 -0.000002 0.210630 525 9 Session_01 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.711420 -12.228478 -18.023381 -10.170000 19.875825 -0.695193 -0.315859 -0.000002 0.200589 526 10 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.666719 22.296486 28.306614 1.710000 37.450394 -0.290459 -0.147284 -0.000014 0.609363 527 11 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.671553 22.291060 28.306614 1.710000 37.450394 -0.285706 -0.152592 -0.000014 0.614130 528 12 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.652854 22.273271 28.306614 1.710000 37.450394 -0.304093 -0.169990 -0.000014 0.595689 529 13 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.684168 22.263156 28.306614 1.710000 37.450394 -0.273302 -0.179883 -0.000014 0.626572 530 14 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.662702 22.253578 28.306614 1.710000 37.450394 -0.294409 -0.189251 -0.000014 0.605401 531 15 Session_01 ETH-3 -4.000 26.000 5.742374 11.161270 16.681957 22.230907 28.306614 1.710000 37.450394 -0.275476 -0.211424 -0.000014 0.624391 532 16 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.312044 5.395798 4.665655 -5.000000 28.907344 -0.598436 -0.268176 -0.000006 0.298996 533 17 Session_01 FOO -4.000 26.000 -0.840413 2.828738 1.328123 5.307086 4.665655 -5.000000 28.907344 -0.582387 -0.356389 -0.000006 0.315092 534 18 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.122201 21.340606 27.780042 2.020000 37.024281 -0.706785 -0.263217 -0.000013 0.195135 535 19 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.134868 21.305714 27.780042 2.020000 37.024281 -0.694328 -0.297370 -0.000013 0.207564 536 20 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.140008 21.261931 27.780042 2.020000 37.024281 -0.689273 -0.340227 -0.000013 0.212607 537 21 Session_02 ETH-1 -4.000 26.000 6.018962 10.747026 16.135540 21.298472 27.780042 2.020000 37.024281 -0.693667 -0.304459 -0.000013 0.208224 538 22 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701213 -12.202602 -18.023381 -10.170000 19.875825 -0.684862 -0.289671 -0.000002 0.213842 539 23 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.685649 -12.190405 -18.023381 -10.170000 19.875825 -0.669108 -0.277327 -0.000002 0.229559 540 24 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.719003 -12.257955 -18.023381 -10.170000 19.875825 -0.702869 -0.345692 -0.000002 0.195876 541 25 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.700592 -12.204641 -18.023381 -10.170000 19.875825 -0.684233 -0.291735 -0.000002 0.214469 542 26 Session_02 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720426 -12.214561 -18.023381 -10.170000 19.875825 -0.704308 -0.301774 -0.000002 0.194439 543 27 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.673044 22.262090 28.306614 1.710000 37.450394 -0.284240 -0.180926 -0.000014 0.616730 544 28 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.666542 22.263401 28.306614 1.710000 37.450394 -0.290634 -0.179643 -0.000014 0.610350 545 29 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.680487 22.243486 28.306614 1.710000 37.450394 -0.276921 -0.199121 -0.000014 0.624031 546 30 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.663900 22.245175 28.306614 1.710000 37.450394 -0.293231 -0.197469 -0.000014 0.607759 547 31 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.674379 22.301309 28.306614 1.710000 37.450394 -0.282927 -0.142568 -0.000014 0.618039 548 32 Session_02 ETH-3 -4.000 26.000 5.742374 11.161270 16.660825 22.270466 28.306614 1.710000 37.450394 -0.296255 -0.172733 -0.000014 0.604742 549 33 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.294076 5.349940 4.665655 -5.000000 28.907344 -0.616369 -0.313776 -0.000006 0.283707 550 34 Session_02 FOO -4.000 26.000 -0.840413 2.828738 1.313775 5.292121 4.665655 -5.000000 28.907344 -0.596708 -0.371269 -0.000006 0.303323 551 35 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.121613 21.259909 27.780042 2.020000 37.024281 -0.707364 -0.342207 -0.000013 0.194934 552 36 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.145714 21.304889 27.780042 2.020000 37.024281 -0.683661 -0.298178 -0.000013 0.218401 553 37 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.126573 21.325093 27.780042 2.020000 37.024281 -0.702485 -0.278401 -0.000013 0.199764 554 38 Session_03 ETH-1 -4.000 26.000 6.018962 10.747026 16.132057 21.323211 27.780042 2.020000 37.024281 -0.697092 -0.280244 -0.000013 0.205104 555 39 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.708448 -12.232023 -18.023381 -10.170000 19.875825 -0.692185 -0.319447 -0.000002 0.208915 556 40 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.714417 -12.202504 -18.023381 -10.170000 19.875825 -0.698226 -0.289572 -0.000002 0.202934 557 41 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.720039 -12.264469 -18.023381 -10.170000 19.875825 -0.703917 -0.352285 -0.000002 0.197300 558 42 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.701953 -12.228550 -18.023381 -10.170000 19.875825 -0.685611 -0.315932 -0.000002 0.215423 559 43 Session_03 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.704535 -12.213634 -18.023381 -10.170000 19.875825 -0.688224 -0.300836 -0.000002 0.212837 560 44 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.652920 22.230043 28.306614 1.710000 37.450394 -0.304028 -0.212269 -0.000014 0.594265 561 45 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.691485 22.261017 28.306614 1.710000 37.450394 -0.266106 -0.181975 -0.000014 0.631810 562 46 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.679119 22.305357 28.306614 1.710000 37.450394 -0.278266 -0.138609 -0.000014 0.619771 563 47 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.663623 22.327286 28.306614 1.710000 37.450394 -0.293503 -0.117161 -0.000014 0.604685 564 48 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.678524 22.282103 28.306614 1.710000 37.450394 -0.278851 -0.161352 -0.000014 0.619192 565 49 Session_03 ETH-3 -4.000 26.000 5.742374 11.161270 16.666246 22.283361 28.306614 1.710000 37.450394 -0.290925 -0.160121 -0.000014 0.607238 566 50 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.309929 5.340249 4.665655 -5.000000 28.907344 -0.600546 -0.323413 -0.000006 0.300148 567 51 Session_03 FOO -4.000 26.000 -0.840413 2.828738 1.317548 5.334102 4.665655 -5.000000 28.907344 -0.592942 -0.329524 -0.000006 0.307676 568 52 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.136865 21.300298 27.780042 2.020000 37.024281 -0.692364 -0.302672 -0.000013 0.204033 569 53 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.133538 21.291260 27.780042 2.020000 37.024281 -0.695637 -0.311519 -0.000013 0.200762 570 54 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.139991 21.319865 27.780042 2.020000 37.024281 -0.689290 -0.283519 -0.000013 0.207107 571 55 Session_04 ETH-1 -4.000 26.000 6.018962 10.747026 16.145748 21.330075 27.780042 2.020000 37.024281 -0.683629 -0.273524 -0.000013 0.212766 572 56 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702989 -12.202762 -18.023381 -10.170000 19.875825 -0.686660 -0.289833 -0.000002 0.204507 573 57 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.692830 -12.240287 -18.023381 -10.170000 19.875825 -0.676377 -0.327811 -0.000002 0.214786 574 58 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.702899 -12.180291 -18.023381 -10.170000 19.875825 -0.686568 -0.267091 -0.000002 0.204598 575 59 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.709282 -12.282257 -18.023381 -10.170000 19.875825 -0.693029 -0.370287 -0.000002 0.198140 576 60 Session_04 ETH-2 -4.000 26.000 -5.995859 -5.976076 -12.679330 -12.235994 -18.023381 -10.170000 19.875825 -0.662712 -0.323466 -0.000002 0.228446 577 61 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.695594 22.238663 28.306614 1.710000 37.450394 -0.262066 -0.203838 -0.000014 0.634200 578 62 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.663504 22.286354 28.306614 1.710000 37.450394 -0.293620 -0.157194 -0.000014 0.602656 579 63 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666457 22.254290 28.306614 1.710000 37.450394 -0.290717 -0.188555 -0.000014 0.605558 580 64 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.666910 22.223232 28.306614 1.710000 37.450394 -0.290271 -0.218930 -0.000014 0.606004 581 65 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.679662 22.257256 28.306614 1.710000 37.450394 -0.277732 -0.185653 -0.000014 0.618539 582 66 Session_04 ETH-3 -4.000 26.000 5.742374 11.161270 16.676768 22.267680 28.306614 1.710000 37.450394 -0.280578 -0.175459 -0.000014 0.615693 583 67 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.307663 5.317330 4.665655 -5.000000 28.907344 -0.602808 -0.346202 -0.000006 0.290853 584 68 Session_04 FOO -4.000 26.000 -0.840413 2.828738 1.308562 5.331400 4.665655 -5.000000 28.907344 -0.601911 -0.332212 -0.000006 0.291749 585 ––– –––––––––– –––––– ––––––––––– –––––––––––– ––––––––– ––––––––– –––––––––– –––––––––– –––––––––– –––––––––– –––––––––– ––––––––– ––––––––– ––––––––– –––––––– 586 ``` 587 ''' 588 589 kwargs = locals().copy() 590 591 from numpy import random as nprandom 592 if seed: 593 rng = nprandom.default_rng(seed) 594 else: 595 rng = nprandom.default_rng() 596 597 N = sum([s['N'] for s in samples]) 598 errors47 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 599 errors47 *= rD47 / stdev(errors47) # scale errors to rD47 600 errors48 = rng.normal(loc = 0, scale = 1, size = N) # generate random measurement errors 601 errors48 *= rD48 / stdev(errors48) # scale errors to rD48 602 603 k = 0 604 out = [] 605 for s in samples: 606 kw = {} 607 kw['sample'] = s['Sample'] 608 kw = { 609 **kw, 610 **{var: kwargs[var] 611 for var in [ 612 'd13Cwg_VPDB', 'd18Owg_VSMOW', 'ALPHA_18O_ACID_REACTION', 613 'Nominal_D47', 'Nominal_D48', 'Nominal_d13C_VPDB', 'Nominal_d18O_VPDB', 614 'R13_VPDB', 'R17_VSMOW', 'R18_VSMOW', 'LAMBDA_17', 'R18_VPDB', 615 'a47', 'b47', 'c47', 'a48', 'b48', 'c48', 616 ] 617 if kwargs[var] is not None}, 618 **{var: s[var] 619 for var in ['d13C_VPDB', 'd18O_VPDB', 'D47', 'D48', 'D49', 'D17O'] 620 if var in s}, 621 } 622 623 sN = s['N'] 624 while sN: 625 out.append(simulate_single_analysis(**kw)) 626 out[-1]['d47'] += errors47[k] * a47 627 out[-1]['d48'] += errors48[k] * a48 628 sN -= 1 629 k += 1 630 631 if session is not None: 632 for r in out: 633 r['Session'] = session 634 return out 635 636def table_of_samples( 637 data47 = None, 638 data48 = None, 639 dir = 'output', 640 filename = None, 641 save_to_file = True, 642 print_out = True, 643 output = None, 644 ): 645 ''' 646 Print out, save to disk and/or return a combined table of samples 647 for a pair of `D47data` and `D48data` objects. 648 649 **Parameters** 650 651 + `data47`: `D47data` instance 652 + `data48`: `D48data` instance 653 + `dir`: the directory in which to save the table 654 + `filename`: the name to the csv file to write to 655 + `save_to_file`: whether to save the table to disk 656 + `print_out`: whether to print out the table 657 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 658 if set to `'raw'`: return a list of list of strings 659 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 660 ''' 661 if data47 is None: 662 if data48 is None: 663 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 664 else: 665 return data48.table_of_samples( 666 dir = dir, 667 filename = filename, 668 save_to_file = save_to_file, 669 print_out = print_out, 670 output = output 671 ) 672 else: 673 if data48 is None: 674 return data47.table_of_samples( 675 dir = dir, 676 filename = filename, 677 save_to_file = save_to_file, 678 print_out = print_out, 679 output = output 680 ) 681 else: 682 out47 = data47.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 683 out48 = data48.table_of_samples(save_to_file = False, print_out = False, output = 'raw') 684 out = transpose_table(transpose_table(out47) + transpose_table(out48)[4:]) 685 686 if save_to_file: 687 if not os.path.exists(dir): 688 os.makedirs(dir) 689 if filename is None: 690 filename = f'D47D48_samples.csv' 691 with open(f'{dir}/{filename}', 'w') as fid: 692 fid.write(make_csv(out)) 693 if print_out: 694 print('\n'+pretty_table(out)) 695 if output == 'raw': 696 return out 697 elif output == 'pretty': 698 return pretty_table(out) 699 700 701def table_of_sessions( 702 data47 = None, 703 data48 = None, 704 dir = 'output', 705 filename = None, 706 save_to_file = True, 707 print_out = True, 708 output = None, 709 ): 710 ''' 711 Print out, save to disk and/or return a combined table of sessions 712 for a pair of `D47data` and `D48data` objects. 713 ***Only applicable if the sessions in `data47` and those in `data48` 714 consist of the exact same sets of analyses.*** 715 716 **Parameters** 717 718 + `data47`: `D47data` instance 719 + `data48`: `D48data` instance 720 + `dir`: the directory in which to save the table 721 + `filename`: the name to the csv file to write to 722 + `save_to_file`: whether to save the table to disk 723 + `print_out`: whether to print out the table 724 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 725 if set to `'raw'`: return a list of list of strings 726 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 727 ''' 728 if data47 is None: 729 if data48 is None: 730 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 731 else: 732 return data48.table_of_sessions( 733 dir = dir, 734 filename = filename, 735 save_to_file = save_to_file, 736 print_out = print_out, 737 output = output 738 ) 739 else: 740 if data48 is None: 741 return data47.table_of_sessions( 742 dir = dir, 743 filename = filename, 744 save_to_file = save_to_file, 745 print_out = print_out, 746 output = output 747 ) 748 else: 749 out47 = data47.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 750 out48 = data48.table_of_sessions(save_to_file = False, print_out = False, output = 'raw') 751 for k,x in enumerate(out47[0]): 752 if k>7: 753 out47[0][k] = out47[0][k].replace('a', 'a_47').replace('b', 'b_47').replace('c', 'c_47') 754 out48[0][k] = out48[0][k].replace('a', 'a_48').replace('b', 'b_48').replace('c', 'c_48') 755 out = transpose_table(transpose_table(out47) + transpose_table(out48)[7:]) 756 757 if save_to_file: 758 if not os.path.exists(dir): 759 os.makedirs(dir) 760 if filename is None: 761 filename = f'D47D48_sessions.csv' 762 with open(f'{dir}/{filename}', 'w') as fid: 763 fid.write(make_csv(out)) 764 if print_out: 765 print('\n'+pretty_table(out)) 766 if output == 'raw': 767 return out 768 elif output == 'pretty': 769 return pretty_table(out) 770 771 772def table_of_analyses( 773 data47 = None, 774 data48 = None, 775 dir = 'output', 776 filename = None, 777 save_to_file = True, 778 print_out = True, 779 output = None, 780 ): 781 ''' 782 Print out, save to disk and/or return a combined table of analyses 783 for a pair of `D47data` and `D48data` objects. 784 785 If the sessions in `data47` and those in `data48` do not consist of 786 the exact same sets of analyses, the table will have two columns 787 `Session_47` and `Session_48` instead of a single `Session` column. 788 789 **Parameters** 790 791 + `data47`: `D47data` instance 792 + `data48`: `D48data` instance 793 + `dir`: the directory in which to save the table 794 + `filename`: the name to the csv file to write to 795 + `save_to_file`: whether to save the table to disk 796 + `print_out`: whether to print out the table 797 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 798 if set to `'raw'`: return a list of list of strings 799 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 800 ''' 801 if data47 is None: 802 if data48 is None: 803 raise TypeError("Arguments must include at least one D47data() or D48data() instance.") 804 else: 805 return data48.table_of_analyses( 806 dir = dir, 807 filename = filename, 808 save_to_file = save_to_file, 809 print_out = print_out, 810 output = output 811 ) 812 else: 813 if data48 is None: 814 return data47.table_of_analyses( 815 dir = dir, 816 filename = filename, 817 save_to_file = save_to_file, 818 print_out = print_out, 819 output = output 820 ) 821 else: 822 out47 = data47.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 823 out48 = data48.table_of_analyses(save_to_file = False, print_out = False, output = 'raw') 824 825 if [l[1] for l in out47[1:]] == [l[1] for l in out48[1:]]: # if sessions are identical 826 out = transpose_table(transpose_table(out47) + transpose_table(out48)[-1:]) 827 else: 828 out47[0][1] = 'Session_47' 829 out48[0][1] = 'Session_48' 830 out47 = transpose_table(out47) 831 out48 = transpose_table(out48) 832 out = transpose_table(out47[:2] + out48[1:2] + out47[2:] + out48[-1:]) 833 834 if save_to_file: 835 if not os.path.exists(dir): 836 os.makedirs(dir) 837 if filename is None: 838 filename = f'D47D48_sessions.csv' 839 with open(f'{dir}/{filename}', 'w') as fid: 840 fid.write(make_csv(out)) 841 if print_out: 842 print('\n'+pretty_table(out)) 843 if output == 'raw': 844 return out 845 elif output == 'pretty': 846 return pretty_table(out) 847 848 849def _fullcovar(minresult, epsilon = 0.01, named = False): 850 ''' 851 Construct full covariance matrix in the case of constrained parameters 852 ''' 853 854 import asteval 855 856 def f(values): 857 interp = asteval.Interpreter() 858 print(minresult.var_names, values) 859 for n,v in zip(minresult.var_names, values): 860 interp(f'{n} = {v}') 861 print(f'{n} = {v}') 862 for q in minresult.params: 863 print(q, minresult.params[q].expr) 864 if minresult.params[q].expr: 865 interp(f'{q} = {minresult.params[q].expr}') 866 print(f'{q} = {minresult.params[q].expr}') 867 print() 868 return np.array([interp.symtable[q] for q in minresult.params]) 869 870 # construct Jacobian 871 J = np.zeros((minresult.nvarys, len(minresult.params))) 872 X = np.array([minresult.params[p].value for p in minresult.var_names]) 873 sX = np.array([minresult.params[p].stderr for p in minresult.var_names]) 874 875 for j in range(minresult.nvarys): 876 x1 = [_ for _ in X] 877 x1[j] += epsilon * sX[j] 878 x2 = [_ for _ in X] 879 x2[j] -= epsilon * sX[j] 880 J[j,:] = (f(x1) - f(x2)) / (2 * epsilon * sX[j]) 881 882 _names = [q for q in minresult.params] 883 _covar = J.T @ minresult.covar @ J 884 _se = np.diag(_covar)**.5 885 _correl = _covar.copy() 886 for k,s in enumerate(_se): 887 if s: 888 _correl[k,:] /= s 889 _correl[:,k] /= s 890 891 if named: 892 _covar = {i: {j:_covar[i,j] for j in minresult.params} for i in minresult.params} 893 _se = {i: _se[i] for i in minresult.params} 894 _correl = {i: {j:_correl[i,j] for j in minresult.params} for i in minresult.params} 895 896 return _names, _covar, _se, _correl 897 898 899class D4xdata(list): 900 ''' 901 Store and process data for a large set of Δ47 and/or Δ48 902 analyses, usually comprising more than one analytical session. 903 ''' 904 905 ### 17O CORRECTION PARAMETERS 906 R13_VPDB = 0.01118 # (Chang & Li, 1990) 907 ''' 908 Absolute (13C/12C) ratio of VPDB. 909 By default equal to 0.01118 ([Chang & Li, 1990](http://www.cnki.com.cn/Article/CJFDTotal-JXTW199004006.htm)) 910 ''' 911 912 R18_VSMOW = 0.0020052 # (Baertschi, 1976) 913 ''' 914 Absolute (18O/16C) ratio of VSMOW. 915 By default equal to 0.0020052 ([Baertschi, 1976](https://doi.org/10.1016/0012-821X(76)90115-1)) 916 ''' 917 918 LAMBDA_17 = 0.528 # (Barkan & Luz, 2005) 919 ''' 920 Mass-dependent exponent for triple oxygen isotopes. 921 By default equal to 0.528 ([Barkan & Luz, 2005](https://doi.org/10.1002/rcm.2250)) 922 ''' 923 924 R17_VSMOW = 0.00038475 # (Assonov & Brenninkmeijer, 2003, rescaled to R13_VPDB) 925 ''' 926 Absolute (17O/16C) ratio of VSMOW. 927 By default equal to 0.00038475 928 ([Assonov & Brenninkmeijer, 2003](https://dx.doi.org/10.1002/rcm.1011), 929 rescaled to `R13_VPDB`) 930 ''' 931 932 R18_VPDB = R18_VSMOW * 1.03092 933 ''' 934 Absolute (18O/16C) ratio of VPDB. 935 By definition equal to `R18_VSMOW * 1.03092`. 936 ''' 937 938 R17_VPDB = R17_VSMOW * 1.03092 ** LAMBDA_17 939 ''' 940 Absolute (17O/16C) ratio of VPDB. 941 By definition equal to `R17_VSMOW * 1.03092 ** LAMBDA_17`. 942 ''' 943 944 LEVENE_REF_SAMPLE = 'ETH-3' 945 ''' 946 After the Δ4x standardization step, each sample is tested to 947 assess whether the Δ4x variance within all analyses for that 948 sample differs significantly from that observed for a given reference 949 sample (using [Levene's test](https://en.wikipedia.org/wiki/Levene%27s_test), 950 which yields a p-value corresponding to the null hypothesis that the 951 underlying variances are equal). 952 953 `LEVENE_REF_SAMPLE` (by default equal to `'ETH-3'`) specifies which 954 sample should be used as a reference for this test. 955 ''' 956 957 ALPHA_18O_ACID_REACTION = round(np.exp(3.59 / (90 + 273.15) - 1.79e-3), 6) # (Kim et al., 2007, calcite) 958 ''' 959 Specifies the 18O/16O fractionation factor generally applicable 960 to acid reactions in the dataset. Currently used by `D4xdata.wg()`, 961 `D4xdata.standardize_d13C`, and `D4xdata.standardize_d18O`. 962 963 By default equal to 1.008129 (calcite reacted at 90 °C, 964 [Kim et al., 2007](https://dx.doi.org/10.1016/j.chemgeo.2007.08.005)). 965 ''' 966 967 Nominal_d13C_VPDB = { 968 'ETH-1': 2.02, 969 'ETH-2': -10.17, 970 'ETH-3': 1.71, 971 } # (Bernasconi et al., 2018) 972 ''' 973 Nominal δ13C_VPDB values assigned to carbonate standards, used by 974 `D4xdata.standardize_d13C()`. 975 976 By default equal to `{'ETH-1': 2.02, 'ETH-2': -10.17, 'ETH-3': 1.71}` after 977 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 978 ''' 979 980 Nominal_d18O_VPDB = { 981 'ETH-1': -2.19, 982 'ETH-2': -18.69, 983 'ETH-3': -1.78, 984 } # (Bernasconi et al., 2018) 985 ''' 986 Nominal δ18O_VPDB values assigned to carbonate standards, used by 987 `D4xdata.standardize_d18O()`. 988 989 By default equal to `{'ETH-1': -2.19, 'ETH-2': -18.69, 'ETH-3': -1.78}` after 990 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 991 ''' 992 993 d13C_STANDARDIZATION_METHOD = '2pt' 994 ''' 995 Method by which to standardize δ13C values: 996 997 + `none`: do not apply any δ13C standardization. 998 + `'1pt'`: within each session, offset all initial δ13C values so as to 999 minimize the difference between final δ13C_VPDB values and 1000 `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` is defined). 1001 + `'2pt'`: within each session, apply a affine trasformation to all δ13C 1002 values so as to minimize the difference between final δ13C_VPDB 1003 values and `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` 1004 is defined). 1005 ''' 1006 1007 d18O_STANDARDIZATION_METHOD = '2pt' 1008 ''' 1009 Method by which to standardize δ18O values: 1010 1011 + `none`: do not apply any δ18O standardization. 1012 + `'1pt'`: within each session, offset all initial δ18O values so as to 1013 minimize the difference between final δ18O_VPDB values and 1014 `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` is defined). 1015 + `'2pt'`: within each session, apply a affine trasformation to all δ18O 1016 values so as to minimize the difference between final δ18O_VPDB 1017 values and `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` 1018 is defined). 1019 ''' 1020 1021 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 1022 ''' 1023 **Parameters** 1024 1025 + `l`: a list of dictionaries, with each dictionary including at least the keys 1026 `Sample`, `d45`, `d46`, and `d47` or `d48`. 1027 + `mass`: `'47'` or `'48'` 1028 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 1029 + `session`: define session name for analyses without a `Session` key 1030 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 1031 1032 Returns a `D4xdata` object derived from `list`. 1033 ''' 1034 self._4x = mass 1035 self.verbose = verbose 1036 self.prefix = 'D4xdata' 1037 self.logfile = logfile 1038 list.__init__(self, l) 1039 self.Nf = None 1040 self.repeatability = {} 1041 self.refresh(session = session) 1042 1043 1044 def make_verbal(oldfun): 1045 ''' 1046 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 1047 ''' 1048 @wraps(oldfun) 1049 def newfun(*args, verbose = '', **kwargs): 1050 myself = args[0] 1051 oldprefix = myself.prefix 1052 myself.prefix = oldfun.__name__ 1053 if verbose != '': 1054 oldverbose = myself.verbose 1055 myself.verbose = verbose 1056 out = oldfun(*args, **kwargs) 1057 myself.prefix = oldprefix 1058 if verbose != '': 1059 myself.verbose = oldverbose 1060 return out 1061 return newfun 1062 1063 1064 def msg(self, txt): 1065 ''' 1066 Log a message to `self.logfile`, and print it out if `verbose = True` 1067 ''' 1068 self.log(txt) 1069 if self.verbose: 1070 print(f'{f"[{self.prefix}]":<16} {txt}') 1071 1072 1073 def vmsg(self, txt): 1074 ''' 1075 Log a message to `self.logfile` and print it out 1076 ''' 1077 self.log(txt) 1078 print(txt) 1079 1080 1081 def log(self, *txts): 1082 ''' 1083 Log a message to `self.logfile` 1084 ''' 1085 if self.logfile: 1086 with open(self.logfile, 'a') as fid: 1087 for txt in txts: 1088 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}') 1089 1090 1091 def refresh(self, session = 'mySession'): 1092 ''' 1093 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1094 ''' 1095 self.fill_in_missing_info(session = session) 1096 self.refresh_sessions() 1097 self.refresh_samples() 1098 1099 1100 def refresh_sessions(self): 1101 ''' 1102 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1103 to `False` for all sessions. 1104 ''' 1105 self.sessions = { 1106 s: {'data': [r for r in self if r['Session'] == s]} 1107 for s in sorted({r['Session'] for r in self}) 1108 } 1109 for s in self.sessions: 1110 self.sessions[s]['scrambling_drift'] = False 1111 self.sessions[s]['slope_drift'] = False 1112 self.sessions[s]['wg_drift'] = False 1113 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1114 self.sessions[s]['d18O_standardization_method'] = self.d18O_STANDARDIZATION_METHOD 1115 1116 1117 def refresh_samples(self): 1118 ''' 1119 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1120 ''' 1121 self.samples = { 1122 s: {'data': [r for r in self if r['Sample'] == s]} 1123 for s in sorted({r['Sample'] for r in self}) 1124 } 1125 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1126 self.unknowns = {s: self.samples[s] for s in self.samples if s not in self.Nominal_D4x} 1127 1128 1129 def read(self, filename, sep = '', session = ''): 1130 ''' 1131 Read file in csv format to load data into a `D47data` object. 1132 1133 In the csv file, spaces before and after field separators (`','` by default) 1134 are optional. Each line corresponds to a single analysis. 1135 1136 The required fields are: 1137 1138 + `UID`: a unique identifier 1139 + `Session`: an identifier for the analytical session 1140 + `Sample`: a sample identifier 1141 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1142 1143 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1144 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1145 and `d49` are optional, and set to NaN by default. 1146 1147 **Parameters** 1148 1149 + `fileneme`: the path of the file to read 1150 + `sep`: csv separator delimiting the fields 1151 + `session`: set `Session` field to this string for all analyses 1152 ''' 1153 with open(filename) as fid: 1154 self.input(fid.read(), sep = sep, session = session) 1155 1156 1157 def input(self, txt, sep = '', session = ''): 1158 ''' 1159 Read `txt` string in csv format to load analysis data into a `D47data` object. 1160 1161 In the csv string, spaces before and after field separators (`','` by default) 1162 are optional. Each line corresponds to a single analysis. 1163 1164 The required fields are: 1165 1166 + `UID`: a unique identifier 1167 + `Session`: an identifier for the analytical session 1168 + `Sample`: a sample identifier 1169 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1170 1171 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1172 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1173 and `d49` are optional, and set to NaN by default. 1174 1175 **Parameters** 1176 1177 + `txt`: the csv string to read 1178 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1179 whichever appers most often in `txt`. 1180 + `session`: set `Session` field to this string for all analyses 1181 ''' 1182 if sep == '': 1183 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1184 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1185 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:]] 1186 1187 if session != '': 1188 for r in data: 1189 r['Session'] = session 1190 1191 self += data 1192 self.refresh() 1193 1194 1195 @make_verbal 1196 def wg(self, samples = None, a18_acid = None): 1197 ''' 1198 Compute bulk composition of the working gas for each session based on 1199 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1200 `self.Nominal_d18O_VPDB`. 1201 ''' 1202 1203 self.msg('Computing WG composition:') 1204 1205 if a18_acid is None: 1206 a18_acid = self.ALPHA_18O_ACID_REACTION 1207 if samples is None: 1208 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1209 1210 assert a18_acid, f'Acid fractionation factor should not be zero.' 1211 1212 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1213 R45R46_standards = {} 1214 for sample in samples: 1215 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1216 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1217 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1218 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1219 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1220 1221 C12_s = 1 / (1 + R13_s) 1222 C13_s = R13_s / (1 + R13_s) 1223 C16_s = 1 / (1 + R17_s + R18_s) 1224 C17_s = R17_s / (1 + R17_s + R18_s) 1225 C18_s = R18_s / (1 + R17_s + R18_s) 1226 1227 C626_s = C12_s * C16_s ** 2 1228 C627_s = 2 * C12_s * C16_s * C17_s 1229 C628_s = 2 * C12_s * C16_s * C18_s 1230 C636_s = C13_s * C16_s ** 2 1231 C637_s = 2 * C13_s * C16_s * C17_s 1232 C727_s = C12_s * C17_s ** 2 1233 1234 R45_s = (C627_s + C636_s) / C626_s 1235 R46_s = (C628_s + C637_s + C727_s) / C626_s 1236 R45R46_standards[sample] = (R45_s, R46_s) 1237 1238 for s in self.sessions: 1239 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1240 assert db, f'No sample from {samples} found in session "{s}".' 1241# dbsamples = sorted({r['Sample'] for r in db}) 1242 1243 X = [r['d45'] for r in db] 1244 Y = [R45R46_standards[r['Sample']][0] for r in db] 1245 x1, x2 = np.min(X), np.max(X) 1246 1247 if x1 < x2: 1248 wgcoord = x1/(x1-x2) 1249 else: 1250 wgcoord = 999 1251 1252 if wgcoord < -.5 or wgcoord > 1.5: 1253 # unreasonable to extrapolate to d45 = 0 1254 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1255 else : 1256 # d45 = 0 is reasonably well bracketed 1257 R45_wg = np.polyfit(X, Y, 1)[1] 1258 1259 X = [r['d46'] for r in db] 1260 Y = [R45R46_standards[r['Sample']][1] for r in db] 1261 x1, x2 = np.min(X), np.max(X) 1262 1263 if x1 < x2: 1264 wgcoord = x1/(x1-x2) 1265 else: 1266 wgcoord = 999 1267 1268 if wgcoord < -.5 or wgcoord > 1.5: 1269 # unreasonable to extrapolate to d46 = 0 1270 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1271 else : 1272 # d46 = 0 is reasonably well bracketed 1273 R46_wg = np.polyfit(X, Y, 1)[1] 1274 1275 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1276 1277 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1278 1279 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1280 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1281 for r in self.sessions[s]['data']: 1282 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1283 r['d18Owg_VSMOW'] = d18Owg_VSMOW 1284 1285 1286 def compute_bulk_delta(self, R45, R46, D17O = 0): 1287 ''' 1288 Compute δ13C_VPDB and δ18O_VSMOW, 1289 by solving the generalized form of equation (17) from 1290 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1291 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1292 solving the corresponding second-order Taylor polynomial. 1293 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1294 ''' 1295 1296 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1297 1298 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1299 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1300 C = 2 * self.R18_VSMOW 1301 D = -R46 1302 1303 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1304 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1305 cc = A + B + C + D 1306 1307 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1308 1309 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1310 R17 = K * R18 ** self.LAMBDA_17 1311 R13 = R45 - 2 * R17 1312 1313 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1314 1315 return d13C_VPDB, d18O_VSMOW 1316 1317 1318 @make_verbal 1319 def crunch(self, verbose = ''): 1320 ''' 1321 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1322 ''' 1323 for r in self: 1324 self.compute_bulk_and_clumping_deltas(r) 1325 self.standardize_d13C() 1326 self.standardize_d18O() 1327 self.msg(f"Crunched {len(self)} analyses.") 1328 1329 1330 def fill_in_missing_info(self, session = 'mySession'): 1331 ''' 1332 Fill in optional fields with default values 1333 ''' 1334 for i,r in enumerate(self): 1335 if 'D17O' not in r: 1336 r['D17O'] = 0. 1337 if 'UID' not in r: 1338 r['UID'] = f'{i+1}' 1339 if 'Session' not in r: 1340 r['Session'] = session 1341 for k in ['d47', 'd48', 'd49']: 1342 if k not in r: 1343 r[k] = np.nan 1344 1345 1346 def standardize_d13C(self): 1347 ''' 1348 Perform δ13C standadization within each session `s` according to 1349 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1350 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1351 may be redefined abitrarily at a later stage. 1352 ''' 1353 for s in self.sessions: 1354 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1355 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] 1356 X,Y = zip(*XY) 1357 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1358 offset = np.mean(Y) - np.mean(X) 1359 for r in self.sessions[s]['data']: 1360 r['d13C_VPDB'] += offset 1361 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1362 a,b = np.polyfit(X,Y,1) 1363 for r in self.sessions[s]['data']: 1364 r['d13C_VPDB'] = a * r['d13C_VPDB'] + b 1365 1366 def standardize_d18O(self): 1367 ''' 1368 Perform δ18O standadization within each session `s` according to 1369 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1370 which is defined by default by `D47data.refresh_sessions()`as equal to 1371 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1372 ''' 1373 for s in self.sessions: 1374 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1375 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] 1376 X,Y = zip(*XY) 1377 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1378 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1379 offset = np.mean(Y) - np.mean(X) 1380 for r in self.sessions[s]['data']: 1381 r['d18O_VSMOW'] += offset 1382 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1383 a,b = np.polyfit(X,Y,1) 1384 for r in self.sessions[s]['data']: 1385 r['d18O_VSMOW'] = a * r['d18O_VSMOW'] + b 1386 1387 1388 def compute_bulk_and_clumping_deltas(self, r): 1389 ''' 1390 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1391 ''' 1392 1393 # Compute working gas R13, R18, and isobar ratios 1394 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1395 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1396 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1397 1398 # Compute analyte isobar ratios 1399 R45 = (1 + r['d45'] / 1000) * R45_wg 1400 R46 = (1 + r['d46'] / 1000) * R46_wg 1401 R47 = (1 + r['d47'] / 1000) * R47_wg 1402 R48 = (1 + r['d48'] / 1000) * R48_wg 1403 R49 = (1 + r['d49'] / 1000) * R49_wg 1404 1405 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1406 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1407 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1408 1409 # Compute stochastic isobar ratios of the analyte 1410 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1411 R13, R18, D17O = r['D17O'] 1412 ) 1413 1414 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1415 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1416 if (R45 / R45stoch - 1) > 5e-8: 1417 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1418 if (R46 / R46stoch - 1) > 5e-8: 1419 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1420 1421 # Compute raw clumped isotope anomalies 1422 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1423 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1424 r['D49raw'] = 1000 * (R49 / R49stoch - 1) 1425 1426 1427 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1428 ''' 1429 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1430 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1431 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1432 ''' 1433 1434 # Compute R17 1435 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1436 1437 # Compute isotope concentrations 1438 C12 = (1 + R13) ** -1 1439 C13 = C12 * R13 1440 C16 = (1 + R17 + R18) ** -1 1441 C17 = C16 * R17 1442 C18 = C16 * R18 1443 1444 # Compute stochastic isotopologue concentrations 1445 C626 = C16 * C12 * C16 1446 C627 = C16 * C12 * C17 * 2 1447 C628 = C16 * C12 * C18 * 2 1448 C636 = C16 * C13 * C16 1449 C637 = C16 * C13 * C17 * 2 1450 C638 = C16 * C13 * C18 * 2 1451 C727 = C17 * C12 * C17 1452 C728 = C17 * C12 * C18 * 2 1453 C737 = C17 * C13 * C17 1454 C738 = C17 * C13 * C18 * 2 1455 C828 = C18 * C12 * C18 1456 C838 = C18 * C13 * C18 1457 1458 # Compute stochastic isobar ratios 1459 R45 = (C636 + C627) / C626 1460 R46 = (C628 + C637 + C727) / C626 1461 R47 = (C638 + C728 + C737) / C626 1462 R48 = (C738 + C828) / C626 1463 R49 = C838 / C626 1464 1465 # Account for stochastic anomalies 1466 R47 *= 1 + D47 / 1000 1467 R48 *= 1 + D48 / 1000 1468 R49 *= 1 + D49 / 1000 1469 1470 # Return isobar ratios 1471 return R45, R46, R47, R48, R49 1472 1473 1474 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1475 ''' 1476 Split unknown samples by UID (treat all analyses as different samples) 1477 or by session (treat analyses of a given sample in different sessions as 1478 different samples). 1479 1480 **Parameters** 1481 1482 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1483 + `grouping`: `by_uid` | `by_session` 1484 ''' 1485 if samples_to_split == 'all': 1486 samples_to_split = [s for s in self.unknowns] 1487 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1488 self.grouping = grouping.lower() 1489 if self.grouping in gkeys: 1490 gkey = gkeys[self.grouping] 1491 for r in self: 1492 if r['Sample'] in samples_to_split: 1493 r['Sample_original'] = r['Sample'] 1494 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1495 elif r['Sample'] in self.unknowns: 1496 r['Sample_original'] = r['Sample'] 1497 self.refresh_samples() 1498 1499 1500 def unsplit_samples(self, tables = False): 1501 ''' 1502 Reverse the effects of `D47data.split_samples()`. 1503 1504 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1505 1506 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1507 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1508 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1509 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1510 that case session-averaged Δ4x values are statistically independent). 1511 ''' 1512 unknowns_old = sorted({s for s in self.unknowns}) 1513 CM_old = self.standardization.covar[:,:] 1514 VD_old = self.standardization.params.valuesdict().copy() 1515 vars_old = self.standardization.var_names 1516 1517 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1518 1519 Ns = len(vars_old) - len(unknowns_old) 1520 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1521 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1522 1523 W = np.zeros((len(vars_new), len(vars_old))) 1524 W[:Ns,:Ns] = np.eye(Ns) 1525 for u in unknowns_new: 1526 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1527 if self.grouping == 'by_session': 1528 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1529 elif self.grouping == 'by_uid': 1530 weights = [1 for s in splits] 1531 sw = sum(weights) 1532 weights = [w/sw for w in weights] 1533 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1534 1535 CM_new = W @ CM_old @ W.T 1536 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1537 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1538 1539 self.standardization.covar = CM_new 1540 self.standardization.params.valuesdict = lambda : VD_new 1541 self.standardization.var_names = vars_new 1542 1543 for r in self: 1544 if r['Sample'] in self.unknowns: 1545 r['Sample_split'] = r['Sample'] 1546 r['Sample'] = r['Sample_original'] 1547 1548 self.refresh_samples() 1549 self.consolidate_samples() 1550 self.repeatabilities() 1551 1552 if tables: 1553 self.table_of_analyses() 1554 self.table_of_samples() 1555 1556 def assign_timestamps(self): 1557 ''' 1558 Assign a time field `t` of type `float` to each analysis. 1559 1560 If `TimeTag` is one of the data fields, `t` is equal within a given session 1561 to `TimeTag` minus the mean value of `TimeTag` for that session. 1562 Otherwise, `TimeTag` is by default equal to the index of each analysis 1563 in the dataset and `t` is defined as above. 1564 ''' 1565 for session in self.sessions: 1566 sdata = self.sessions[session]['data'] 1567 try: 1568 t0 = np.mean([r['TimeTag'] for r in sdata]) 1569 for r in sdata: 1570 r['t'] = r['TimeTag'] - t0 1571 except KeyError: 1572 t0 = (len(sdata)-1)/2 1573 for t,r in enumerate(sdata): 1574 r['t'] = t - t0 1575 1576 1577 def report(self): 1578 ''' 1579 Prints a report on the standardization fit. 1580 Only applicable after `D4xdata.standardize(method='pooled')`. 1581 ''' 1582 report_fit(self.standardization) 1583 1584 1585 def combine_samples(self, sample_groups): 1586 ''' 1587 Combine analyses of different samples to compute weighted average Δ4x 1588 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1589 dictionary. 1590 1591 Caution: samples are weighted by number of replicate analyses, which is a 1592 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1593 correlated analytical errors for one or more samples). 1594 1595 Returns a tuplet of: 1596 1597 + the list of group names 1598 + an array of the corresponding Δ4x values 1599 + the corresponding (co)variance matrix 1600 1601 **Parameters** 1602 1603 + `sample_groups`: a dictionary of the form: 1604 ```py 1605 {'group1': ['sample_1', 'sample_2'], 1606 'group2': ['sample_3', 'sample_4', 'sample_5']} 1607 ``` 1608 ''' 1609 1610 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1611 groups = sorted(sample_groups.keys()) 1612 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1613 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1614 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1615 W = np.array([ 1616 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1617 for j in groups]) 1618 D4x_new = W @ D4x_old 1619 CM_new = W @ CM_old @ W.T 1620 1621 return groups, D4x_new[:,0], CM_new 1622 1623 1624 @make_verbal 1625 def standardize(self, 1626 method = 'pooled', 1627 weighted_sessions = [], 1628 consolidate = True, 1629 consolidate_tables = False, 1630 consolidate_plots = False, 1631 constraints = {}, 1632 ): 1633 ''' 1634 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1635 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1636 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1637 i.e. that their true Δ4x value does not change between sessions, 1638 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1639 `'indep_sessions'`, the standardization processes each session independently, based only 1640 on anchors analyses. 1641 ''' 1642 1643 self.standardization_method = method 1644 self.assign_timestamps() 1645 1646 if method == 'pooled': 1647 if weighted_sessions: 1648 for session_group in weighted_sessions: 1649 if self._4x == '47': 1650 X = D47data([r for r in self if r['Session'] in session_group]) 1651 elif self._4x == '48': 1652 X = D48data([r for r in self if r['Session'] in session_group]) 1653 X.Nominal_D4x = self.Nominal_D4x.copy() 1654 X.refresh() 1655 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1656 w = np.sqrt(result.redchi) 1657 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1658 for r in X: 1659 r[f'wD{self._4x}raw'] *= w 1660 else: 1661 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1662 for r in self: 1663 r[f'wD{self._4x}raw'] = 1. 1664 1665 params = Parameters() 1666 for k,session in enumerate(self.sessions): 1667 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1668 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1669 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1670 s = pf(session) 1671 params.add(f'a_{s}', value = 0.9) 1672 params.add(f'b_{s}', value = 0.) 1673 params.add(f'c_{s}', value = -0.9) 1674 params.add(f'a2_{s}', value = 0., 1675# vary = self.sessions[session]['scrambling_drift'], 1676 ) 1677 params.add(f'b2_{s}', value = 0., 1678# vary = self.sessions[session]['slope_drift'], 1679 ) 1680 params.add(f'c2_{s}', value = 0., 1681# vary = self.sessions[session]['wg_drift'], 1682 ) 1683 if not self.sessions[session]['scrambling_drift']: 1684 params[f'a2_{s}'].expr = '0' 1685 if not self.sessions[session]['slope_drift']: 1686 params[f'b2_{s}'].expr = '0' 1687 if not self.sessions[session]['wg_drift']: 1688 params[f'c2_{s}'].expr = '0' 1689 1690 for sample in self.unknowns: 1691 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1692 1693 for k in constraints: 1694 params[k].expr = constraints[k] 1695 1696 def residuals(p): 1697 R = [] 1698 for r in self: 1699 session = pf(r['Session']) 1700 sample = pf(r['Sample']) 1701 if r['Sample'] in self.Nominal_D4x: 1702 R += [ ( 1703 r[f'D{self._4x}raw'] - ( 1704 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1705 + p[f'b_{session}'] * r[f'd{self._4x}'] 1706 + p[f'c_{session}'] 1707 + r['t'] * ( 1708 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1709 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1710 + p[f'c2_{session}'] 1711 ) 1712 ) 1713 ) / r[f'wD{self._4x}raw'] ] 1714 else: 1715 R += [ ( 1716 r[f'D{self._4x}raw'] - ( 1717 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1718 + p[f'b_{session}'] * r[f'd{self._4x}'] 1719 + p[f'c_{session}'] 1720 + r['t'] * ( 1721 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1722 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1723 + p[f'c2_{session}'] 1724 ) 1725 ) 1726 ) / r[f'wD{self._4x}raw'] ] 1727 return R 1728 1729 M = Minimizer(residuals, params) 1730 result = M.least_squares() 1731 self.Nf = result.nfree 1732 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1733 new_names, new_covar, new_se = _fullcovar(result)[:3] 1734 result.var_names = new_names 1735 result.covar = new_covar 1736 1737 for r in self: 1738 s = pf(r["Session"]) 1739 a = result.params.valuesdict()[f'a_{s}'] 1740 b = result.params.valuesdict()[f'b_{s}'] 1741 c = result.params.valuesdict()[f'c_{s}'] 1742 a2 = result.params.valuesdict()[f'a2_{s}'] 1743 b2 = result.params.valuesdict()[f'b2_{s}'] 1744 c2 = result.params.valuesdict()[f'c2_{s}'] 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 1747 self.standardization = result 1748 1749 for session in self.sessions: 1750 self.sessions[session]['Np'] = 3 1751 for k in ['scrambling', 'slope', 'wg']: 1752 if self.sessions[session][f'{k}_drift']: 1753 self.sessions[session]['Np'] += 1 1754 1755 if consolidate: 1756 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1757 return result 1758 1759 1760 elif method == 'indep_sessions': 1761 1762 if weighted_sessions: 1763 for session_group in weighted_sessions: 1764 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1765 X.Nominal_D4x = self.Nominal_D4x.copy() 1766 X.refresh() 1767 # This is only done to assign r['wD47raw'] for r in X: 1768 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1769 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}') 1770 else: 1771 self.msg('All weights set to 1 ‰') 1772 for r in self: 1773 r[f'wD{self._4x}raw'] = 1 1774 1775 for session in self.sessions: 1776 s = self.sessions[session] 1777 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1778 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1779 s['Np'] = sum(p_active) 1780 sdata = s['data'] 1781 1782 A = np.array([ 1783 [ 1784 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1785 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1786 1 / r[f'wD{self._4x}raw'], 1787 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1788 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1789 r['t'] / r[f'wD{self._4x}raw'] 1790 ] 1791 for r in sdata if r['Sample'] in self.anchors 1792 ])[:,p_active] # only keep columns for the active parameters 1793 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]) 1794 s['Na'] = Y.size 1795 CM = linalg.inv(A.T @ A) 1796 bf = (CM @ A.T @ Y).T[0,:] 1797 k = 0 1798 for n,a in zip(p_names, p_active): 1799 if a: 1800 s[n] = bf[k] 1801# self.msg(f'{n} = {bf[k]}') 1802 k += 1 1803 else: 1804 s[n] = 0. 1805# self.msg(f'{n} = 0.0') 1806 1807 for r in sdata : 1808 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1809 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']) 1810 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1811 1812 s['CM'] = np.zeros((6,6)) 1813 i = 0 1814 k_active = [j for j,a in enumerate(p_active) if a] 1815 for j,a in enumerate(p_active): 1816 if a: 1817 s['CM'][j,k_active] = CM[i,:] 1818 i += 1 1819 1820 if not weighted_sessions: 1821 w = self.rmswd()['rmswd'] 1822 for r in self: 1823 r[f'wD{self._4x}'] *= w 1824 r[f'wD{self._4x}raw'] *= w 1825 for session in self.sessions: 1826 self.sessions[session]['CM'] *= w**2 1827 1828 for session in self.sessions: 1829 s = self.sessions[session] 1830 s['SE_a'] = s['CM'][0,0]**.5 1831 s['SE_b'] = s['CM'][1,1]**.5 1832 s['SE_c'] = s['CM'][2,2]**.5 1833 s['SE_a2'] = s['CM'][3,3]**.5 1834 s['SE_b2'] = s['CM'][4,4]**.5 1835 s['SE_c2'] = s['CM'][5,5]**.5 1836 1837 if not weighted_sessions: 1838 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1839 else: 1840 self.Nf = 0 1841 for sg in weighted_sessions: 1842 self.Nf += self.rmswd(sessions = sg)['Nf'] 1843 1844 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1845 1846 avgD4x = { 1847 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1848 for sample in self.samples 1849 } 1850 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1851 rD4x = (chi2/self.Nf)**.5 1852 self.repeatability[f'sigma_{self._4x}'] = rD4x 1853 1854 if consolidate: 1855 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1856 1857 1858 def standardization_error(self, session, d4x, D4x, t = 0): 1859 ''' 1860 Compute standardization error for a given session and 1861 (δ47, Δ47) composition. 1862 ''' 1863 a = self.sessions[session]['a'] 1864 b = self.sessions[session]['b'] 1865 c = self.sessions[session]['c'] 1866 a2 = self.sessions[session]['a2'] 1867 b2 = self.sessions[session]['b2'] 1868 c2 = self.sessions[session]['c2'] 1869 CM = self.sessions[session]['CM'] 1870 1871 x, y = D4x, d4x 1872 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1873# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1874 dxdy = -(b+b2*t) / (a+a2*t) 1875 dxdz = 1. / (a+a2*t) 1876 dxda = -x / (a+a2*t) 1877 dxdb = -y / (a+a2*t) 1878 dxdc = -1. / (a+a2*t) 1879 dxda2 = -x * a2 / (a+a2*t) 1880 dxdb2 = -y * t / (a+a2*t) 1881 dxdc2 = -t / (a+a2*t) 1882 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1883 sx = (V @ CM @ V.T) ** .5 1884 return sx 1885 1886 1887 @make_verbal 1888 def summary(self, 1889 dir = 'output', 1890 filename = None, 1891 save_to_file = True, 1892 print_out = True, 1893 ): 1894 ''' 1895 Print out an/or save to disk a summary of the standardization results. 1896 1897 **Parameters** 1898 1899 + `dir`: the directory in which to save the table 1900 + `filename`: the name to the csv file to write to 1901 + `save_to_file`: whether to save the table to disk 1902 + `print_out`: whether to print out the table 1903 ''' 1904 1905 out = [] 1906 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1907 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])})"]] 1908 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1909 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1910 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1911 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1912 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1913 out += [['Model degrees of freedom', f"{self.Nf}"]] 1914 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1915 out += [['Standardization method', self.standardization_method]] 1916 1917 if save_to_file: 1918 if not os.path.exists(dir): 1919 os.makedirs(dir) 1920 if filename is None: 1921 filename = f'D{self._4x}_summary.csv' 1922 with open(f'{dir}/{filename}', 'w') as fid: 1923 fid.write(make_csv(out)) 1924 if print_out: 1925 self.msg('\n' + pretty_table(out, header = 0)) 1926 1927 1928 @make_verbal 1929 def table_of_sessions(self, 1930 dir = 'output', 1931 filename = None, 1932 save_to_file = True, 1933 print_out = True, 1934 output = None, 1935 ): 1936 ''' 1937 Print out an/or save to disk a table of sessions. 1938 1939 **Parameters** 1940 1941 + `dir`: the directory in which to save the table 1942 + `filename`: the name to the csv file to write to 1943 + `save_to_file`: whether to save the table to disk 1944 + `print_out`: whether to print out the table 1945 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1946 if set to `'raw'`: return a list of list of strings 1947 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1948 ''' 1949 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1950 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1951 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1952 1953 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']] 1954 if include_a2: 1955 out[-1] += ['a2 ± SE'] 1956 if include_b2: 1957 out[-1] += ['b2 ± SE'] 1958 if include_c2: 1959 out[-1] += ['c2 ± SE'] 1960 for session in self.sessions: 1961 out += [[ 1962 session, 1963 f"{self.sessions[session]['Na']}", 1964 f"{self.sessions[session]['Nu']}", 1965 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1966 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1967 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1968 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1969 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1970 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1971 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1972 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1973 ]] 1974 if include_a2: 1975 if self.sessions[session]['scrambling_drift']: 1976 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1977 else: 1978 out[-1] += [''] 1979 if include_b2: 1980 if self.sessions[session]['slope_drift']: 1981 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1982 else: 1983 out[-1] += [''] 1984 if include_c2: 1985 if self.sessions[session]['wg_drift']: 1986 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1987 else: 1988 out[-1] += [''] 1989 1990 if save_to_file: 1991 if not os.path.exists(dir): 1992 os.makedirs(dir) 1993 if filename is None: 1994 filename = f'D{self._4x}_sessions.csv' 1995 with open(f'{dir}/{filename}', 'w') as fid: 1996 fid.write(make_csv(out)) 1997 if print_out: 1998 self.msg('\n' + pretty_table(out)) 1999 if output == 'raw': 2000 return out 2001 elif output == 'pretty': 2002 return pretty_table(out) 2003 2004 2005 @make_verbal 2006 def table_of_analyses( 2007 self, 2008 dir = 'output', 2009 filename = None, 2010 save_to_file = True, 2011 print_out = True, 2012 output = None, 2013 ): 2014 ''' 2015 Print out an/or save to disk a table of analyses. 2016 2017 **Parameters** 2018 2019 + `dir`: the directory in which to save the table 2020 + `filename`: the name to the csv file to write to 2021 + `save_to_file`: whether to save the table to disk 2022 + `print_out`: whether to print out the table 2023 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2024 if set to `'raw'`: return a list of list of strings 2025 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2026 ''' 2027 2028 out = [['UID','Session','Sample']] 2029 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}] 2030 for f in extra_fields: 2031 out[-1] += [f[0]] 2032 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 2033 for r in self: 2034 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 2035 for f in extra_fields: 2036 out[-1] += [f"{r[f[0]]:{f[1]}}"] 2037 out[-1] += [ 2038 f"{r['d13Cwg_VPDB']:.3f}", 2039 f"{r['d18Owg_VSMOW']:.3f}", 2040 f"{r['d45']:.6f}", 2041 f"{r['d46']:.6f}", 2042 f"{r['d47']:.6f}", 2043 f"{r['d48']:.6f}", 2044 f"{r['d49']:.6f}", 2045 f"{r['d13C_VPDB']:.6f}", 2046 f"{r['d18O_VSMOW']:.6f}", 2047 f"{r['D47raw']:.6f}", 2048 f"{r['D48raw']:.6f}", 2049 f"{r['D49raw']:.6f}", 2050 f"{r[f'D{self._4x}']:.6f}" 2051 ] 2052 if save_to_file: 2053 if not os.path.exists(dir): 2054 os.makedirs(dir) 2055 if filename is None: 2056 filename = f'D{self._4x}_analyses.csv' 2057 with open(f'{dir}/{filename}', 'w') as fid: 2058 fid.write(make_csv(out)) 2059 if print_out: 2060 self.msg('\n' + pretty_table(out)) 2061 return out 2062 2063 @make_verbal 2064 def covar_table( 2065 self, 2066 correl = False, 2067 dir = 'output', 2068 filename = None, 2069 save_to_file = True, 2070 print_out = True, 2071 output = None, 2072 ): 2073 ''' 2074 Print out, save to disk and/or return the variance-covariance matrix of D4x 2075 for all unknown samples. 2076 2077 **Parameters** 2078 2079 + `dir`: the directory in which to save the csv 2080 + `filename`: the name of the csv file to write to 2081 + `save_to_file`: whether to save the csv 2082 + `print_out`: whether to print out the matrix 2083 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2084 if set to `'raw'`: return a list of list of strings 2085 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2086 ''' 2087 samples = sorted([u for u in self.unknowns]) 2088 out = [[''] + samples] 2089 for s1 in samples: 2090 out.append([s1]) 2091 for s2 in samples: 2092 if correl: 2093 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2094 else: 2095 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2096 2097 if save_to_file: 2098 if not os.path.exists(dir): 2099 os.makedirs(dir) 2100 if filename is None: 2101 if correl: 2102 filename = f'D{self._4x}_correl.csv' 2103 else: 2104 filename = f'D{self._4x}_covar.csv' 2105 with open(f'{dir}/{filename}', 'w') as fid: 2106 fid.write(make_csv(out)) 2107 if print_out: 2108 self.msg('\n'+pretty_table(out)) 2109 if output == 'raw': 2110 return out 2111 elif output == 'pretty': 2112 return pretty_table(out) 2113 2114 @make_verbal 2115 def table_of_samples( 2116 self, 2117 dir = 'output', 2118 filename = None, 2119 save_to_file = True, 2120 print_out = True, 2121 output = None, 2122 ): 2123 ''' 2124 Print out, save to disk and/or return a table of samples. 2125 2126 **Parameters** 2127 2128 + `dir`: the directory in which to save the csv 2129 + `filename`: the name of the csv file to write to 2130 + `save_to_file`: whether to save the csv 2131 + `print_out`: whether to print out the table 2132 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2133 if set to `'raw'`: return a list of list of strings 2134 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2135 ''' 2136 2137 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2138 for sample in self.anchors: 2139 out += [[ 2140 f"{sample}", 2141 f"{self.samples[sample]['N']}", 2142 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2143 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2144 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2145 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2146 ]] 2147 for sample in self.unknowns: 2148 out += [[ 2149 f"{sample}", 2150 f"{self.samples[sample]['N']}", 2151 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2152 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2153 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2154 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2155 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2156 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2157 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2158 ]] 2159 if save_to_file: 2160 if not os.path.exists(dir): 2161 os.makedirs(dir) 2162 if filename is None: 2163 filename = f'D{self._4x}_samples.csv' 2164 with open(f'{dir}/{filename}', 'w') as fid: 2165 fid.write(make_csv(out)) 2166 if print_out: 2167 self.msg('\n'+pretty_table(out)) 2168 if output == 'raw': 2169 return out 2170 elif output == 'pretty': 2171 return pretty_table(out) 2172 2173 2174 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2175 ''' 2176 Generate session plots and save them to disk. 2177 2178 **Parameters** 2179 2180 + `dir`: the directory in which to save the plots 2181 + `figsize`: the width and height (in inches) of each plot 2182 ''' 2183 if not os.path.exists(dir): 2184 os.makedirs(dir) 2185 2186 for session in self.sessions: 2187 sp = self.plot_single_session(session, xylimits = 'constant') 2188 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2189 ppl.close(sp.fig) 2190 2191 2192 @make_verbal 2193 def consolidate_samples(self): 2194 ''' 2195 Compile various statistics for each sample. 2196 2197 For each anchor sample: 2198 2199 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2200 + `SE_D47` or `SE_D48`: set to zero by definition 2201 2202 For each unknown sample: 2203 2204 + `D47` or `D48`: the standardized Δ4x value for this unknown 2205 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2206 2207 For each anchor and unknown: 2208 2209 + `N`: the total number of analyses of this sample 2210 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2211 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2212 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2213 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2214 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2215 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2216 ''' 2217 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2218 for sample in self.samples: 2219 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2220 if self.samples[sample]['N'] > 1: 2221 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2222 2223 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2224 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2225 2226 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2227 if len(D4x_pop) > 2: 2228 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2229 2230 if self.standardization_method == 'pooled': 2231 for sample in self.anchors: 2232 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2233 self.samples[sample][f'SE_D{self._4x}'] = 0. 2234 for sample in self.unknowns: 2235 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2236 try: 2237 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2238 except ValueError: 2239 # when `sample` is constrained by self.standardize(constraints = {...}), 2240 # it is no longer listed in self.standardization.var_names. 2241 # Temporary fix: define SE as zero for now 2242 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2243 2244 elif self.standardization_method == 'indep_sessions': 2245 for sample in self.anchors: 2246 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2247 self.samples[sample][f'SE_D{self._4x}'] = 0. 2248 for sample in self.unknowns: 2249 self.msg(f'Consolidating sample {sample}') 2250 self.unknowns[sample][f'session_D{self._4x}'] = {} 2251 session_avg = [] 2252 for session in self.sessions: 2253 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2254 if sdata: 2255 self.msg(f'{sample} found in session {session}') 2256 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2257 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2258 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2259 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2260 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2261 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2262 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2263 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2264 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2265 wsum = sum([weights[s] for s in weights]) 2266 for s in weights: 2267 self.unknowns[sample][f'session_D{self._4x}'][s] += [self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 / wsum] 2268 2269 2270 def consolidate_sessions(self): 2271 ''' 2272 Compute various statistics for each session. 2273 2274 + `Na`: Number of anchor analyses in the session 2275 + `Nu`: Number of unknown analyses in the session 2276 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2277 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2278 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2279 + `a`: scrambling factor 2280 + `b`: compositional slope 2281 + `c`: WG offset 2282 + `SE_a`: Model stadard erorr of `a` 2283 + `SE_b`: Model stadard erorr of `b` 2284 + `SE_c`: Model stadard erorr of `c` 2285 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2286 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2287 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2288 + `a2`: scrambling factor drift 2289 + `b2`: compositional slope drift 2290 + `c2`: WG offset drift 2291 + `Np`: Number of standardization parameters to fit 2292 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2293 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2294 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2295 ''' 2296 for session in self.sessions: 2297 if 'd13Cwg_VPDB' not in self.sessions[session]: 2298 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2299 if 'd18Owg_VSMOW' not in self.sessions[session]: 2300 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2301 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2302 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2303 2304 self.msg(f'Computing repeatabilities for session {session}') 2305 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2306 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2307 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2308 2309 if self.standardization_method == 'pooled': 2310 for session in self.sessions: 2311 2312 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2313 i = self.standardization.var_names.index(f'a_{pf(session)}') 2314 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2315 2316 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2317 i = self.standardization.var_names.index(f'b_{pf(session)}') 2318 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2319 2320 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2321 i = self.standardization.var_names.index(f'c_{pf(session)}') 2322 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2323 2324 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2325 if self.sessions[session]['scrambling_drift']: 2326 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2327 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2328 else: 2329 self.sessions[session]['SE_a2'] = 0. 2330 2331 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2332 if self.sessions[session]['slope_drift']: 2333 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2334 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2335 else: 2336 self.sessions[session]['SE_b2'] = 0. 2337 2338 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2339 if self.sessions[session]['wg_drift']: 2340 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2341 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2342 else: 2343 self.sessions[session]['SE_c2'] = 0. 2344 2345 i = self.standardization.var_names.index(f'a_{pf(session)}') 2346 j = self.standardization.var_names.index(f'b_{pf(session)}') 2347 k = self.standardization.var_names.index(f'c_{pf(session)}') 2348 CM = np.zeros((6,6)) 2349 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2350 try: 2351 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2352 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2353 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2354 try: 2355 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2356 CM[3,4] = self.standardization.covar[i2,j2] 2357 CM[4,3] = self.standardization.covar[j2,i2] 2358 except ValueError: 2359 pass 2360 try: 2361 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2362 CM[3,5] = self.standardization.covar[i2,k2] 2363 CM[5,3] = self.standardization.covar[k2,i2] 2364 except ValueError: 2365 pass 2366 except ValueError: 2367 pass 2368 try: 2369 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2370 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2371 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2372 try: 2373 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2374 CM[4,5] = self.standardization.covar[j2,k2] 2375 CM[5,4] = self.standardization.covar[k2,j2] 2376 except ValueError: 2377 pass 2378 except ValueError: 2379 pass 2380 try: 2381 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2382 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2383 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2384 except ValueError: 2385 pass 2386 2387 self.sessions[session]['CM'] = CM 2388 2389 elif self.standardization_method == 'indep_sessions': 2390 pass # Not implemented yet 2391 2392 2393 @make_verbal 2394 def repeatabilities(self): 2395 ''' 2396 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2397 (for all samples, for anchors, and for unknowns). 2398 ''' 2399 self.msg('Computing reproducibilities for all sessions') 2400 2401 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2402 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2403 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2404 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2405 self.repeatability[f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', samples = 'all samples') 2406 2407 2408 @make_verbal 2409 def consolidate(self, tables = True, plots = True): 2410 ''' 2411 Collect information about samples, sessions and repeatabilities. 2412 ''' 2413 self.consolidate_samples() 2414 self.consolidate_sessions() 2415 self.repeatabilities() 2416 2417 if tables: 2418 self.summary() 2419 self.table_of_sessions() 2420 self.table_of_analyses() 2421 self.table_of_samples() 2422 2423 if plots: 2424 self.plot_sessions() 2425 2426 2427 @make_verbal 2428 def rmswd(self, 2429 samples = 'all samples', 2430 sessions = 'all sessions', 2431 ): 2432 ''' 2433 Compute the χ2, root mean squared weighted deviation 2434 (i.e. reduced χ2), and corresponding degrees of freedom of the 2435 Δ4x values for samples in `samples` and sessions in `sessions`. 2436 2437 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2438 ''' 2439 if samples == 'all samples': 2440 mysamples = [k for k in self.samples] 2441 elif samples == 'anchors': 2442 mysamples = [k for k in self.anchors] 2443 elif samples == 'unknowns': 2444 mysamples = [k for k in self.unknowns] 2445 else: 2446 mysamples = samples 2447 2448 if sessions == 'all sessions': 2449 sessions = [k for k in self.sessions] 2450 2451 chisq, Nf = 0, 0 2452 for sample in mysamples : 2453 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2454 if len(G) > 1 : 2455 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2456 Nf += (len(G) - 1) 2457 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2458 r = (chisq / Nf)**.5 if Nf > 0 else 0 2459 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2460 return {'rmswd': r, 'chisq': chisq, 'Nf': Nf} 2461 2462 2463 @make_verbal 2464 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2465 ''' 2466 Compute the repeatability of `[r[key] for r in self]` 2467 ''' 2468 # NB: it's debatable whether rD47 should be computed 2469 # with Nf = len(self)-len(self.samples) instead of 2470 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2471 2472 if samples == 'all samples': 2473 mysamples = [k for k in self.samples] 2474 elif samples == 'anchors': 2475 mysamples = [k for k in self.anchors] 2476 elif samples == 'unknowns': 2477 mysamples = [k for k in self.unknowns] 2478 else: 2479 mysamples = samples 2480 2481 if sessions == 'all sessions': 2482 sessions = [k for k in self.sessions] 2483 2484 if key in ['D47', 'D48']: 2485 chisq, Nf = 0, 0 2486 for sample in mysamples : 2487 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2488 if len(X) > 1 : 2489 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2490 if sample in self.unknowns: 2491 Nf += len(X) - 1 2492 else: 2493 Nf += len(X) 2494 if samples in ['anchors', 'all samples']: 2495 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2496 r = (chisq / Nf)**.5 if Nf > 0 else 0 2497 2498 else: # if key not in ['D47', 'D48'] 2499 chisq, Nf = 0, 0 2500 for sample in mysamples : 2501 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2502 if len(X) > 1 : 2503 Nf += len(X) - 1 2504 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2505 r = (chisq / Nf)**.5 if Nf > 0 else 0 2506 2507 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2508 return r 2509 2510 def sample_average(self, samples, weights = 'equal', normalize = True): 2511 ''' 2512 Weighted average Δ4x value of a group of samples, accounting for covariance. 2513 2514 Returns the weighed average Δ4x value and associated SE 2515 of a group of samples. Weights are equal by default. If `normalize` is 2516 true, `weights` will be rescaled so that their sum equals 1. 2517 2518 **Examples** 2519 2520 ```python 2521 self.sample_average(['X','Y'], [1, 2]) 2522 ``` 2523 2524 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2525 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2526 values of samples X and Y, respectively. 2527 2528 ```python 2529 self.sample_average(['X','Y'], [1, -1], normalize = False) 2530 ``` 2531 2532 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2533 ''' 2534 if weights == 'equal': 2535 weights = [1/len(samples)] * len(samples) 2536 2537 if normalize: 2538 s = sum(weights) 2539 if s: 2540 weights = [w/s for w in weights] 2541 2542 try: 2543# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2544# C = self.standardization.covar[indices,:][:,indices] 2545 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2546 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2547 return correlated_sum(X, C, weights) 2548 except ValueError: 2549 return (0., 0.) 2550 2551 2552 def sample_D4x_covar(self, sample1, sample2 = None): 2553 ''' 2554 Covariance between Δ4x values of samples 2555 2556 Returns the error covariance between the average Δ4x values of two 2557 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2558 returns the Δ4x variance for that sample. 2559 ''' 2560 if sample2 is None: 2561 sample2 = sample1 2562 if self.standardization_method == 'pooled': 2563 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2564 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2565 return self.standardization.covar[i, j] 2566 elif self.standardization_method == 'indep_sessions': 2567 if sample1 == sample2: 2568 return self.samples[sample1][f'SE_D{self._4x}']**2 2569 else: 2570 c = 0 2571 for session in self.sessions: 2572 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2573 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2574 if sdata1 and sdata2: 2575 a = self.sessions[session]['a'] 2576 # !! TODO: CM below does not account for temporal changes in standardization parameters 2577 CM = self.sessions[session]['CM'][:3,:3] 2578 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2579 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2580 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2581 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2582 c += ( 2583 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2584 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2585 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2586 @ CM 2587 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2588 ) / a**2 2589 return float(c) 2590 2591 def sample_D4x_correl(self, sample1, sample2 = None): 2592 ''' 2593 Correlation between Δ4x errors of samples 2594 2595 Returns the error correlation between the average Δ4x values of two samples. 2596 ''' 2597 if sample2 is None or sample2 == sample1: 2598 return 1. 2599 return ( 2600 self.sample_D4x_covar(sample1, sample2) 2601 / self.unknowns[sample1][f'SE_D{self._4x}'] 2602 / self.unknowns[sample2][f'SE_D{self._4x}'] 2603 ) 2604 2605 def plot_single_session(self, 2606 session, 2607 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2608 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2609 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2610 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2611 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2612 xylimits = 'free', # | 'constant' 2613 x_label = None, 2614 y_label = None, 2615 error_contour_interval = 'auto', 2616 fig = 'new', 2617 ): 2618 ''' 2619 Generate plot for a single session 2620 ''' 2621 if x_label is None: 2622 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2623 if y_label is None: 2624 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2625 2626 out = _SessionPlot() 2627 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2628 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2629 2630 if fig == 'new': 2631 out.fig = ppl.figure(figsize = (6,6)) 2632 ppl.subplots_adjust(.1,.1,.9,.9) 2633 2634 out.anchor_analyses, = ppl.plot( 2635 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2636 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2637 **kw_plot_anchors) 2638 out.unknown_analyses, = ppl.plot( 2639 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2640 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2641 **kw_plot_unknowns) 2642 out.anchor_avg = ppl.plot( 2643 np.array([ np.array([ 2644 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2645 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2646 ]) for sample in anchors]).T, 2647 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2648 **kw_plot_anchor_avg) 2649 out.unknown_avg = ppl.plot( 2650 np.array([ np.array([ 2651 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2652 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2653 ]) for sample in unknowns]).T, 2654 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2655 **kw_plot_unknown_avg) 2656 if xylimits == 'constant': 2657 x = [r[f'd{self._4x}'] for r in self] 2658 y = [r[f'D{self._4x}'] for r in self] 2659 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2660 w, h = x2-x1, y2-y1 2661 x1 -= w/20 2662 x2 += w/20 2663 y1 -= h/20 2664 y2 += h/20 2665 ppl.axis([x1, x2, y1, y2]) 2666 elif xylimits == 'free': 2667 x1, x2, y1, y2 = ppl.axis() 2668 else: 2669 x1, x2, y1, y2 = ppl.axis(xylimits) 2670 2671 if error_contour_interval != 'none': 2672 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2673 XI,YI = np.meshgrid(xi, yi) 2674 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2675 if error_contour_interval == 'auto': 2676 rng = np.max(SI) - np.min(SI) 2677 if rng <= 0.01: 2678 cinterval = 0.001 2679 elif rng <= 0.03: 2680 cinterval = 0.004 2681 elif rng <= 0.1: 2682 cinterval = 0.01 2683 elif rng <= 0.3: 2684 cinterval = 0.03 2685 elif rng <= 1.: 2686 cinterval = 0.1 2687 else: 2688 cinterval = 0.5 2689 else: 2690 cinterval = error_contour_interval 2691 2692 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2693 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2694 out.clabel = ppl.clabel(out.contour) 2695 2696 ppl.xlabel(x_label) 2697 ppl.ylabel(y_label) 2698 ppl.title(session, weight = 'bold') 2699 ppl.grid(alpha = .2) 2700 out.ax = ppl.gca() 2701 2702 return out 2703 2704 def plot_residuals( 2705 self, 2706 hist = False, 2707 binwidth = 2/3, 2708 dir = 'output', 2709 filename = None, 2710 highlight = [], 2711 colors = None, 2712 figsize = None, 2713 ): 2714 ''' 2715 Plot residuals of each analysis as a function of time (actually, as a function of 2716 the order of analyses in the `D4xdata` object) 2717 2718 + `hist`: whether to add a histogram of residuals 2719 + `histbins`: specify bin edges for the histogram 2720 + `dir`: the directory in which to save the plot 2721 + `highlight`: a list of samples to highlight 2722 + `colors`: a dict of `{<sample>: <color>}` for all samples 2723 + `figsize`: (width, height) of figure 2724 ''' 2725 # Layout 2726 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2727 if hist: 2728 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2729 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2730 else: 2731 ppl.subplots_adjust(.08,.05,.78,.8) 2732 ax1 = ppl.subplot(111) 2733 2734 # Colors 2735 N = len(self.anchors) 2736 if colors is None: 2737 if len(highlight) > 0: 2738 Nh = len(highlight) 2739 if Nh == 1: 2740 colors = {highlight[0]: (0,0,0)} 2741 elif Nh == 3: 2742 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2743 elif Nh == 4: 2744 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2745 else: 2746 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2747 else: 2748 if N == 3: 2749 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2750 elif N == 4: 2751 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2752 else: 2753 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2754 2755 ppl.sca(ax1) 2756 2757 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2758 2759 session = self[0]['Session'] 2760 x1 = 0 2761# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2762 x_sessions = {} 2763 one_or_more_singlets = False 2764 one_or_more_multiplets = False 2765 multiplets = set() 2766 for k,r in enumerate(self): 2767 if r['Session'] != session: 2768 x2 = k-1 2769 x_sessions[session] = (x1+x2)/2 2770 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2771 session = r['Session'] 2772 x1 = k 2773 singlet = len(self.samples[r['Sample']]['data']) == 1 2774 if not singlet: 2775 multiplets.add(r['Sample']) 2776 if r['Sample'] in self.unknowns: 2777 if singlet: 2778 one_or_more_singlets = True 2779 else: 2780 one_or_more_multiplets = True 2781 kw = dict( 2782 marker = 'x' if singlet else '+', 2783 ms = 4 if singlet else 5, 2784 ls = 'None', 2785 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2786 mew = 1, 2787 alpha = 0.2 if singlet else 1, 2788 ) 2789 if highlight and r['Sample'] not in highlight: 2790 kw['alpha'] = 0.2 2791 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2792 x2 = k 2793 x_sessions[session] = (x1+x2)/2 2794 2795 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2796 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2797 if not hist: 2798 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2799 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') 2800 2801 xmin, xmax, ymin, ymax = ppl.axis() 2802 for s in x_sessions: 2803 ppl.text( 2804 x_sessions[s], 2805 ymax +1, 2806 s, 2807 va = 'bottom', 2808 **( 2809 dict(ha = 'center') 2810 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2811 else dict(ha = 'left', rotation = 45) 2812 ) 2813 ) 2814 2815 if hist: 2816 ppl.sca(ax2) 2817 2818 for s in colors: 2819 kw['marker'] = '+' 2820 kw['ms'] = 5 2821 kw['mec'] = colors[s] 2822 kw['label'] = s 2823 kw['alpha'] = 1 2824 ppl.plot([], [], **kw) 2825 2826 kw['mec'] = (0,0,0) 2827 2828 if one_or_more_singlets: 2829 kw['marker'] = 'x' 2830 kw['ms'] = 4 2831 kw['alpha'] = .2 2832 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2833 ppl.plot([], [], **kw) 2834 2835 if one_or_more_multiplets: 2836 kw['marker'] = '+' 2837 kw['ms'] = 4 2838 kw['alpha'] = 1 2839 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2840 ppl.plot([], [], **kw) 2841 2842 if hist: 2843 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2844 else: 2845 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2846 leg.set_zorder(-1000) 2847 2848 ppl.sca(ax1) 2849 2850 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2851 ppl.xticks([]) 2852 ppl.axis([-1, len(self), None, None]) 2853 2854 if hist: 2855 ppl.sca(ax2) 2856 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2857 ppl.hist( 2858 X, 2859 orientation = 'horizontal', 2860 histtype = 'stepfilled', 2861 ec = [.4]*3, 2862 fc = [.25]*3, 2863 alpha = .25, 2864 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2865 ) 2866 ppl.axis([None, None, ymin, ymax]) 2867 ppl.text(0, 0, 2868 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2869 size = 8, 2870 alpha = 1, 2871 va = 'center', 2872 ha = 'left', 2873 ) 2874 2875 ppl.xticks([]) 2876 ppl.yticks([]) 2877# ax2.spines['left'].set_visible(False) 2878 ax2.spines['right'].set_visible(False) 2879 ax2.spines['top'].set_visible(False) 2880 ax2.spines['bottom'].set_visible(False) 2881 2882 2883 if not os.path.exists(dir): 2884 os.makedirs(dir) 2885 if filename is None: 2886 return fig 2887 elif filename == '': 2888 filename = f'D{self._4x}_residuals.pdf' 2889 ppl.savefig(f'{dir}/{filename}') 2890 ppl.close(fig) 2891 2892 2893 def simulate(self, *args, **kwargs): 2894 ''' 2895 Legacy function with warning message pointing to `virtual_data()` 2896 ''' 2897 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()') 2898 2899 def plot_distribution_of_analyses( 2900 self, 2901 dir = 'output', 2902 filename = None, 2903 vs_time = False, 2904 figsize = (6,4), 2905 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2906 output = None, 2907 ): 2908 ''' 2909 Plot temporal distribution of all analyses in the data set. 2910 2911 **Parameters** 2912 2913 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2914 ''' 2915 2916 asamples = [s for s in self.anchors] 2917 usamples = [s for s in self.unknowns] 2918 if output is None or output == 'fig': 2919 fig = ppl.figure(figsize = figsize) 2920 ppl.subplots_adjust(*subplots_adjust) 2921 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2922 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2923 Xmax += (Xmax-Xmin)/40 2924 Xmin -= (Xmax-Xmin)/41 2925 for k, s in enumerate(asamples + usamples): 2926 if vs_time: 2927 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2928 else: 2929 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2930 Y = [-k for x in X] 2931 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2932 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2933 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2934 ppl.axis([Xmin, Xmax, -k-1, 1]) 2935 ppl.xlabel('\ntime') 2936 ppl.gca().annotate('', 2937 xy = (0.6, -0.02), 2938 xycoords = 'axes fraction', 2939 xytext = (.4, -0.02), 2940 arrowprops = dict(arrowstyle = "->", color = 'k'), 2941 ) 2942 2943 2944 x2 = -1 2945 for session in self.sessions: 2946 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2947 if vs_time: 2948 ppl.axvline(x1, color = 'k', lw = .75) 2949 if x2 > -1: 2950 if not vs_time: 2951 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2952 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2953# from xlrd import xldate_as_datetime 2954# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2955 if vs_time: 2956 ppl.axvline(x2, color = 'k', lw = .75) 2957 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2958 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2959 2960 ppl.xticks([]) 2961 ppl.yticks([]) 2962 2963 if output is None: 2964 if not os.path.exists(dir): 2965 os.makedirs(dir) 2966 if filename == None: 2967 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2968 ppl.savefig(f'{dir}/{filename}') 2969 ppl.close(fig) 2970 elif output == 'ax': 2971 return ppl.gca() 2972 elif output == 'fig': 2973 return fig 2974 2975 2976class D47data(D4xdata): 2977 ''' 2978 Store and process data for a large set of Δ47 analyses, 2979 usually comprising more than one analytical session. 2980 ''' 2981 2982 Nominal_D4x = { 2983 'ETH-1': 0.2052, 2984 'ETH-2': 0.2085, 2985 'ETH-3': 0.6132, 2986 'ETH-4': 0.4511, 2987 'IAEA-C1': 0.3018, 2988 'IAEA-C2': 0.6409, 2989 'MERCK': 0.5135, 2990 } # I-CDES (Bernasconi et al., 2021) 2991 ''' 2992 Nominal Δ47 values assigned to the Δ47 anchor samples, used by 2993 `D47data.standardize()` to normalize unknown samples to an absolute Δ47 2994 reference frame. 2995 2996 By default equal to (after [Bernasconi et al. (2021)](https://doi.org/10.1029/2020GC009588)): 2997 ```py 2998 { 2999 'ETH-1' : 0.2052, 3000 'ETH-2' : 0.2085, 3001 'ETH-3' : 0.6132, 3002 'ETH-4' : 0.4511, 3003 'IAEA-C1' : 0.3018, 3004 'IAEA-C2' : 0.6409, 3005 'MERCK' : 0.5135, 3006 } 3007 ``` 3008 ''' 3009 3010 3011 @property 3012 def Nominal_D47(self): 3013 return self.Nominal_D4x 3014 3015 3016 @Nominal_D47.setter 3017 def Nominal_D47(self, new): 3018 self.Nominal_D4x = dict(**new) 3019 self.refresh() 3020 3021 3022 def __init__(self, l = [], **kwargs): 3023 ''' 3024 **Parameters:** same as `D4xdata.__init__()` 3025 ''' 3026 D4xdata.__init__(self, l = l, mass = '47', **kwargs) 3027 3028 3029 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 3030 ''' 3031 Find all samples for which `Teq` is specified, compute equilibrium Δ47 3032 value for that temperature, and add treat these samples as additional anchors. 3033 3034 **Parameters** 3035 3036 + `fCo2eqD47`: Which CO2 equilibrium law to use 3037 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 3038 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 3039 + `priority`: if `replace`: forget old anchors and only use the new ones; 3040 if `new`: keep pre-existing anchors but update them in case of conflict 3041 between old and new Δ47 values; 3042 if `old`: keep pre-existing anchors but preserve their original Δ47 3043 values in case of conflict. 3044 ''' 3045 f = { 3046 'petersen': fCO2eqD47_Petersen, 3047 'wang': fCO2eqD47_Wang, 3048 }[fCo2eqD47] 3049 foo = {} 3050 for r in self: 3051 if 'Teq' in r: 3052 if r['Sample'] in foo: 3053 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 3054 else: 3055 foo[r['Sample']] = f(r['Teq']) 3056 else: 3057 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 3058 3059 if priority == 'replace': 3060 self.Nominal_D47 = {} 3061 for s in foo: 3062 if priority != 'old' or s not in self.Nominal_D47: 3063 self.Nominal_D47[s] = foo[s] 3064 3065 3066 3067 3068class D48data(D4xdata): 3069 ''' 3070 Store and process data for a large set of Δ48 analyses, 3071 usually comprising more than one analytical session. 3072 ''' 3073 3074 Nominal_D4x = { 3075 'ETH-1': 0.138, 3076 'ETH-2': 0.138, 3077 'ETH-3': 0.270, 3078 'ETH-4': 0.223, 3079 'GU-1': -0.419, 3080 } # (Fiebig et al., 2019, 2021) 3081 ''' 3082 Nominal Δ48 values assigned to the Δ48 anchor samples, used by 3083 `D48data.standardize()` to normalize unknown samples to an absolute Δ48 3084 reference frame. 3085 3086 By default equal to (after [Fiebig et al. (2019)](https://doi.org/10.1016/j.chemgeo.2019.05.019), 3087 Fiebig et al. (in press)): 3088 3089 ```py 3090 { 3091 'ETH-1' : 0.138, 3092 'ETH-2' : 0.138, 3093 'ETH-3' : 0.270, 3094 'ETH-4' : 0.223, 3095 'GU-1' : -0.419, 3096 } 3097 ``` 3098 ''' 3099 3100 3101 @property 3102 def Nominal_D48(self): 3103 return self.Nominal_D4x 3104 3105 3106 @Nominal_D48.setter 3107 def Nominal_D48(self, new): 3108 self.Nominal_D4x = dict(**new) 3109 self.refresh() 3110 3111 3112 def __init__(self, l = [], **kwargs): 3113 ''' 3114 **Parameters:** same as `D4xdata.__init__()` 3115 ''' 3116 D4xdata.__init__(self, l = l, mass = '48', **kwargs) 3117 3118 3119class _SessionPlot(): 3120 ''' 3121 Simple placeholder class 3122 ''' 3123 def __init__(self): 3124 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']]
)
900class D4xdata(list): 901 ''' 902 Store and process data for a large set of Δ47 and/or Δ48 903 analyses, usually comprising more than one analytical session. 904 ''' 905 906 ### 17O CORRECTION PARAMETERS 907 R13_VPDB = 0.01118 # (Chang & Li, 1990) 908 ''' 909 Absolute (13C/12C) ratio of VPDB. 910 By default equal to 0.01118 ([Chang & Li, 1990](http://www.cnki.com.cn/Article/CJFDTotal-JXTW199004006.htm)) 911 ''' 912 913 R18_VSMOW = 0.0020052 # (Baertschi, 1976) 914 ''' 915 Absolute (18O/16C) ratio of VSMOW. 916 By default equal to 0.0020052 ([Baertschi, 1976](https://doi.org/10.1016/0012-821X(76)90115-1)) 917 ''' 918 919 LAMBDA_17 = 0.528 # (Barkan & Luz, 2005) 920 ''' 921 Mass-dependent exponent for triple oxygen isotopes. 922 By default equal to 0.528 ([Barkan & Luz, 2005](https://doi.org/10.1002/rcm.2250)) 923 ''' 924 925 R17_VSMOW = 0.00038475 # (Assonov & Brenninkmeijer, 2003, rescaled to R13_VPDB) 926 ''' 927 Absolute (17O/16C) ratio of VSMOW. 928 By default equal to 0.00038475 929 ([Assonov & Brenninkmeijer, 2003](https://dx.doi.org/10.1002/rcm.1011), 930 rescaled to `R13_VPDB`) 931 ''' 932 933 R18_VPDB = R18_VSMOW * 1.03092 934 ''' 935 Absolute (18O/16C) ratio of VPDB. 936 By definition equal to `R18_VSMOW * 1.03092`. 937 ''' 938 939 R17_VPDB = R17_VSMOW * 1.03092 ** LAMBDA_17 940 ''' 941 Absolute (17O/16C) ratio of VPDB. 942 By definition equal to `R17_VSMOW * 1.03092 ** LAMBDA_17`. 943 ''' 944 945 LEVENE_REF_SAMPLE = 'ETH-3' 946 ''' 947 After the Δ4x standardization step, each sample is tested to 948 assess whether the Δ4x variance within all analyses for that 949 sample differs significantly from that observed for a given reference 950 sample (using [Levene's test](https://en.wikipedia.org/wiki/Levene%27s_test), 951 which yields a p-value corresponding to the null hypothesis that the 952 underlying variances are equal). 953 954 `LEVENE_REF_SAMPLE` (by default equal to `'ETH-3'`) specifies which 955 sample should be used as a reference for this test. 956 ''' 957 958 ALPHA_18O_ACID_REACTION = round(np.exp(3.59 / (90 + 273.15) - 1.79e-3), 6) # (Kim et al., 2007, calcite) 959 ''' 960 Specifies the 18O/16O fractionation factor generally applicable 961 to acid reactions in the dataset. Currently used by `D4xdata.wg()`, 962 `D4xdata.standardize_d13C`, and `D4xdata.standardize_d18O`. 963 964 By default equal to 1.008129 (calcite reacted at 90 °C, 965 [Kim et al., 2007](https://dx.doi.org/10.1016/j.chemgeo.2007.08.005)). 966 ''' 967 968 Nominal_d13C_VPDB = { 969 'ETH-1': 2.02, 970 'ETH-2': -10.17, 971 'ETH-3': 1.71, 972 } # (Bernasconi et al., 2018) 973 ''' 974 Nominal δ13C_VPDB values assigned to carbonate standards, used by 975 `D4xdata.standardize_d13C()`. 976 977 By default equal to `{'ETH-1': 2.02, 'ETH-2': -10.17, 'ETH-3': 1.71}` after 978 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 979 ''' 980 981 Nominal_d18O_VPDB = { 982 'ETH-1': -2.19, 983 'ETH-2': -18.69, 984 'ETH-3': -1.78, 985 } # (Bernasconi et al., 2018) 986 ''' 987 Nominal δ18O_VPDB values assigned to carbonate standards, used by 988 `D4xdata.standardize_d18O()`. 989 990 By default equal to `{'ETH-1': -2.19, 'ETH-2': -18.69, 'ETH-3': -1.78}` after 991 [Bernasconi et al. (2018)](https://doi.org/10.1029/2017GC007385). 992 ''' 993 994 d13C_STANDARDIZATION_METHOD = '2pt' 995 ''' 996 Method by which to standardize δ13C values: 997 998 + `none`: do not apply any δ13C standardization. 999 + `'1pt'`: within each session, offset all initial δ13C values so as to 1000 minimize the difference between final δ13C_VPDB values and 1001 `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` is defined). 1002 + `'2pt'`: within each session, apply a affine trasformation to all δ13C 1003 values so as to minimize the difference between final δ13C_VPDB 1004 values and `Nominal_d13C_VPDB` (averaged over all analyses for which `Nominal_d13C_VPDB` 1005 is defined). 1006 ''' 1007 1008 d18O_STANDARDIZATION_METHOD = '2pt' 1009 ''' 1010 Method by which to standardize δ18O values: 1011 1012 + `none`: do not apply any δ18O standardization. 1013 + `'1pt'`: within each session, offset all initial δ18O values so as to 1014 minimize the difference between final δ18O_VPDB values and 1015 `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` is defined). 1016 + `'2pt'`: within each session, apply a affine trasformation to all δ18O 1017 values so as to minimize the difference between final δ18O_VPDB 1018 values and `Nominal_d18O_VPDB` (averaged over all analyses for which `Nominal_d18O_VPDB` 1019 is defined). 1020 ''' 1021 1022 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 1023 ''' 1024 **Parameters** 1025 1026 + `l`: a list of dictionaries, with each dictionary including at least the keys 1027 `Sample`, `d45`, `d46`, and `d47` or `d48`. 1028 + `mass`: `'47'` or `'48'` 1029 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 1030 + `session`: define session name for analyses without a `Session` key 1031 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 1032 1033 Returns a `D4xdata` object derived from `list`. 1034 ''' 1035 self._4x = mass 1036 self.verbose = verbose 1037 self.prefix = 'D4xdata' 1038 self.logfile = logfile 1039 list.__init__(self, l) 1040 self.Nf = None 1041 self.repeatability = {} 1042 self.refresh(session = session) 1043 1044 1045 def make_verbal(oldfun): 1046 ''' 1047 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 1048 ''' 1049 @wraps(oldfun) 1050 def newfun(*args, verbose = '', **kwargs): 1051 myself = args[0] 1052 oldprefix = myself.prefix 1053 myself.prefix = oldfun.__name__ 1054 if verbose != '': 1055 oldverbose = myself.verbose 1056 myself.verbose = verbose 1057 out = oldfun(*args, **kwargs) 1058 myself.prefix = oldprefix 1059 if verbose != '': 1060 myself.verbose = oldverbose 1061 return out 1062 return newfun 1063 1064 1065 def msg(self, txt): 1066 ''' 1067 Log a message to `self.logfile`, and print it out if `verbose = True` 1068 ''' 1069 self.log(txt) 1070 if self.verbose: 1071 print(f'{f"[{self.prefix}]":<16} {txt}') 1072 1073 1074 def vmsg(self, txt): 1075 ''' 1076 Log a message to `self.logfile` and print it out 1077 ''' 1078 self.log(txt) 1079 print(txt) 1080 1081 1082 def log(self, *txts): 1083 ''' 1084 Log a message to `self.logfile` 1085 ''' 1086 if self.logfile: 1087 with open(self.logfile, 'a') as fid: 1088 for txt in txts: 1089 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}') 1090 1091 1092 def refresh(self, session = 'mySession'): 1093 ''' 1094 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1095 ''' 1096 self.fill_in_missing_info(session = session) 1097 self.refresh_sessions() 1098 self.refresh_samples() 1099 1100 1101 def refresh_sessions(self): 1102 ''' 1103 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1104 to `False` for all sessions. 1105 ''' 1106 self.sessions = { 1107 s: {'data': [r for r in self if r['Session'] == s]} 1108 for s in sorted({r['Session'] for r in self}) 1109 } 1110 for s in self.sessions: 1111 self.sessions[s]['scrambling_drift'] = False 1112 self.sessions[s]['slope_drift'] = False 1113 self.sessions[s]['wg_drift'] = False 1114 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1115 self.sessions[s]['d18O_standardization_method'] = self.d18O_STANDARDIZATION_METHOD 1116 1117 1118 def refresh_samples(self): 1119 ''' 1120 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1121 ''' 1122 self.samples = { 1123 s: {'data': [r for r in self if r['Sample'] == s]} 1124 for s in sorted({r['Sample'] for r in self}) 1125 } 1126 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1127 self.unknowns = {s: self.samples[s] for s in self.samples if s not in self.Nominal_D4x} 1128 1129 1130 def read(self, filename, sep = '', session = ''): 1131 ''' 1132 Read file in csv format to load data into a `D47data` object. 1133 1134 In the csv file, spaces before and after field separators (`','` by default) 1135 are optional. Each line corresponds to a single analysis. 1136 1137 The required fields are: 1138 1139 + `UID`: a unique identifier 1140 + `Session`: an identifier for the analytical session 1141 + `Sample`: a sample identifier 1142 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1143 1144 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1145 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1146 and `d49` are optional, and set to NaN by default. 1147 1148 **Parameters** 1149 1150 + `fileneme`: the path of the file to read 1151 + `sep`: csv separator delimiting the fields 1152 + `session`: set `Session` field to this string for all analyses 1153 ''' 1154 with open(filename) as fid: 1155 self.input(fid.read(), sep = sep, session = session) 1156 1157 1158 def input(self, txt, sep = '', session = ''): 1159 ''' 1160 Read `txt` string in csv format to load analysis data into a `D47data` object. 1161 1162 In the csv string, spaces before and after field separators (`','` by default) 1163 are optional. Each line corresponds to a single analysis. 1164 1165 The required fields are: 1166 1167 + `UID`: a unique identifier 1168 + `Session`: an identifier for the analytical session 1169 + `Sample`: a sample identifier 1170 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1171 1172 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1173 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1174 and `d49` are optional, and set to NaN by default. 1175 1176 **Parameters** 1177 1178 + `txt`: the csv string to read 1179 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1180 whichever appers most often in `txt`. 1181 + `session`: set `Session` field to this string for all analyses 1182 ''' 1183 if sep == '': 1184 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1185 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1186 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:]] 1187 1188 if session != '': 1189 for r in data: 1190 r['Session'] = session 1191 1192 self += data 1193 self.refresh() 1194 1195 1196 @make_verbal 1197 def wg(self, samples = None, a18_acid = None): 1198 ''' 1199 Compute bulk composition of the working gas for each session based on 1200 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1201 `self.Nominal_d18O_VPDB`. 1202 ''' 1203 1204 self.msg('Computing WG composition:') 1205 1206 if a18_acid is None: 1207 a18_acid = self.ALPHA_18O_ACID_REACTION 1208 if samples is None: 1209 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1210 1211 assert a18_acid, f'Acid fractionation factor should not be zero.' 1212 1213 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1214 R45R46_standards = {} 1215 for sample in samples: 1216 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1217 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1218 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1219 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1220 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1221 1222 C12_s = 1 / (1 + R13_s) 1223 C13_s = R13_s / (1 + R13_s) 1224 C16_s = 1 / (1 + R17_s + R18_s) 1225 C17_s = R17_s / (1 + R17_s + R18_s) 1226 C18_s = R18_s / (1 + R17_s + R18_s) 1227 1228 C626_s = C12_s * C16_s ** 2 1229 C627_s = 2 * C12_s * C16_s * C17_s 1230 C628_s = 2 * C12_s * C16_s * C18_s 1231 C636_s = C13_s * C16_s ** 2 1232 C637_s = 2 * C13_s * C16_s * C17_s 1233 C727_s = C12_s * C17_s ** 2 1234 1235 R45_s = (C627_s + C636_s) / C626_s 1236 R46_s = (C628_s + C637_s + C727_s) / C626_s 1237 R45R46_standards[sample] = (R45_s, R46_s) 1238 1239 for s in self.sessions: 1240 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1241 assert db, f'No sample from {samples} found in session "{s}".' 1242# dbsamples = sorted({r['Sample'] for r in db}) 1243 1244 X = [r['d45'] for r in db] 1245 Y = [R45R46_standards[r['Sample']][0] for r in db] 1246 x1, x2 = np.min(X), np.max(X) 1247 1248 if x1 < x2: 1249 wgcoord = x1/(x1-x2) 1250 else: 1251 wgcoord = 999 1252 1253 if wgcoord < -.5 or wgcoord > 1.5: 1254 # unreasonable to extrapolate to d45 = 0 1255 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1256 else : 1257 # d45 = 0 is reasonably well bracketed 1258 R45_wg = np.polyfit(X, Y, 1)[1] 1259 1260 X = [r['d46'] for r in db] 1261 Y = [R45R46_standards[r['Sample']][1] for r in db] 1262 x1, x2 = np.min(X), np.max(X) 1263 1264 if x1 < x2: 1265 wgcoord = x1/(x1-x2) 1266 else: 1267 wgcoord = 999 1268 1269 if wgcoord < -.5 or wgcoord > 1.5: 1270 # unreasonable to extrapolate to d46 = 0 1271 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1272 else : 1273 # d46 = 0 is reasonably well bracketed 1274 R46_wg = np.polyfit(X, Y, 1)[1] 1275 1276 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1277 1278 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1279 1280 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1281 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1282 for r in self.sessions[s]['data']: 1283 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1284 r['d18Owg_VSMOW'] = d18Owg_VSMOW 1285 1286 1287 def compute_bulk_delta(self, R45, R46, D17O = 0): 1288 ''' 1289 Compute δ13C_VPDB and δ18O_VSMOW, 1290 by solving the generalized form of equation (17) from 1291 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1292 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1293 solving the corresponding second-order Taylor polynomial. 1294 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1295 ''' 1296 1297 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1298 1299 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1300 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1301 C = 2 * self.R18_VSMOW 1302 D = -R46 1303 1304 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1305 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1306 cc = A + B + C + D 1307 1308 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1309 1310 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1311 R17 = K * R18 ** self.LAMBDA_17 1312 R13 = R45 - 2 * R17 1313 1314 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1315 1316 return d13C_VPDB, d18O_VSMOW 1317 1318 1319 @make_verbal 1320 def crunch(self, verbose = ''): 1321 ''' 1322 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1323 ''' 1324 for r in self: 1325 self.compute_bulk_and_clumping_deltas(r) 1326 self.standardize_d13C() 1327 self.standardize_d18O() 1328 self.msg(f"Crunched {len(self)} analyses.") 1329 1330 1331 def fill_in_missing_info(self, session = 'mySession'): 1332 ''' 1333 Fill in optional fields with default values 1334 ''' 1335 for i,r in enumerate(self): 1336 if 'D17O' not in r: 1337 r['D17O'] = 0. 1338 if 'UID' not in r: 1339 r['UID'] = f'{i+1}' 1340 if 'Session' not in r: 1341 r['Session'] = session 1342 for k in ['d47', 'd48', 'd49']: 1343 if k not in r: 1344 r[k] = np.nan 1345 1346 1347 def standardize_d13C(self): 1348 ''' 1349 Perform δ13C standadization within each session `s` according to 1350 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1351 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1352 may be redefined abitrarily at a later stage. 1353 ''' 1354 for s in self.sessions: 1355 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1356 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] 1357 X,Y = zip(*XY) 1358 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1359 offset = np.mean(Y) - np.mean(X) 1360 for r in self.sessions[s]['data']: 1361 r['d13C_VPDB'] += offset 1362 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1363 a,b = np.polyfit(X,Y,1) 1364 for r in self.sessions[s]['data']: 1365 r['d13C_VPDB'] = a * r['d13C_VPDB'] + b 1366 1367 def standardize_d18O(self): 1368 ''' 1369 Perform δ18O standadization within each session `s` according to 1370 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1371 which is defined by default by `D47data.refresh_sessions()`as equal to 1372 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1373 ''' 1374 for s in self.sessions: 1375 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1376 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] 1377 X,Y = zip(*XY) 1378 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1379 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1380 offset = np.mean(Y) - np.mean(X) 1381 for r in self.sessions[s]['data']: 1382 r['d18O_VSMOW'] += offset 1383 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1384 a,b = np.polyfit(X,Y,1) 1385 for r in self.sessions[s]['data']: 1386 r['d18O_VSMOW'] = a * r['d18O_VSMOW'] + b 1387 1388 1389 def compute_bulk_and_clumping_deltas(self, r): 1390 ''' 1391 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1392 ''' 1393 1394 # Compute working gas R13, R18, and isobar ratios 1395 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1396 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1397 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1398 1399 # Compute analyte isobar ratios 1400 R45 = (1 + r['d45'] / 1000) * R45_wg 1401 R46 = (1 + r['d46'] / 1000) * R46_wg 1402 R47 = (1 + r['d47'] / 1000) * R47_wg 1403 R48 = (1 + r['d48'] / 1000) * R48_wg 1404 R49 = (1 + r['d49'] / 1000) * R49_wg 1405 1406 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1407 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1408 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1409 1410 # Compute stochastic isobar ratios of the analyte 1411 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1412 R13, R18, D17O = r['D17O'] 1413 ) 1414 1415 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1416 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1417 if (R45 / R45stoch - 1) > 5e-8: 1418 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1419 if (R46 / R46stoch - 1) > 5e-8: 1420 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1421 1422 # Compute raw clumped isotope anomalies 1423 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1424 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1425 r['D49raw'] = 1000 * (R49 / R49stoch - 1) 1426 1427 1428 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1429 ''' 1430 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1431 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1432 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1433 ''' 1434 1435 # Compute R17 1436 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1437 1438 # Compute isotope concentrations 1439 C12 = (1 + R13) ** -1 1440 C13 = C12 * R13 1441 C16 = (1 + R17 + R18) ** -1 1442 C17 = C16 * R17 1443 C18 = C16 * R18 1444 1445 # Compute stochastic isotopologue concentrations 1446 C626 = C16 * C12 * C16 1447 C627 = C16 * C12 * C17 * 2 1448 C628 = C16 * C12 * C18 * 2 1449 C636 = C16 * C13 * C16 1450 C637 = C16 * C13 * C17 * 2 1451 C638 = C16 * C13 * C18 * 2 1452 C727 = C17 * C12 * C17 1453 C728 = C17 * C12 * C18 * 2 1454 C737 = C17 * C13 * C17 1455 C738 = C17 * C13 * C18 * 2 1456 C828 = C18 * C12 * C18 1457 C838 = C18 * C13 * C18 1458 1459 # Compute stochastic isobar ratios 1460 R45 = (C636 + C627) / C626 1461 R46 = (C628 + C637 + C727) / C626 1462 R47 = (C638 + C728 + C737) / C626 1463 R48 = (C738 + C828) / C626 1464 R49 = C838 / C626 1465 1466 # Account for stochastic anomalies 1467 R47 *= 1 + D47 / 1000 1468 R48 *= 1 + D48 / 1000 1469 R49 *= 1 + D49 / 1000 1470 1471 # Return isobar ratios 1472 return R45, R46, R47, R48, R49 1473 1474 1475 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1476 ''' 1477 Split unknown samples by UID (treat all analyses as different samples) 1478 or by session (treat analyses of a given sample in different sessions as 1479 different samples). 1480 1481 **Parameters** 1482 1483 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1484 + `grouping`: `by_uid` | `by_session` 1485 ''' 1486 if samples_to_split == 'all': 1487 samples_to_split = [s for s in self.unknowns] 1488 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1489 self.grouping = grouping.lower() 1490 if self.grouping in gkeys: 1491 gkey = gkeys[self.grouping] 1492 for r in self: 1493 if r['Sample'] in samples_to_split: 1494 r['Sample_original'] = r['Sample'] 1495 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1496 elif r['Sample'] in self.unknowns: 1497 r['Sample_original'] = r['Sample'] 1498 self.refresh_samples() 1499 1500 1501 def unsplit_samples(self, tables = False): 1502 ''' 1503 Reverse the effects of `D47data.split_samples()`. 1504 1505 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1506 1507 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1508 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1509 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1510 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1511 that case session-averaged Δ4x values are statistically independent). 1512 ''' 1513 unknowns_old = sorted({s for s in self.unknowns}) 1514 CM_old = self.standardization.covar[:,:] 1515 VD_old = self.standardization.params.valuesdict().copy() 1516 vars_old = self.standardization.var_names 1517 1518 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1519 1520 Ns = len(vars_old) - len(unknowns_old) 1521 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1522 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1523 1524 W = np.zeros((len(vars_new), len(vars_old))) 1525 W[:Ns,:Ns] = np.eye(Ns) 1526 for u in unknowns_new: 1527 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1528 if self.grouping == 'by_session': 1529 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1530 elif self.grouping == 'by_uid': 1531 weights = [1 for s in splits] 1532 sw = sum(weights) 1533 weights = [w/sw for w in weights] 1534 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1535 1536 CM_new = W @ CM_old @ W.T 1537 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1538 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1539 1540 self.standardization.covar = CM_new 1541 self.standardization.params.valuesdict = lambda : VD_new 1542 self.standardization.var_names = vars_new 1543 1544 for r in self: 1545 if r['Sample'] in self.unknowns: 1546 r['Sample_split'] = r['Sample'] 1547 r['Sample'] = r['Sample_original'] 1548 1549 self.refresh_samples() 1550 self.consolidate_samples() 1551 self.repeatabilities() 1552 1553 if tables: 1554 self.table_of_analyses() 1555 self.table_of_samples() 1556 1557 def assign_timestamps(self): 1558 ''' 1559 Assign a time field `t` of type `float` to each analysis. 1560 1561 If `TimeTag` is one of the data fields, `t` is equal within a given session 1562 to `TimeTag` minus the mean value of `TimeTag` for that session. 1563 Otherwise, `TimeTag` is by default equal to the index of each analysis 1564 in the dataset and `t` is defined as above. 1565 ''' 1566 for session in self.sessions: 1567 sdata = self.sessions[session]['data'] 1568 try: 1569 t0 = np.mean([r['TimeTag'] for r in sdata]) 1570 for r in sdata: 1571 r['t'] = r['TimeTag'] - t0 1572 except KeyError: 1573 t0 = (len(sdata)-1)/2 1574 for t,r in enumerate(sdata): 1575 r['t'] = t - t0 1576 1577 1578 def report(self): 1579 ''' 1580 Prints a report on the standardization fit. 1581 Only applicable after `D4xdata.standardize(method='pooled')`. 1582 ''' 1583 report_fit(self.standardization) 1584 1585 1586 def combine_samples(self, sample_groups): 1587 ''' 1588 Combine analyses of different samples to compute weighted average Δ4x 1589 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1590 dictionary. 1591 1592 Caution: samples are weighted by number of replicate analyses, which is a 1593 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1594 correlated analytical errors for one or more samples). 1595 1596 Returns a tuplet of: 1597 1598 + the list of group names 1599 + an array of the corresponding Δ4x values 1600 + the corresponding (co)variance matrix 1601 1602 **Parameters** 1603 1604 + `sample_groups`: a dictionary of the form: 1605 ```py 1606 {'group1': ['sample_1', 'sample_2'], 1607 'group2': ['sample_3', 'sample_4', 'sample_5']} 1608 ``` 1609 ''' 1610 1611 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1612 groups = sorted(sample_groups.keys()) 1613 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1614 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1615 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1616 W = np.array([ 1617 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1618 for j in groups]) 1619 D4x_new = W @ D4x_old 1620 CM_new = W @ CM_old @ W.T 1621 1622 return groups, D4x_new[:,0], CM_new 1623 1624 1625 @make_verbal 1626 def standardize(self, 1627 method = 'pooled', 1628 weighted_sessions = [], 1629 consolidate = True, 1630 consolidate_tables = False, 1631 consolidate_plots = False, 1632 constraints = {}, 1633 ): 1634 ''' 1635 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1636 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1637 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1638 i.e. that their true Δ4x value does not change between sessions, 1639 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1640 `'indep_sessions'`, the standardization processes each session independently, based only 1641 on anchors analyses. 1642 ''' 1643 1644 self.standardization_method = method 1645 self.assign_timestamps() 1646 1647 if method == 'pooled': 1648 if weighted_sessions: 1649 for session_group in weighted_sessions: 1650 if self._4x == '47': 1651 X = D47data([r for r in self if r['Session'] in session_group]) 1652 elif self._4x == '48': 1653 X = D48data([r for r in self if r['Session'] in session_group]) 1654 X.Nominal_D4x = self.Nominal_D4x.copy() 1655 X.refresh() 1656 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1657 w = np.sqrt(result.redchi) 1658 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1659 for r in X: 1660 r[f'wD{self._4x}raw'] *= w 1661 else: 1662 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1663 for r in self: 1664 r[f'wD{self._4x}raw'] = 1. 1665 1666 params = Parameters() 1667 for k,session in enumerate(self.sessions): 1668 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1669 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1670 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1671 s = pf(session) 1672 params.add(f'a_{s}', value = 0.9) 1673 params.add(f'b_{s}', value = 0.) 1674 params.add(f'c_{s}', value = -0.9) 1675 params.add(f'a2_{s}', value = 0., 1676# vary = self.sessions[session]['scrambling_drift'], 1677 ) 1678 params.add(f'b2_{s}', value = 0., 1679# vary = self.sessions[session]['slope_drift'], 1680 ) 1681 params.add(f'c2_{s}', value = 0., 1682# vary = self.sessions[session]['wg_drift'], 1683 ) 1684 if not self.sessions[session]['scrambling_drift']: 1685 params[f'a2_{s}'].expr = '0' 1686 if not self.sessions[session]['slope_drift']: 1687 params[f'b2_{s}'].expr = '0' 1688 if not self.sessions[session]['wg_drift']: 1689 params[f'c2_{s}'].expr = '0' 1690 1691 for sample in self.unknowns: 1692 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1693 1694 for k in constraints: 1695 params[k].expr = constraints[k] 1696 1697 def residuals(p): 1698 R = [] 1699 for r in self: 1700 session = pf(r['Session']) 1701 sample = pf(r['Sample']) 1702 if r['Sample'] in self.Nominal_D4x: 1703 R += [ ( 1704 r[f'D{self._4x}raw'] - ( 1705 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1706 + p[f'b_{session}'] * r[f'd{self._4x}'] 1707 + p[f'c_{session}'] 1708 + r['t'] * ( 1709 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1710 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1711 + p[f'c2_{session}'] 1712 ) 1713 ) 1714 ) / r[f'wD{self._4x}raw'] ] 1715 else: 1716 R += [ ( 1717 r[f'D{self._4x}raw'] - ( 1718 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1719 + p[f'b_{session}'] * r[f'd{self._4x}'] 1720 + p[f'c_{session}'] 1721 + r['t'] * ( 1722 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1723 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1724 + p[f'c2_{session}'] 1725 ) 1726 ) 1727 ) / r[f'wD{self._4x}raw'] ] 1728 return R 1729 1730 M = Minimizer(residuals, params) 1731 result = M.least_squares() 1732 self.Nf = result.nfree 1733 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1734 new_names, new_covar, new_se = _fullcovar(result)[:3] 1735 result.var_names = new_names 1736 result.covar = new_covar 1737 1738 for r in self: 1739 s = pf(r["Session"]) 1740 a = result.params.valuesdict()[f'a_{s}'] 1741 b = result.params.valuesdict()[f'b_{s}'] 1742 c = result.params.valuesdict()[f'c_{s}'] 1743 a2 = result.params.valuesdict()[f'a2_{s}'] 1744 b2 = result.params.valuesdict()[f'b2_{s}'] 1745 c2 = result.params.valuesdict()[f'c2_{s}'] 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 1748 self.standardization = result 1749 1750 for session in self.sessions: 1751 self.sessions[session]['Np'] = 3 1752 for k in ['scrambling', 'slope', 'wg']: 1753 if self.sessions[session][f'{k}_drift']: 1754 self.sessions[session]['Np'] += 1 1755 1756 if consolidate: 1757 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1758 return result 1759 1760 1761 elif method == 'indep_sessions': 1762 1763 if weighted_sessions: 1764 for session_group in weighted_sessions: 1765 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1766 X.Nominal_D4x = self.Nominal_D4x.copy() 1767 X.refresh() 1768 # This is only done to assign r['wD47raw'] for r in X: 1769 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1770 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}') 1771 else: 1772 self.msg('All weights set to 1 ‰') 1773 for r in self: 1774 r[f'wD{self._4x}raw'] = 1 1775 1776 for session in self.sessions: 1777 s = self.sessions[session] 1778 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1779 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1780 s['Np'] = sum(p_active) 1781 sdata = s['data'] 1782 1783 A = np.array([ 1784 [ 1785 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1786 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1787 1 / r[f'wD{self._4x}raw'], 1788 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1789 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1790 r['t'] / r[f'wD{self._4x}raw'] 1791 ] 1792 for r in sdata if r['Sample'] in self.anchors 1793 ])[:,p_active] # only keep columns for the active parameters 1794 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]) 1795 s['Na'] = Y.size 1796 CM = linalg.inv(A.T @ A) 1797 bf = (CM @ A.T @ Y).T[0,:] 1798 k = 0 1799 for n,a in zip(p_names, p_active): 1800 if a: 1801 s[n] = bf[k] 1802# self.msg(f'{n} = {bf[k]}') 1803 k += 1 1804 else: 1805 s[n] = 0. 1806# self.msg(f'{n} = 0.0') 1807 1808 for r in sdata : 1809 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1810 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']) 1811 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1812 1813 s['CM'] = np.zeros((6,6)) 1814 i = 0 1815 k_active = [j for j,a in enumerate(p_active) if a] 1816 for j,a in enumerate(p_active): 1817 if a: 1818 s['CM'][j,k_active] = CM[i,:] 1819 i += 1 1820 1821 if not weighted_sessions: 1822 w = self.rmswd()['rmswd'] 1823 for r in self: 1824 r[f'wD{self._4x}'] *= w 1825 r[f'wD{self._4x}raw'] *= w 1826 for session in self.sessions: 1827 self.sessions[session]['CM'] *= w**2 1828 1829 for session in self.sessions: 1830 s = self.sessions[session] 1831 s['SE_a'] = s['CM'][0,0]**.5 1832 s['SE_b'] = s['CM'][1,1]**.5 1833 s['SE_c'] = s['CM'][2,2]**.5 1834 s['SE_a2'] = s['CM'][3,3]**.5 1835 s['SE_b2'] = s['CM'][4,4]**.5 1836 s['SE_c2'] = s['CM'][5,5]**.5 1837 1838 if not weighted_sessions: 1839 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1840 else: 1841 self.Nf = 0 1842 for sg in weighted_sessions: 1843 self.Nf += self.rmswd(sessions = sg)['Nf'] 1844 1845 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1846 1847 avgD4x = { 1848 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1849 for sample in self.samples 1850 } 1851 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1852 rD4x = (chi2/self.Nf)**.5 1853 self.repeatability[f'sigma_{self._4x}'] = rD4x 1854 1855 if consolidate: 1856 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1857 1858 1859 def standardization_error(self, session, d4x, D4x, t = 0): 1860 ''' 1861 Compute standardization error for a given session and 1862 (δ47, Δ47) composition. 1863 ''' 1864 a = self.sessions[session]['a'] 1865 b = self.sessions[session]['b'] 1866 c = self.sessions[session]['c'] 1867 a2 = self.sessions[session]['a2'] 1868 b2 = self.sessions[session]['b2'] 1869 c2 = self.sessions[session]['c2'] 1870 CM = self.sessions[session]['CM'] 1871 1872 x, y = D4x, d4x 1873 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1874# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1875 dxdy = -(b+b2*t) / (a+a2*t) 1876 dxdz = 1. / (a+a2*t) 1877 dxda = -x / (a+a2*t) 1878 dxdb = -y / (a+a2*t) 1879 dxdc = -1. / (a+a2*t) 1880 dxda2 = -x * a2 / (a+a2*t) 1881 dxdb2 = -y * t / (a+a2*t) 1882 dxdc2 = -t / (a+a2*t) 1883 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1884 sx = (V @ CM @ V.T) ** .5 1885 return sx 1886 1887 1888 @make_verbal 1889 def summary(self, 1890 dir = 'output', 1891 filename = None, 1892 save_to_file = True, 1893 print_out = True, 1894 ): 1895 ''' 1896 Print out an/or save to disk a summary of the standardization results. 1897 1898 **Parameters** 1899 1900 + `dir`: the directory in which to save the table 1901 + `filename`: the name to the csv file to write to 1902 + `save_to_file`: whether to save the table to disk 1903 + `print_out`: whether to print out the table 1904 ''' 1905 1906 out = [] 1907 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1908 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])})"]] 1909 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1910 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1911 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1912 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1913 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1914 out += [['Model degrees of freedom', f"{self.Nf}"]] 1915 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1916 out += [['Standardization method', self.standardization_method]] 1917 1918 if save_to_file: 1919 if not os.path.exists(dir): 1920 os.makedirs(dir) 1921 if filename is None: 1922 filename = f'D{self._4x}_summary.csv' 1923 with open(f'{dir}/{filename}', 'w') as fid: 1924 fid.write(make_csv(out)) 1925 if print_out: 1926 self.msg('\n' + pretty_table(out, header = 0)) 1927 1928 1929 @make_verbal 1930 def table_of_sessions(self, 1931 dir = 'output', 1932 filename = None, 1933 save_to_file = True, 1934 print_out = True, 1935 output = None, 1936 ): 1937 ''' 1938 Print out an/or save to disk a table of sessions. 1939 1940 **Parameters** 1941 1942 + `dir`: the directory in which to save the table 1943 + `filename`: the name to the csv file to write to 1944 + `save_to_file`: whether to save the table to disk 1945 + `print_out`: whether to print out the table 1946 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1947 if set to `'raw'`: return a list of list of strings 1948 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1949 ''' 1950 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1951 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1952 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1953 1954 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']] 1955 if include_a2: 1956 out[-1] += ['a2 ± SE'] 1957 if include_b2: 1958 out[-1] += ['b2 ± SE'] 1959 if include_c2: 1960 out[-1] += ['c2 ± SE'] 1961 for session in self.sessions: 1962 out += [[ 1963 session, 1964 f"{self.sessions[session]['Na']}", 1965 f"{self.sessions[session]['Nu']}", 1966 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1967 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1968 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1969 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1970 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1971 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1972 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1973 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1974 ]] 1975 if include_a2: 1976 if self.sessions[session]['scrambling_drift']: 1977 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1978 else: 1979 out[-1] += [''] 1980 if include_b2: 1981 if self.sessions[session]['slope_drift']: 1982 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1983 else: 1984 out[-1] += [''] 1985 if include_c2: 1986 if self.sessions[session]['wg_drift']: 1987 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1988 else: 1989 out[-1] += [''] 1990 1991 if save_to_file: 1992 if not os.path.exists(dir): 1993 os.makedirs(dir) 1994 if filename is None: 1995 filename = f'D{self._4x}_sessions.csv' 1996 with open(f'{dir}/{filename}', 'w') as fid: 1997 fid.write(make_csv(out)) 1998 if print_out: 1999 self.msg('\n' + pretty_table(out)) 2000 if output == 'raw': 2001 return out 2002 elif output == 'pretty': 2003 return pretty_table(out) 2004 2005 2006 @make_verbal 2007 def table_of_analyses( 2008 self, 2009 dir = 'output', 2010 filename = None, 2011 save_to_file = True, 2012 print_out = True, 2013 output = None, 2014 ): 2015 ''' 2016 Print out an/or save to disk a table of analyses. 2017 2018 **Parameters** 2019 2020 + `dir`: the directory in which to save the table 2021 + `filename`: the name to the csv file to write to 2022 + `save_to_file`: whether to save the table to disk 2023 + `print_out`: whether to print out the table 2024 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2025 if set to `'raw'`: return a list of list of strings 2026 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2027 ''' 2028 2029 out = [['UID','Session','Sample']] 2030 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}] 2031 for f in extra_fields: 2032 out[-1] += [f[0]] 2033 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 2034 for r in self: 2035 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 2036 for f in extra_fields: 2037 out[-1] += [f"{r[f[0]]:{f[1]}}"] 2038 out[-1] += [ 2039 f"{r['d13Cwg_VPDB']:.3f}", 2040 f"{r['d18Owg_VSMOW']:.3f}", 2041 f"{r['d45']:.6f}", 2042 f"{r['d46']:.6f}", 2043 f"{r['d47']:.6f}", 2044 f"{r['d48']:.6f}", 2045 f"{r['d49']:.6f}", 2046 f"{r['d13C_VPDB']:.6f}", 2047 f"{r['d18O_VSMOW']:.6f}", 2048 f"{r['D47raw']:.6f}", 2049 f"{r['D48raw']:.6f}", 2050 f"{r['D49raw']:.6f}", 2051 f"{r[f'D{self._4x}']:.6f}" 2052 ] 2053 if save_to_file: 2054 if not os.path.exists(dir): 2055 os.makedirs(dir) 2056 if filename is None: 2057 filename = f'D{self._4x}_analyses.csv' 2058 with open(f'{dir}/{filename}', 'w') as fid: 2059 fid.write(make_csv(out)) 2060 if print_out: 2061 self.msg('\n' + pretty_table(out)) 2062 return out 2063 2064 @make_verbal 2065 def covar_table( 2066 self, 2067 correl = False, 2068 dir = 'output', 2069 filename = None, 2070 save_to_file = True, 2071 print_out = True, 2072 output = None, 2073 ): 2074 ''' 2075 Print out, save to disk and/or return the variance-covariance matrix of D4x 2076 for all unknown samples. 2077 2078 **Parameters** 2079 2080 + `dir`: the directory in which to save the csv 2081 + `filename`: the name of the csv file to write to 2082 + `save_to_file`: whether to save the csv 2083 + `print_out`: whether to print out the matrix 2084 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2085 if set to `'raw'`: return a list of list of strings 2086 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2087 ''' 2088 samples = sorted([u for u in self.unknowns]) 2089 out = [[''] + samples] 2090 for s1 in samples: 2091 out.append([s1]) 2092 for s2 in samples: 2093 if correl: 2094 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2095 else: 2096 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2097 2098 if save_to_file: 2099 if not os.path.exists(dir): 2100 os.makedirs(dir) 2101 if filename is None: 2102 if correl: 2103 filename = f'D{self._4x}_correl.csv' 2104 else: 2105 filename = f'D{self._4x}_covar.csv' 2106 with open(f'{dir}/{filename}', 'w') as fid: 2107 fid.write(make_csv(out)) 2108 if print_out: 2109 self.msg('\n'+pretty_table(out)) 2110 if output == 'raw': 2111 return out 2112 elif output == 'pretty': 2113 return pretty_table(out) 2114 2115 @make_verbal 2116 def table_of_samples( 2117 self, 2118 dir = 'output', 2119 filename = None, 2120 save_to_file = True, 2121 print_out = True, 2122 output = None, 2123 ): 2124 ''' 2125 Print out, save to disk and/or return a table of samples. 2126 2127 **Parameters** 2128 2129 + `dir`: the directory in which to save the csv 2130 + `filename`: the name of the csv file to write to 2131 + `save_to_file`: whether to save the csv 2132 + `print_out`: whether to print out the table 2133 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2134 if set to `'raw'`: return a list of list of strings 2135 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2136 ''' 2137 2138 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2139 for sample in self.anchors: 2140 out += [[ 2141 f"{sample}", 2142 f"{self.samples[sample]['N']}", 2143 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2144 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2145 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2146 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2147 ]] 2148 for sample in self.unknowns: 2149 out += [[ 2150 f"{sample}", 2151 f"{self.samples[sample]['N']}", 2152 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2153 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2154 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2155 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2156 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2157 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2158 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2159 ]] 2160 if save_to_file: 2161 if not os.path.exists(dir): 2162 os.makedirs(dir) 2163 if filename is None: 2164 filename = f'D{self._4x}_samples.csv' 2165 with open(f'{dir}/{filename}', 'w') as fid: 2166 fid.write(make_csv(out)) 2167 if print_out: 2168 self.msg('\n'+pretty_table(out)) 2169 if output == 'raw': 2170 return out 2171 elif output == 'pretty': 2172 return pretty_table(out) 2173 2174 2175 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2176 ''' 2177 Generate session plots and save them to disk. 2178 2179 **Parameters** 2180 2181 + `dir`: the directory in which to save the plots 2182 + `figsize`: the width and height (in inches) of each plot 2183 ''' 2184 if not os.path.exists(dir): 2185 os.makedirs(dir) 2186 2187 for session in self.sessions: 2188 sp = self.plot_single_session(session, xylimits = 'constant') 2189 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2190 ppl.close(sp.fig) 2191 2192 2193 @make_verbal 2194 def consolidate_samples(self): 2195 ''' 2196 Compile various statistics for each sample. 2197 2198 For each anchor sample: 2199 2200 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2201 + `SE_D47` or `SE_D48`: set to zero by definition 2202 2203 For each unknown sample: 2204 2205 + `D47` or `D48`: the standardized Δ4x value for this unknown 2206 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2207 2208 For each anchor and unknown: 2209 2210 + `N`: the total number of analyses of this sample 2211 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2212 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2213 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2214 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2215 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2216 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2217 ''' 2218 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2219 for sample in self.samples: 2220 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2221 if self.samples[sample]['N'] > 1: 2222 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2223 2224 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2225 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2226 2227 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2228 if len(D4x_pop) > 2: 2229 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2230 2231 if self.standardization_method == 'pooled': 2232 for sample in self.anchors: 2233 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2234 self.samples[sample][f'SE_D{self._4x}'] = 0. 2235 for sample in self.unknowns: 2236 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2237 try: 2238 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2239 except ValueError: 2240 # when `sample` is constrained by self.standardize(constraints = {...}), 2241 # it is no longer listed in self.standardization.var_names. 2242 # Temporary fix: define SE as zero for now 2243 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2244 2245 elif self.standardization_method == 'indep_sessions': 2246 for sample in self.anchors: 2247 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2248 self.samples[sample][f'SE_D{self._4x}'] = 0. 2249 for sample in self.unknowns: 2250 self.msg(f'Consolidating sample {sample}') 2251 self.unknowns[sample][f'session_D{self._4x}'] = {} 2252 session_avg = [] 2253 for session in self.sessions: 2254 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2255 if sdata: 2256 self.msg(f'{sample} found in session {session}') 2257 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2258 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2259 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2260 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2261 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2262 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2263 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2264 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2265 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2266 wsum = sum([weights[s] for s in weights]) 2267 for s in weights: 2268 self.unknowns[sample][f'session_D{self._4x}'][s] += [self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 / wsum] 2269 2270 2271 def consolidate_sessions(self): 2272 ''' 2273 Compute various statistics for each session. 2274 2275 + `Na`: Number of anchor analyses in the session 2276 + `Nu`: Number of unknown analyses in the session 2277 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2278 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2279 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2280 + `a`: scrambling factor 2281 + `b`: compositional slope 2282 + `c`: WG offset 2283 + `SE_a`: Model stadard erorr of `a` 2284 + `SE_b`: Model stadard erorr of `b` 2285 + `SE_c`: Model stadard erorr of `c` 2286 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2287 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2288 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2289 + `a2`: scrambling factor drift 2290 + `b2`: compositional slope drift 2291 + `c2`: WG offset drift 2292 + `Np`: Number of standardization parameters to fit 2293 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2294 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2295 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2296 ''' 2297 for session in self.sessions: 2298 if 'd13Cwg_VPDB' not in self.sessions[session]: 2299 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2300 if 'd18Owg_VSMOW' not in self.sessions[session]: 2301 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2302 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2303 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2304 2305 self.msg(f'Computing repeatabilities for session {session}') 2306 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2307 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2308 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2309 2310 if self.standardization_method == 'pooled': 2311 for session in self.sessions: 2312 2313 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2314 i = self.standardization.var_names.index(f'a_{pf(session)}') 2315 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2316 2317 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2318 i = self.standardization.var_names.index(f'b_{pf(session)}') 2319 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2320 2321 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2322 i = self.standardization.var_names.index(f'c_{pf(session)}') 2323 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2324 2325 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2326 if self.sessions[session]['scrambling_drift']: 2327 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2328 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2329 else: 2330 self.sessions[session]['SE_a2'] = 0. 2331 2332 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2333 if self.sessions[session]['slope_drift']: 2334 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2335 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2336 else: 2337 self.sessions[session]['SE_b2'] = 0. 2338 2339 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2340 if self.sessions[session]['wg_drift']: 2341 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2342 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2343 else: 2344 self.sessions[session]['SE_c2'] = 0. 2345 2346 i = self.standardization.var_names.index(f'a_{pf(session)}') 2347 j = self.standardization.var_names.index(f'b_{pf(session)}') 2348 k = self.standardization.var_names.index(f'c_{pf(session)}') 2349 CM = np.zeros((6,6)) 2350 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2351 try: 2352 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2353 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2354 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2355 try: 2356 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2357 CM[3,4] = self.standardization.covar[i2,j2] 2358 CM[4,3] = self.standardization.covar[j2,i2] 2359 except ValueError: 2360 pass 2361 try: 2362 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2363 CM[3,5] = self.standardization.covar[i2,k2] 2364 CM[5,3] = self.standardization.covar[k2,i2] 2365 except ValueError: 2366 pass 2367 except ValueError: 2368 pass 2369 try: 2370 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2371 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2372 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2373 try: 2374 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2375 CM[4,5] = self.standardization.covar[j2,k2] 2376 CM[5,4] = self.standardization.covar[k2,j2] 2377 except ValueError: 2378 pass 2379 except ValueError: 2380 pass 2381 try: 2382 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2383 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2384 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2385 except ValueError: 2386 pass 2387 2388 self.sessions[session]['CM'] = CM 2389 2390 elif self.standardization_method == 'indep_sessions': 2391 pass # Not implemented yet 2392 2393 2394 @make_verbal 2395 def repeatabilities(self): 2396 ''' 2397 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2398 (for all samples, for anchors, and for unknowns). 2399 ''' 2400 self.msg('Computing reproducibilities for all sessions') 2401 2402 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2403 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2404 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2405 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2406 self.repeatability[f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', samples = 'all samples') 2407 2408 2409 @make_verbal 2410 def consolidate(self, tables = True, plots = True): 2411 ''' 2412 Collect information about samples, sessions and repeatabilities. 2413 ''' 2414 self.consolidate_samples() 2415 self.consolidate_sessions() 2416 self.repeatabilities() 2417 2418 if tables: 2419 self.summary() 2420 self.table_of_sessions() 2421 self.table_of_analyses() 2422 self.table_of_samples() 2423 2424 if plots: 2425 self.plot_sessions() 2426 2427 2428 @make_verbal 2429 def rmswd(self, 2430 samples = 'all samples', 2431 sessions = 'all sessions', 2432 ): 2433 ''' 2434 Compute the χ2, root mean squared weighted deviation 2435 (i.e. reduced χ2), and corresponding degrees of freedom of the 2436 Δ4x values for samples in `samples` and sessions in `sessions`. 2437 2438 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2439 ''' 2440 if samples == 'all samples': 2441 mysamples = [k for k in self.samples] 2442 elif samples == 'anchors': 2443 mysamples = [k for k in self.anchors] 2444 elif samples == 'unknowns': 2445 mysamples = [k for k in self.unknowns] 2446 else: 2447 mysamples = samples 2448 2449 if sessions == 'all sessions': 2450 sessions = [k for k in self.sessions] 2451 2452 chisq, Nf = 0, 0 2453 for sample in mysamples : 2454 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2455 if len(G) > 1 : 2456 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2457 Nf += (len(G) - 1) 2458 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2459 r = (chisq / Nf)**.5 if Nf > 0 else 0 2460 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2461 return {'rmswd': r, 'chisq': chisq, 'Nf': Nf} 2462 2463 2464 @make_verbal 2465 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2466 ''' 2467 Compute the repeatability of `[r[key] for r in self]` 2468 ''' 2469 # NB: it's debatable whether rD47 should be computed 2470 # with Nf = len(self)-len(self.samples) instead of 2471 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2472 2473 if samples == 'all samples': 2474 mysamples = [k for k in self.samples] 2475 elif samples == 'anchors': 2476 mysamples = [k for k in self.anchors] 2477 elif samples == 'unknowns': 2478 mysamples = [k for k in self.unknowns] 2479 else: 2480 mysamples = samples 2481 2482 if sessions == 'all sessions': 2483 sessions = [k for k in self.sessions] 2484 2485 if key in ['D47', 'D48']: 2486 chisq, Nf = 0, 0 2487 for sample in mysamples : 2488 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2489 if len(X) > 1 : 2490 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2491 if sample in self.unknowns: 2492 Nf += len(X) - 1 2493 else: 2494 Nf += len(X) 2495 if samples in ['anchors', 'all samples']: 2496 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2497 r = (chisq / Nf)**.5 if Nf > 0 else 0 2498 2499 else: # if key not in ['D47', 'D48'] 2500 chisq, Nf = 0, 0 2501 for sample in mysamples : 2502 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2503 if len(X) > 1 : 2504 Nf += len(X) - 1 2505 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2506 r = (chisq / Nf)**.5 if Nf > 0 else 0 2507 2508 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2509 return r 2510 2511 def sample_average(self, samples, weights = 'equal', normalize = True): 2512 ''' 2513 Weighted average Δ4x value of a group of samples, accounting for covariance. 2514 2515 Returns the weighed average Δ4x value and associated SE 2516 of a group of samples. Weights are equal by default. If `normalize` is 2517 true, `weights` will be rescaled so that their sum equals 1. 2518 2519 **Examples** 2520 2521 ```python 2522 self.sample_average(['X','Y'], [1, 2]) 2523 ``` 2524 2525 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2526 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2527 values of samples X and Y, respectively. 2528 2529 ```python 2530 self.sample_average(['X','Y'], [1, -1], normalize = False) 2531 ``` 2532 2533 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2534 ''' 2535 if weights == 'equal': 2536 weights = [1/len(samples)] * len(samples) 2537 2538 if normalize: 2539 s = sum(weights) 2540 if s: 2541 weights = [w/s for w in weights] 2542 2543 try: 2544# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2545# C = self.standardization.covar[indices,:][:,indices] 2546 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2547 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2548 return correlated_sum(X, C, weights) 2549 except ValueError: 2550 return (0., 0.) 2551 2552 2553 def sample_D4x_covar(self, sample1, sample2 = None): 2554 ''' 2555 Covariance between Δ4x values of samples 2556 2557 Returns the error covariance between the average Δ4x values of two 2558 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2559 returns the Δ4x variance for that sample. 2560 ''' 2561 if sample2 is None: 2562 sample2 = sample1 2563 if self.standardization_method == 'pooled': 2564 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2565 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2566 return self.standardization.covar[i, j] 2567 elif self.standardization_method == 'indep_sessions': 2568 if sample1 == sample2: 2569 return self.samples[sample1][f'SE_D{self._4x}']**2 2570 else: 2571 c = 0 2572 for session in self.sessions: 2573 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2574 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2575 if sdata1 and sdata2: 2576 a = self.sessions[session]['a'] 2577 # !! TODO: CM below does not account for temporal changes in standardization parameters 2578 CM = self.sessions[session]['CM'][:3,:3] 2579 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2580 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2581 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2582 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2583 c += ( 2584 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2585 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2586 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2587 @ CM 2588 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2589 ) / a**2 2590 return float(c) 2591 2592 def sample_D4x_correl(self, sample1, sample2 = None): 2593 ''' 2594 Correlation between Δ4x errors of samples 2595 2596 Returns the error correlation between the average Δ4x values of two samples. 2597 ''' 2598 if sample2 is None or sample2 == sample1: 2599 return 1. 2600 return ( 2601 self.sample_D4x_covar(sample1, sample2) 2602 / self.unknowns[sample1][f'SE_D{self._4x}'] 2603 / self.unknowns[sample2][f'SE_D{self._4x}'] 2604 ) 2605 2606 def plot_single_session(self, 2607 session, 2608 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2609 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2610 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2611 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2612 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2613 xylimits = 'free', # | 'constant' 2614 x_label = None, 2615 y_label = None, 2616 error_contour_interval = 'auto', 2617 fig = 'new', 2618 ): 2619 ''' 2620 Generate plot for a single session 2621 ''' 2622 if x_label is None: 2623 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2624 if y_label is None: 2625 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2626 2627 out = _SessionPlot() 2628 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2629 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2630 2631 if fig == 'new': 2632 out.fig = ppl.figure(figsize = (6,6)) 2633 ppl.subplots_adjust(.1,.1,.9,.9) 2634 2635 out.anchor_analyses, = ppl.plot( 2636 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2637 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2638 **kw_plot_anchors) 2639 out.unknown_analyses, = ppl.plot( 2640 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2641 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2642 **kw_plot_unknowns) 2643 out.anchor_avg = ppl.plot( 2644 np.array([ np.array([ 2645 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2646 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2647 ]) for sample in anchors]).T, 2648 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2649 **kw_plot_anchor_avg) 2650 out.unknown_avg = ppl.plot( 2651 np.array([ np.array([ 2652 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2653 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2654 ]) for sample in unknowns]).T, 2655 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2656 **kw_plot_unknown_avg) 2657 if xylimits == 'constant': 2658 x = [r[f'd{self._4x}'] for r in self] 2659 y = [r[f'D{self._4x}'] for r in self] 2660 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2661 w, h = x2-x1, y2-y1 2662 x1 -= w/20 2663 x2 += w/20 2664 y1 -= h/20 2665 y2 += h/20 2666 ppl.axis([x1, x2, y1, y2]) 2667 elif xylimits == 'free': 2668 x1, x2, y1, y2 = ppl.axis() 2669 else: 2670 x1, x2, y1, y2 = ppl.axis(xylimits) 2671 2672 if error_contour_interval != 'none': 2673 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2674 XI,YI = np.meshgrid(xi, yi) 2675 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2676 if error_contour_interval == 'auto': 2677 rng = np.max(SI) - np.min(SI) 2678 if rng <= 0.01: 2679 cinterval = 0.001 2680 elif rng <= 0.03: 2681 cinterval = 0.004 2682 elif rng <= 0.1: 2683 cinterval = 0.01 2684 elif rng <= 0.3: 2685 cinterval = 0.03 2686 elif rng <= 1.: 2687 cinterval = 0.1 2688 else: 2689 cinterval = 0.5 2690 else: 2691 cinterval = error_contour_interval 2692 2693 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2694 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2695 out.clabel = ppl.clabel(out.contour) 2696 2697 ppl.xlabel(x_label) 2698 ppl.ylabel(y_label) 2699 ppl.title(session, weight = 'bold') 2700 ppl.grid(alpha = .2) 2701 out.ax = ppl.gca() 2702 2703 return out 2704 2705 def plot_residuals( 2706 self, 2707 hist = False, 2708 binwidth = 2/3, 2709 dir = 'output', 2710 filename = None, 2711 highlight = [], 2712 colors = None, 2713 figsize = None, 2714 ): 2715 ''' 2716 Plot residuals of each analysis as a function of time (actually, as a function of 2717 the order of analyses in the `D4xdata` object) 2718 2719 + `hist`: whether to add a histogram of residuals 2720 + `histbins`: specify bin edges for the histogram 2721 + `dir`: the directory in which to save the plot 2722 + `highlight`: a list of samples to highlight 2723 + `colors`: a dict of `{<sample>: <color>}` for all samples 2724 + `figsize`: (width, height) of figure 2725 ''' 2726 # Layout 2727 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2728 if hist: 2729 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2730 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2731 else: 2732 ppl.subplots_adjust(.08,.05,.78,.8) 2733 ax1 = ppl.subplot(111) 2734 2735 # Colors 2736 N = len(self.anchors) 2737 if colors is None: 2738 if len(highlight) > 0: 2739 Nh = len(highlight) 2740 if Nh == 1: 2741 colors = {highlight[0]: (0,0,0)} 2742 elif Nh == 3: 2743 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2744 elif Nh == 4: 2745 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2746 else: 2747 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2748 else: 2749 if N == 3: 2750 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2751 elif N == 4: 2752 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2753 else: 2754 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2755 2756 ppl.sca(ax1) 2757 2758 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2759 2760 session = self[0]['Session'] 2761 x1 = 0 2762# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2763 x_sessions = {} 2764 one_or_more_singlets = False 2765 one_or_more_multiplets = False 2766 multiplets = set() 2767 for k,r in enumerate(self): 2768 if r['Session'] != session: 2769 x2 = k-1 2770 x_sessions[session] = (x1+x2)/2 2771 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2772 session = r['Session'] 2773 x1 = k 2774 singlet = len(self.samples[r['Sample']]['data']) == 1 2775 if not singlet: 2776 multiplets.add(r['Sample']) 2777 if r['Sample'] in self.unknowns: 2778 if singlet: 2779 one_or_more_singlets = True 2780 else: 2781 one_or_more_multiplets = True 2782 kw = dict( 2783 marker = 'x' if singlet else '+', 2784 ms = 4 if singlet else 5, 2785 ls = 'None', 2786 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2787 mew = 1, 2788 alpha = 0.2 if singlet else 1, 2789 ) 2790 if highlight and r['Sample'] not in highlight: 2791 kw['alpha'] = 0.2 2792 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2793 x2 = k 2794 x_sessions[session] = (x1+x2)/2 2795 2796 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2797 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2798 if not hist: 2799 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2800 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') 2801 2802 xmin, xmax, ymin, ymax = ppl.axis() 2803 for s in x_sessions: 2804 ppl.text( 2805 x_sessions[s], 2806 ymax +1, 2807 s, 2808 va = 'bottom', 2809 **( 2810 dict(ha = 'center') 2811 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2812 else dict(ha = 'left', rotation = 45) 2813 ) 2814 ) 2815 2816 if hist: 2817 ppl.sca(ax2) 2818 2819 for s in colors: 2820 kw['marker'] = '+' 2821 kw['ms'] = 5 2822 kw['mec'] = colors[s] 2823 kw['label'] = s 2824 kw['alpha'] = 1 2825 ppl.plot([], [], **kw) 2826 2827 kw['mec'] = (0,0,0) 2828 2829 if one_or_more_singlets: 2830 kw['marker'] = 'x' 2831 kw['ms'] = 4 2832 kw['alpha'] = .2 2833 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2834 ppl.plot([], [], **kw) 2835 2836 if one_or_more_multiplets: 2837 kw['marker'] = '+' 2838 kw['ms'] = 4 2839 kw['alpha'] = 1 2840 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2841 ppl.plot([], [], **kw) 2842 2843 if hist: 2844 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2845 else: 2846 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2847 leg.set_zorder(-1000) 2848 2849 ppl.sca(ax1) 2850 2851 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2852 ppl.xticks([]) 2853 ppl.axis([-1, len(self), None, None]) 2854 2855 if hist: 2856 ppl.sca(ax2) 2857 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2858 ppl.hist( 2859 X, 2860 orientation = 'horizontal', 2861 histtype = 'stepfilled', 2862 ec = [.4]*3, 2863 fc = [.25]*3, 2864 alpha = .25, 2865 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2866 ) 2867 ppl.axis([None, None, ymin, ymax]) 2868 ppl.text(0, 0, 2869 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2870 size = 8, 2871 alpha = 1, 2872 va = 'center', 2873 ha = 'left', 2874 ) 2875 2876 ppl.xticks([]) 2877 ppl.yticks([]) 2878# ax2.spines['left'].set_visible(False) 2879 ax2.spines['right'].set_visible(False) 2880 ax2.spines['top'].set_visible(False) 2881 ax2.spines['bottom'].set_visible(False) 2882 2883 2884 if not os.path.exists(dir): 2885 os.makedirs(dir) 2886 if filename is None: 2887 return fig 2888 elif filename == '': 2889 filename = f'D{self._4x}_residuals.pdf' 2890 ppl.savefig(f'{dir}/{filename}') 2891 ppl.close(fig) 2892 2893 2894 def simulate(self, *args, **kwargs): 2895 ''' 2896 Legacy function with warning message pointing to `virtual_data()` 2897 ''' 2898 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()') 2899 2900 def plot_distribution_of_analyses( 2901 self, 2902 dir = 'output', 2903 filename = None, 2904 vs_time = False, 2905 figsize = (6,4), 2906 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2907 output = None, 2908 ): 2909 ''' 2910 Plot temporal distribution of all analyses in the data set. 2911 2912 **Parameters** 2913 2914 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2915 ''' 2916 2917 asamples = [s for s in self.anchors] 2918 usamples = [s for s in self.unknowns] 2919 if output is None or output == 'fig': 2920 fig = ppl.figure(figsize = figsize) 2921 ppl.subplots_adjust(*subplots_adjust) 2922 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2923 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2924 Xmax += (Xmax-Xmin)/40 2925 Xmin -= (Xmax-Xmin)/41 2926 for k, s in enumerate(asamples + usamples): 2927 if vs_time: 2928 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2929 else: 2930 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2931 Y = [-k for x in X] 2932 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2933 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2934 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2935 ppl.axis([Xmin, Xmax, -k-1, 1]) 2936 ppl.xlabel('\ntime') 2937 ppl.gca().annotate('', 2938 xy = (0.6, -0.02), 2939 xycoords = 'axes fraction', 2940 xytext = (.4, -0.02), 2941 arrowprops = dict(arrowstyle = "->", color = 'k'), 2942 ) 2943 2944 2945 x2 = -1 2946 for session in self.sessions: 2947 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2948 if vs_time: 2949 ppl.axvline(x1, color = 'k', lw = .75) 2950 if x2 > -1: 2951 if not vs_time: 2952 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2953 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2954# from xlrd import xldate_as_datetime 2955# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2956 if vs_time: 2957 ppl.axvline(x2, color = 'k', lw = .75) 2958 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2959 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2960 2961 ppl.xticks([]) 2962 ppl.yticks([]) 2963 2964 if output is None: 2965 if not os.path.exists(dir): 2966 os.makedirs(dir) 2967 if filename == None: 2968 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2969 ppl.savefig(f'{dir}/{filename}') 2970 ppl.close(fig) 2971 elif output == 'ax': 2972 return ppl.gca() 2973 elif output == 'fig': 2974 return fig
Store and process data for a large set of Δ47 and/or Δ48 analyses, usually comprising more than one analytical session.
1022 def __init__(self, l = [], mass = '47', logfile = '', session = 'mySession', verbose = False): 1023 ''' 1024 **Parameters** 1025 1026 + `l`: a list of dictionaries, with each dictionary including at least the keys 1027 `Sample`, `d45`, `d46`, and `d47` or `d48`. 1028 + `mass`: `'47'` or `'48'` 1029 + `logfile`: if specified, write detailed logs to this file path when calling `D4xdata` methods. 1030 + `session`: define session name for analyses without a `Session` key 1031 + `verbose`: if `True`, print out detailed logs when calling `D4xdata` methods. 1032 1033 Returns a `D4xdata` object derived from `list`. 1034 ''' 1035 self._4x = mass 1036 self.verbose = verbose 1037 self.prefix = 'D4xdata' 1038 self.logfile = logfile 1039 list.__init__(self, l) 1040 self.Nf = None 1041 self.repeatability = {} 1042 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).
1045 def make_verbal(oldfun): 1046 ''' 1047 Decorator: allow temporarily changing `self.prefix` and overriding `self.verbose`. 1048 ''' 1049 @wraps(oldfun) 1050 def newfun(*args, verbose = '', **kwargs): 1051 myself = args[0] 1052 oldprefix = myself.prefix 1053 myself.prefix = oldfun.__name__ 1054 if verbose != '': 1055 oldverbose = myself.verbose 1056 myself.verbose = verbose 1057 out = oldfun(*args, **kwargs) 1058 myself.prefix = oldprefix 1059 if verbose != '': 1060 myself.verbose = oldverbose 1061 return out 1062 return newfun
Decorator: allow temporarily changing self.prefix
and overriding self.verbose
.
1065 def msg(self, txt): 1066 ''' 1067 Log a message to `self.logfile`, and print it out if `verbose = True` 1068 ''' 1069 self.log(txt) 1070 if self.verbose: 1071 print(f'{f"[{self.prefix}]":<16} {txt}')
Log a message to self.logfile
, and print it out if verbose = True
1074 def vmsg(self, txt): 1075 ''' 1076 Log a message to `self.logfile` and print it out 1077 ''' 1078 self.log(txt) 1079 print(txt)
Log a message to self.logfile
and print it out
1082 def log(self, *txts): 1083 ''' 1084 Log a message to `self.logfile` 1085 ''' 1086 if self.logfile: 1087 with open(self.logfile, 'a') as fid: 1088 for txt in txts: 1089 fid.write(f'\n{dt.now().strftime("%Y-%m-%d %H:%M:%S")} {f"[{self.prefix}]":<16} {txt}')
Log a message to self.logfile
1092 def refresh(self, session = 'mySession'): 1093 ''' 1094 Update `self.sessions`, `self.samples`, `self.anchors`, and `self.unknowns`. 1095 ''' 1096 self.fill_in_missing_info(session = session) 1097 self.refresh_sessions() 1098 self.refresh_samples()
Update self.sessions
, self.samples
, self.anchors
, and self.unknowns
.
1101 def refresh_sessions(self): 1102 ''' 1103 Update `self.sessions` and set `scrambling_drift`, `slope_drift`, and `wg_drift` 1104 to `False` for all sessions. 1105 ''' 1106 self.sessions = { 1107 s: {'data': [r for r in self if r['Session'] == s]} 1108 for s in sorted({r['Session'] for r in self}) 1109 } 1110 for s in self.sessions: 1111 self.sessions[s]['scrambling_drift'] = False 1112 self.sessions[s]['slope_drift'] = False 1113 self.sessions[s]['wg_drift'] = False 1114 self.sessions[s]['d13C_standardization_method'] = self.d13C_STANDARDIZATION_METHOD 1115 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.
1118 def refresh_samples(self): 1119 ''' 1120 Define `self.samples`, `self.anchors`, and `self.unknowns`. 1121 ''' 1122 self.samples = { 1123 s: {'data': [r for r in self if r['Sample'] == s]} 1124 for s in sorted({r['Sample'] for r in self}) 1125 } 1126 self.anchors = {s: self.samples[s] for s in self.samples if s in self.Nominal_D4x} 1127 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
.
1130 def read(self, filename, sep = '', session = ''): 1131 ''' 1132 Read file in csv format to load data into a `D47data` object. 1133 1134 In the csv file, spaces before and after field separators (`','` by default) 1135 are optional. Each line corresponds to a single analysis. 1136 1137 The required fields are: 1138 1139 + `UID`: a unique identifier 1140 + `Session`: an identifier for the analytical session 1141 + `Sample`: a sample identifier 1142 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1143 1144 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1145 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1146 and `d49` are optional, and set to NaN by default. 1147 1148 **Parameters** 1149 1150 + `fileneme`: the path of the file to read 1151 + `sep`: csv separator delimiting the fields 1152 + `session`: set `Session` field to this string for all analyses 1153 ''' 1154 with open(filename) as fid: 1155 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
1158 def input(self, txt, sep = '', session = ''): 1159 ''' 1160 Read `txt` string in csv format to load analysis data into a `D47data` object. 1161 1162 In the csv string, spaces before and after field separators (`','` by default) 1163 are optional. Each line corresponds to a single analysis. 1164 1165 The required fields are: 1166 1167 + `UID`: a unique identifier 1168 + `Session`: an identifier for the analytical session 1169 + `Sample`: a sample identifier 1170 + `d45`, `d46`, and at least one of `d47` or `d48`: the working-gas delta values 1171 1172 Independently known oxygen-17 anomalies may be provided as `D17O` (in ‰ relative to 1173 VSMOW, λ = `self.LAMBDA_17`), and are otherwise assumed to be zero. Working-gas deltas `d47`, `d48` 1174 and `d49` are optional, and set to NaN by default. 1175 1176 **Parameters** 1177 1178 + `txt`: the csv string to read 1179 + `sep`: csv separator delimiting the fields. By default, use `,`, `;`, or `\t`, 1180 whichever appers most often in `txt`. 1181 + `session`: set `Session` field to this string for all analyses 1182 ''' 1183 if sep == '': 1184 sep = sorted(',;\t', key = lambda x: - txt.count(x))[0] 1185 txt = [[x.strip() for x in l.split(sep)] for l in txt.splitlines() if l.strip()] 1186 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:]] 1187 1188 if session != '': 1189 for r in data: 1190 r['Session'] = session 1191 1192 self += data 1193 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
1196 @make_verbal 1197 def wg(self, samples = None, a18_acid = None): 1198 ''' 1199 Compute bulk composition of the working gas for each session based on 1200 the carbonate standards defined in both `self.Nominal_d13C_VPDB` and 1201 `self.Nominal_d18O_VPDB`. 1202 ''' 1203 1204 self.msg('Computing WG composition:') 1205 1206 if a18_acid is None: 1207 a18_acid = self.ALPHA_18O_ACID_REACTION 1208 if samples is None: 1209 samples = [s for s in self.Nominal_d13C_VPDB if s in self.Nominal_d18O_VPDB] 1210 1211 assert a18_acid, f'Acid fractionation factor should not be zero.' 1212 1213 samples = [s for s in samples if s in self.Nominal_d13C_VPDB and s in self.Nominal_d18O_VPDB] 1214 R45R46_standards = {} 1215 for sample in samples: 1216 d13C_vpdb = self.Nominal_d13C_VPDB[sample] 1217 d18O_vpdb = self.Nominal_d18O_VPDB[sample] 1218 R13_s = self.R13_VPDB * (1 + d13C_vpdb / 1000) 1219 R17_s = self.R17_VPDB * ((1 + d18O_vpdb / 1000) * a18_acid) ** self.LAMBDA_17 1220 R18_s = self.R18_VPDB * (1 + d18O_vpdb / 1000) * a18_acid 1221 1222 C12_s = 1 / (1 + R13_s) 1223 C13_s = R13_s / (1 + R13_s) 1224 C16_s = 1 / (1 + R17_s + R18_s) 1225 C17_s = R17_s / (1 + R17_s + R18_s) 1226 C18_s = R18_s / (1 + R17_s + R18_s) 1227 1228 C626_s = C12_s * C16_s ** 2 1229 C627_s = 2 * C12_s * C16_s * C17_s 1230 C628_s = 2 * C12_s * C16_s * C18_s 1231 C636_s = C13_s * C16_s ** 2 1232 C637_s = 2 * C13_s * C16_s * C17_s 1233 C727_s = C12_s * C17_s ** 2 1234 1235 R45_s = (C627_s + C636_s) / C626_s 1236 R46_s = (C628_s + C637_s + C727_s) / C626_s 1237 R45R46_standards[sample] = (R45_s, R46_s) 1238 1239 for s in self.sessions: 1240 db = [r for r in self.sessions[s]['data'] if r['Sample'] in samples] 1241 assert db, f'No sample from {samples} found in session "{s}".' 1242# dbsamples = sorted({r['Sample'] for r in db}) 1243 1244 X = [r['d45'] for r in db] 1245 Y = [R45R46_standards[r['Sample']][0] for r in db] 1246 x1, x2 = np.min(X), np.max(X) 1247 1248 if x1 < x2: 1249 wgcoord = x1/(x1-x2) 1250 else: 1251 wgcoord = 999 1252 1253 if wgcoord < -.5 or wgcoord > 1.5: 1254 # unreasonable to extrapolate to d45 = 0 1255 R45_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1256 else : 1257 # d45 = 0 is reasonably well bracketed 1258 R45_wg = np.polyfit(X, Y, 1)[1] 1259 1260 X = [r['d46'] for r in db] 1261 Y = [R45R46_standards[r['Sample']][1] for r in db] 1262 x1, x2 = np.min(X), np.max(X) 1263 1264 if x1 < x2: 1265 wgcoord = x1/(x1-x2) 1266 else: 1267 wgcoord = 999 1268 1269 if wgcoord < -.5 or wgcoord > 1.5: 1270 # unreasonable to extrapolate to d46 = 0 1271 R46_wg = np.mean([y/(1+x/1000) for x,y in zip(X,Y)]) 1272 else : 1273 # d46 = 0 is reasonably well bracketed 1274 R46_wg = np.polyfit(X, Y, 1)[1] 1275 1276 d13Cwg_VPDB, d18Owg_VSMOW = self.compute_bulk_delta(R45_wg, R46_wg) 1277 1278 self.msg(f'Session {s} WG: δ13C_VPDB = {d13Cwg_VPDB:.3f} δ18O_VSMOW = {d18Owg_VSMOW:.3f}') 1279 1280 self.sessions[s]['d13Cwg_VPDB'] = d13Cwg_VPDB 1281 self.sessions[s]['d18Owg_VSMOW'] = d18Owg_VSMOW 1282 for r in self.sessions[s]['data']: 1283 r['d13Cwg_VPDB'] = d13Cwg_VPDB 1284 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
.
1287 def compute_bulk_delta(self, R45, R46, D17O = 0): 1288 ''' 1289 Compute δ13C_VPDB and δ18O_VSMOW, 1290 by solving the generalized form of equation (17) from 1291 [Brand et al. (2010)](https://doi.org/10.1351/PAC-REP-09-01-05), 1292 assuming that δ18O_VSMOW is not too big (0 ± 50 ‰) and 1293 solving the corresponding second-order Taylor polynomial. 1294 (Appendix A of [Daëron et al., 2016](https://doi.org/10.1016/j.chemgeo.2016.08.014)) 1295 ''' 1296 1297 K = np.exp(D17O / 1000) * self.R17_VSMOW * self.R18_VSMOW ** -self.LAMBDA_17 1298 1299 A = -3 * K ** 2 * self.R18_VSMOW ** (2 * self.LAMBDA_17) 1300 B = 2 * K * R45 * self.R18_VSMOW ** self.LAMBDA_17 1301 C = 2 * self.R18_VSMOW 1302 D = -R46 1303 1304 aa = A * self.LAMBDA_17 * (2 * self.LAMBDA_17 - 1) + B * self.LAMBDA_17 * (self.LAMBDA_17 - 1) / 2 1305 bb = 2 * A * self.LAMBDA_17 + B * self.LAMBDA_17 + C 1306 cc = A + B + C + D 1307 1308 d18O_VSMOW = 1000 * (-bb + (bb ** 2 - 4 * aa * cc) ** .5) / (2 * aa) 1309 1310 R18 = (1 + d18O_VSMOW / 1000) * self.R18_VSMOW 1311 R17 = K * R18 ** self.LAMBDA_17 1312 R13 = R45 - 2 * R17 1313 1314 d13C_VPDB = 1000 * (R13 / self.R13_VPDB - 1) 1315 1316 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)
1319 @make_verbal 1320 def crunch(self, verbose = ''): 1321 ''' 1322 Compute bulk composition and raw clumped isotope anomalies for all analyses. 1323 ''' 1324 for r in self: 1325 self.compute_bulk_and_clumping_deltas(r) 1326 self.standardize_d13C() 1327 self.standardize_d18O() 1328 self.msg(f"Crunched {len(self)} analyses.")
Compute bulk composition and raw clumped isotope anomalies for all analyses.
1331 def fill_in_missing_info(self, session = 'mySession'): 1332 ''' 1333 Fill in optional fields with default values 1334 ''' 1335 for i,r in enumerate(self): 1336 if 'D17O' not in r: 1337 r['D17O'] = 0. 1338 if 'UID' not in r: 1339 r['UID'] = f'{i+1}' 1340 if 'Session' not in r: 1341 r['Session'] = session 1342 for k in ['d47', 'd48', 'd49']: 1343 if k not in r: 1344 r[k] = np.nan
Fill in optional fields with default values
1347 def standardize_d13C(self): 1348 ''' 1349 Perform δ13C standadization within each session `s` according to 1350 `self.sessions[s]['d13C_standardization_method']`, which is defined by default 1351 by `D47data.refresh_sessions()`as equal to `self.d13C_STANDARDIZATION_METHOD`, but 1352 may be redefined abitrarily at a later stage. 1353 ''' 1354 for s in self.sessions: 1355 if self.sessions[s]['d13C_standardization_method'] in ['1pt', '2pt']: 1356 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] 1357 X,Y = zip(*XY) 1358 if self.sessions[s]['d13C_standardization_method'] == '1pt': 1359 offset = np.mean(Y) - np.mean(X) 1360 for r in self.sessions[s]['data']: 1361 r['d13C_VPDB'] += offset 1362 elif self.sessions[s]['d13C_standardization_method'] == '2pt': 1363 a,b = np.polyfit(X,Y,1) 1364 for r in self.sessions[s]['data']: 1365 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.
1367 def standardize_d18O(self): 1368 ''' 1369 Perform δ18O standadization within each session `s` according to 1370 `self.ALPHA_18O_ACID_REACTION` and `self.sessions[s]['d18O_standardization_method']`, 1371 which is defined by default by `D47data.refresh_sessions()`as equal to 1372 `self.d18O_STANDARDIZATION_METHOD`, but may be redefined abitrarily at a later stage. 1373 ''' 1374 for s in self.sessions: 1375 if self.sessions[s]['d18O_standardization_method'] in ['1pt', '2pt']: 1376 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] 1377 X,Y = zip(*XY) 1378 Y = [(1000+y) * self.R18_VPDB * self.ALPHA_18O_ACID_REACTION / self.R18_VSMOW - 1000 for y in Y] 1379 if self.sessions[s]['d18O_standardization_method'] == '1pt': 1380 offset = np.mean(Y) - np.mean(X) 1381 for r in self.sessions[s]['data']: 1382 r['d18O_VSMOW'] += offset 1383 elif self.sessions[s]['d18O_standardization_method'] == '2pt': 1384 a,b = np.polyfit(X,Y,1) 1385 for r in self.sessions[s]['data']: 1386 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.
1389 def compute_bulk_and_clumping_deltas(self, r): 1390 ''' 1391 Compute δ13C_VPDB, δ18O_VSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis `r`. 1392 ''' 1393 1394 # Compute working gas R13, R18, and isobar ratios 1395 R13_wg = self.R13_VPDB * (1 + r['d13Cwg_VPDB'] / 1000) 1396 R18_wg = self.R18_VSMOW * (1 + r['d18Owg_VSMOW'] / 1000) 1397 R45_wg, R46_wg, R47_wg, R48_wg, R49_wg = self.compute_isobar_ratios(R13_wg, R18_wg) 1398 1399 # Compute analyte isobar ratios 1400 R45 = (1 + r['d45'] / 1000) * R45_wg 1401 R46 = (1 + r['d46'] / 1000) * R46_wg 1402 R47 = (1 + r['d47'] / 1000) * R47_wg 1403 R48 = (1 + r['d48'] / 1000) * R48_wg 1404 R49 = (1 + r['d49'] / 1000) * R49_wg 1405 1406 r['d13C_VPDB'], r['d18O_VSMOW'] = self.compute_bulk_delta(R45, R46, D17O = r['D17O']) 1407 R13 = (1 + r['d13C_VPDB'] / 1000) * self.R13_VPDB 1408 R18 = (1 + r['d18O_VSMOW'] / 1000) * self.R18_VSMOW 1409 1410 # Compute stochastic isobar ratios of the analyte 1411 R45stoch, R46stoch, R47stoch, R48stoch, R49stoch = self.compute_isobar_ratios( 1412 R13, R18, D17O = r['D17O'] 1413 ) 1414 1415 # Check that R45/R45stoch and R46/R46stoch are undistinguishable from 1, 1416 # and raise a warning if the corresponding anomalies exceed 0.02 ppm. 1417 if (R45 / R45stoch - 1) > 5e-8: 1418 self.vmsg(f'This is unexpected: R45/R45stoch - 1 = {1e6 * (R45 / R45stoch - 1):.3f} ppm') 1419 if (R46 / R46stoch - 1) > 5e-8: 1420 self.vmsg(f'This is unexpected: R46/R46stoch - 1 = {1e6 * (R46 / R46stoch - 1):.3f} ppm') 1421 1422 # Compute raw clumped isotope anomalies 1423 r['D47raw'] = 1000 * (R47 / R47stoch - 1) 1424 r['D48raw'] = 1000 * (R48 / R48stoch - 1) 1425 r['D49raw'] = 1000 * (R49 / R49stoch - 1)
Compute δ13CVPDB, δ18OVSMOW, and raw Δ47, Δ48, Δ49 values for a single analysis r
.
1428 def compute_isobar_ratios(self, R13, R18, D17O=0, D47=0, D48=0, D49=0): 1429 ''' 1430 Compute isobar ratios for a sample with isotopic ratios `R13` and `R18`, 1431 optionally accounting for non-zero values of Δ17O (`D17O`) and clumped isotope 1432 anomalies (`D47`, `D48`, `D49`), all expressed in permil. 1433 ''' 1434 1435 # Compute R17 1436 R17 = self.R17_VSMOW * np.exp(D17O / 1000) * (R18 / self.R18_VSMOW) ** self.LAMBDA_17 1437 1438 # Compute isotope concentrations 1439 C12 = (1 + R13) ** -1 1440 C13 = C12 * R13 1441 C16 = (1 + R17 + R18) ** -1 1442 C17 = C16 * R17 1443 C18 = C16 * R18 1444 1445 # Compute stochastic isotopologue concentrations 1446 C626 = C16 * C12 * C16 1447 C627 = C16 * C12 * C17 * 2 1448 C628 = C16 * C12 * C18 * 2 1449 C636 = C16 * C13 * C16 1450 C637 = C16 * C13 * C17 * 2 1451 C638 = C16 * C13 * C18 * 2 1452 C727 = C17 * C12 * C17 1453 C728 = C17 * C12 * C18 * 2 1454 C737 = C17 * C13 * C17 1455 C738 = C17 * C13 * C18 * 2 1456 C828 = C18 * C12 * C18 1457 C838 = C18 * C13 * C18 1458 1459 # Compute stochastic isobar ratios 1460 R45 = (C636 + C627) / C626 1461 R46 = (C628 + C637 + C727) / C626 1462 R47 = (C638 + C728 + C737) / C626 1463 R48 = (C738 + C828) / C626 1464 R49 = C838 / C626 1465 1466 # Account for stochastic anomalies 1467 R47 *= 1 + D47 / 1000 1468 R48 *= 1 + D48 / 1000 1469 R49 *= 1 + D49 / 1000 1470 1471 # Return isobar ratios 1472 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.
1475 def split_samples(self, samples_to_split = 'all', grouping = 'by_session'): 1476 ''' 1477 Split unknown samples by UID (treat all analyses as different samples) 1478 or by session (treat analyses of a given sample in different sessions as 1479 different samples). 1480 1481 **Parameters** 1482 1483 + `samples_to_split`: a list of samples to split, e.g., `['IAEA-C1', 'IAEA-C2']` 1484 + `grouping`: `by_uid` | `by_session` 1485 ''' 1486 if samples_to_split == 'all': 1487 samples_to_split = [s for s in self.unknowns] 1488 gkeys = {'by_uid':'UID', 'by_session':'Session'} 1489 self.grouping = grouping.lower() 1490 if self.grouping in gkeys: 1491 gkey = gkeys[self.grouping] 1492 for r in self: 1493 if r['Sample'] in samples_to_split: 1494 r['Sample_original'] = r['Sample'] 1495 r['Sample'] = f"{r['Sample']}__{r[gkey]}" 1496 elif r['Sample'] in self.unknowns: 1497 r['Sample_original'] = r['Sample'] 1498 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
1501 def unsplit_samples(self, tables = False): 1502 ''' 1503 Reverse the effects of `D47data.split_samples()`. 1504 1505 This should only be used after `D4xdata.standardize()` with `method='pooled'`. 1506 1507 After `D4xdata.standardize()` with `method='indep_sessions'`, one should 1508 probably use `D4xdata.combine_samples()` instead to reverse the effects of 1509 `D47data.split_samples()` with `grouping='by_uid'`, or `w_avg()` to reverse the 1510 effects of `D47data.split_samples()` with `grouping='by_sessions'` (because in 1511 that case session-averaged Δ4x values are statistically independent). 1512 ''' 1513 unknowns_old = sorted({s for s in self.unknowns}) 1514 CM_old = self.standardization.covar[:,:] 1515 VD_old = self.standardization.params.valuesdict().copy() 1516 vars_old = self.standardization.var_names 1517 1518 unknowns_new = sorted({r['Sample_original'] for r in self if 'Sample_original' in r}) 1519 1520 Ns = len(vars_old) - len(unknowns_old) 1521 vars_new = vars_old[:Ns] + [f'D{self._4x}_{pf(u)}' for u in unknowns_new] 1522 VD_new = {k: VD_old[k] for k in vars_old[:Ns]} 1523 1524 W = np.zeros((len(vars_new), len(vars_old))) 1525 W[:Ns,:Ns] = np.eye(Ns) 1526 for u in unknowns_new: 1527 splits = sorted({r['Sample'] for r in self if 'Sample_original' in r and r['Sample_original'] == u}) 1528 if self.grouping == 'by_session': 1529 weights = [self.samples[s][f'SE_D{self._4x}']**-2 for s in splits] 1530 elif self.grouping == 'by_uid': 1531 weights = [1 for s in splits] 1532 sw = sum(weights) 1533 weights = [w/sw for w in weights] 1534 W[vars_new.index(f'D{self._4x}_{pf(u)}'),[vars_old.index(f'D{self._4x}_{pf(s)}') for s in splits]] = weights[:] 1535 1536 CM_new = W @ CM_old @ W.T 1537 V = W @ np.array([[VD_old[k]] for k in vars_old]) 1538 VD_new = {k:v[0] for k,v in zip(vars_new, V)} 1539 1540 self.standardization.covar = CM_new 1541 self.standardization.params.valuesdict = lambda : VD_new 1542 self.standardization.var_names = vars_new 1543 1544 for r in self: 1545 if r['Sample'] in self.unknowns: 1546 r['Sample_split'] = r['Sample'] 1547 r['Sample'] = r['Sample_original'] 1548 1549 self.refresh_samples() 1550 self.consolidate_samples() 1551 self.repeatabilities() 1552 1553 if tables: 1554 self.table_of_analyses() 1555 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).
1557 def assign_timestamps(self): 1558 ''' 1559 Assign a time field `t` of type `float` to each analysis. 1560 1561 If `TimeTag` is one of the data fields, `t` is equal within a given session 1562 to `TimeTag` minus the mean value of `TimeTag` for that session. 1563 Otherwise, `TimeTag` is by default equal to the index of each analysis 1564 in the dataset and `t` is defined as above. 1565 ''' 1566 for session in self.sessions: 1567 sdata = self.sessions[session]['data'] 1568 try: 1569 t0 = np.mean([r['TimeTag'] for r in sdata]) 1570 for r in sdata: 1571 r['t'] = r['TimeTag'] - t0 1572 except KeyError: 1573 t0 = (len(sdata)-1)/2 1574 for t,r in enumerate(sdata): 1575 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.
1578 def report(self): 1579 ''' 1580 Prints a report on the standardization fit. 1581 Only applicable after `D4xdata.standardize(method='pooled')`. 1582 ''' 1583 report_fit(self.standardization)
Prints a report on the standardization fit.
Only applicable after D4xdata.standardize(method='pooled')
.
1586 def combine_samples(self, sample_groups): 1587 ''' 1588 Combine analyses of different samples to compute weighted average Δ4x 1589 and new error (co)variances corresponding to the groups defined by the `sample_groups` 1590 dictionary. 1591 1592 Caution: samples are weighted by number of replicate analyses, which is a 1593 reasonable default behavior but is not always optimal (e.g., in the case of strongly 1594 correlated analytical errors for one or more samples). 1595 1596 Returns a tuplet of: 1597 1598 + the list of group names 1599 + an array of the corresponding Δ4x values 1600 + the corresponding (co)variance matrix 1601 1602 **Parameters** 1603 1604 + `sample_groups`: a dictionary of the form: 1605 ```py 1606 {'group1': ['sample_1', 'sample_2'], 1607 'group2': ['sample_3', 'sample_4', 'sample_5']} 1608 ``` 1609 ''' 1610 1611 samples = [s for k in sorted(sample_groups.keys()) for s in sorted(sample_groups[k])] 1612 groups = sorted(sample_groups.keys()) 1613 group_total_weights = {k: sum([self.samples[s]['N'] for s in sample_groups[k]]) for k in groups} 1614 D4x_old = np.array([[self.samples[x][f'D{self._4x}']] for x in samples]) 1615 CM_old = np.array([[self.sample_D4x_covar(x,y) for x in samples] for y in samples]) 1616 W = np.array([ 1617 [self.samples[i]['N']/group_total_weights[j] if i in sample_groups[j] else 0 for i in samples] 1618 for j in groups]) 1619 D4x_new = W @ D4x_old 1620 CM_new = W @ CM_old @ W.T 1621 1622 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']}
1625 @make_verbal 1626 def standardize(self, 1627 method = 'pooled', 1628 weighted_sessions = [], 1629 consolidate = True, 1630 consolidate_tables = False, 1631 consolidate_plots = False, 1632 constraints = {}, 1633 ): 1634 ''' 1635 Compute absolute Δ4x values for all replicate analyses and for sample averages. 1636 If `method` argument is set to `'pooled'`, the standardization processes all sessions 1637 in a single step, assuming that all samples (anchors and unknowns alike) are homogeneous, 1638 i.e. that their true Δ4x value does not change between sessions, 1639 ([Daëron, 2021](https://doi.org/10.1029/2020GC009592)). If `method` argument is set to 1640 `'indep_sessions'`, the standardization processes each session independently, based only 1641 on anchors analyses. 1642 ''' 1643 1644 self.standardization_method = method 1645 self.assign_timestamps() 1646 1647 if method == 'pooled': 1648 if weighted_sessions: 1649 for session_group in weighted_sessions: 1650 if self._4x == '47': 1651 X = D47data([r for r in self if r['Session'] in session_group]) 1652 elif self._4x == '48': 1653 X = D48data([r for r in self if r['Session'] in session_group]) 1654 X.Nominal_D4x = self.Nominal_D4x.copy() 1655 X.refresh() 1656 result = X.standardize(method = 'pooled', weighted_sessions = [], consolidate = False) 1657 w = np.sqrt(result.redchi) 1658 self.msg(f'Session group {session_group} MRSWD = {w:.4f}') 1659 for r in X: 1660 r[f'wD{self._4x}raw'] *= w 1661 else: 1662 self.msg(f'All D{self._4x}raw weights set to 1 ‰') 1663 for r in self: 1664 r[f'wD{self._4x}raw'] = 1. 1665 1666 params = Parameters() 1667 for k,session in enumerate(self.sessions): 1668 self.msg(f"Session {session}: scrambling_drift is {self.sessions[session]['scrambling_drift']}.") 1669 self.msg(f"Session {session}: slope_drift is {self.sessions[session]['slope_drift']}.") 1670 self.msg(f"Session {session}: wg_drift is {self.sessions[session]['wg_drift']}.") 1671 s = pf(session) 1672 params.add(f'a_{s}', value = 0.9) 1673 params.add(f'b_{s}', value = 0.) 1674 params.add(f'c_{s}', value = -0.9) 1675 params.add(f'a2_{s}', value = 0., 1676# vary = self.sessions[session]['scrambling_drift'], 1677 ) 1678 params.add(f'b2_{s}', value = 0., 1679# vary = self.sessions[session]['slope_drift'], 1680 ) 1681 params.add(f'c2_{s}', value = 0., 1682# vary = self.sessions[session]['wg_drift'], 1683 ) 1684 if not self.sessions[session]['scrambling_drift']: 1685 params[f'a2_{s}'].expr = '0' 1686 if not self.sessions[session]['slope_drift']: 1687 params[f'b2_{s}'].expr = '0' 1688 if not self.sessions[session]['wg_drift']: 1689 params[f'c2_{s}'].expr = '0' 1690 1691 for sample in self.unknowns: 1692 params.add(f'D{self._4x}_{pf(sample)}', value = 0.5) 1693 1694 for k in constraints: 1695 params[k].expr = constraints[k] 1696 1697 def residuals(p): 1698 R = [] 1699 for r in self: 1700 session = pf(r['Session']) 1701 sample = pf(r['Sample']) 1702 if r['Sample'] in self.Nominal_D4x: 1703 R += [ ( 1704 r[f'D{self._4x}raw'] - ( 1705 p[f'a_{session}'] * self.Nominal_D4x[r['Sample']] 1706 + p[f'b_{session}'] * r[f'd{self._4x}'] 1707 + p[f'c_{session}'] 1708 + r['t'] * ( 1709 p[f'a2_{session}'] * self.Nominal_D4x[r['Sample']] 1710 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1711 + p[f'c2_{session}'] 1712 ) 1713 ) 1714 ) / r[f'wD{self._4x}raw'] ] 1715 else: 1716 R += [ ( 1717 r[f'D{self._4x}raw'] - ( 1718 p[f'a_{session}'] * p[f'D{self._4x}_{sample}'] 1719 + p[f'b_{session}'] * r[f'd{self._4x}'] 1720 + p[f'c_{session}'] 1721 + r['t'] * ( 1722 p[f'a2_{session}'] * p[f'D{self._4x}_{sample}'] 1723 + p[f'b2_{session}'] * r[f'd{self._4x}'] 1724 + p[f'c2_{session}'] 1725 ) 1726 ) 1727 ) / r[f'wD{self._4x}raw'] ] 1728 return R 1729 1730 M = Minimizer(residuals, params) 1731 result = M.least_squares() 1732 self.Nf = result.nfree 1733 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1734 new_names, new_covar, new_se = _fullcovar(result)[:3] 1735 result.var_names = new_names 1736 result.covar = new_covar 1737 1738 for r in self: 1739 s = pf(r["Session"]) 1740 a = result.params.valuesdict()[f'a_{s}'] 1741 b = result.params.valuesdict()[f'b_{s}'] 1742 c = result.params.valuesdict()[f'c_{s}'] 1743 a2 = result.params.valuesdict()[f'a2_{s}'] 1744 b2 = result.params.valuesdict()[f'b2_{s}'] 1745 c2 = result.params.valuesdict()[f'c2_{s}'] 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 1748 self.standardization = result 1749 1750 for session in self.sessions: 1751 self.sessions[session]['Np'] = 3 1752 for k in ['scrambling', 'slope', 'wg']: 1753 if self.sessions[session][f'{k}_drift']: 1754 self.sessions[session]['Np'] += 1 1755 1756 if consolidate: 1757 self.consolidate(tables = consolidate_tables, plots = consolidate_plots) 1758 return result 1759 1760 1761 elif method == 'indep_sessions': 1762 1763 if weighted_sessions: 1764 for session_group in weighted_sessions: 1765 X = D4xdata([r for r in self if r['Session'] in session_group], mass = self._4x) 1766 X.Nominal_D4x = self.Nominal_D4x.copy() 1767 X.refresh() 1768 # This is only done to assign r['wD47raw'] for r in X: 1769 X.standardize(method = method, weighted_sessions = [], consolidate = False) 1770 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}') 1771 else: 1772 self.msg('All weights set to 1 ‰') 1773 for r in self: 1774 r[f'wD{self._4x}raw'] = 1 1775 1776 for session in self.sessions: 1777 s = self.sessions[session] 1778 p_names = ['a', 'b', 'c', 'a2', 'b2', 'c2'] 1779 p_active = [True, True, True, s['scrambling_drift'], s['slope_drift'], s['wg_drift']] 1780 s['Np'] = sum(p_active) 1781 sdata = s['data'] 1782 1783 A = np.array([ 1784 [ 1785 self.Nominal_D4x[r['Sample']] / r[f'wD{self._4x}raw'], 1786 r[f'd{self._4x}'] / r[f'wD{self._4x}raw'], 1787 1 / r[f'wD{self._4x}raw'], 1788 self.Nominal_D4x[r['Sample']] * r['t'] / r[f'wD{self._4x}raw'], 1789 r[f'd{self._4x}'] * r['t'] / r[f'wD{self._4x}raw'], 1790 r['t'] / r[f'wD{self._4x}raw'] 1791 ] 1792 for r in sdata if r['Sample'] in self.anchors 1793 ])[:,p_active] # only keep columns for the active parameters 1794 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]) 1795 s['Na'] = Y.size 1796 CM = linalg.inv(A.T @ A) 1797 bf = (CM @ A.T @ Y).T[0,:] 1798 k = 0 1799 for n,a in zip(p_names, p_active): 1800 if a: 1801 s[n] = bf[k] 1802# self.msg(f'{n} = {bf[k]}') 1803 k += 1 1804 else: 1805 s[n] = 0. 1806# self.msg(f'{n} = 0.0') 1807 1808 for r in sdata : 1809 a, b, c, a2, b2, c2 = s['a'], s['b'], s['c'], s['a2'], s['b2'], s['c2'] 1810 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']) 1811 r[f'wD{self._4x}'] = r[f'wD{self._4x}raw'] / (a + a2 * r['t']) 1812 1813 s['CM'] = np.zeros((6,6)) 1814 i = 0 1815 k_active = [j for j,a in enumerate(p_active) if a] 1816 for j,a in enumerate(p_active): 1817 if a: 1818 s['CM'][j,k_active] = CM[i,:] 1819 i += 1 1820 1821 if not weighted_sessions: 1822 w = self.rmswd()['rmswd'] 1823 for r in self: 1824 r[f'wD{self._4x}'] *= w 1825 r[f'wD{self._4x}raw'] *= w 1826 for session in self.sessions: 1827 self.sessions[session]['CM'] *= w**2 1828 1829 for session in self.sessions: 1830 s = self.sessions[session] 1831 s['SE_a'] = s['CM'][0,0]**.5 1832 s['SE_b'] = s['CM'][1,1]**.5 1833 s['SE_c'] = s['CM'][2,2]**.5 1834 s['SE_a2'] = s['CM'][3,3]**.5 1835 s['SE_b2'] = s['CM'][4,4]**.5 1836 s['SE_c2'] = s['CM'][5,5]**.5 1837 1838 if not weighted_sessions: 1839 self.Nf = len(self) - len(self.unknowns) - np.sum([self.sessions[s]['Np'] for s in self.sessions]) 1840 else: 1841 self.Nf = 0 1842 for sg in weighted_sessions: 1843 self.Nf += self.rmswd(sessions = sg)['Nf'] 1844 1845 self.t95 = tstudent.ppf(1 - 0.05/2, self.Nf) 1846 1847 avgD4x = { 1848 sample: np.mean([r[f'D{self._4x}'] for r in self if r['Sample'] == sample]) 1849 for sample in self.samples 1850 } 1851 chi2 = np.sum([(r[f'D{self._4x}'] - avgD4x[r['Sample']])**2 for r in self]) 1852 rD4x = (chi2/self.Nf)**.5 1853 self.repeatability[f'sigma_{self._4x}'] = rD4x 1854 1855 if consolidate: 1856 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.
1859 def standardization_error(self, session, d4x, D4x, t = 0): 1860 ''' 1861 Compute standardization error for a given session and 1862 (δ47, Δ47) composition. 1863 ''' 1864 a = self.sessions[session]['a'] 1865 b = self.sessions[session]['b'] 1866 c = self.sessions[session]['c'] 1867 a2 = self.sessions[session]['a2'] 1868 b2 = self.sessions[session]['b2'] 1869 c2 = self.sessions[session]['c2'] 1870 CM = self.sessions[session]['CM'] 1871 1872 x, y = D4x, d4x 1873 z = a * x + b * y + c + a2 * x * t + b2 * y * t + c2 * t 1874# x = (z - b*y - b2*y*t - c - c2*t) / (a+a2*t) 1875 dxdy = -(b+b2*t) / (a+a2*t) 1876 dxdz = 1. / (a+a2*t) 1877 dxda = -x / (a+a2*t) 1878 dxdb = -y / (a+a2*t) 1879 dxdc = -1. / (a+a2*t) 1880 dxda2 = -x * a2 / (a+a2*t) 1881 dxdb2 = -y * t / (a+a2*t) 1882 dxdc2 = -t / (a+a2*t) 1883 V = np.array([dxda, dxdb, dxdc, dxda2, dxdb2, dxdc2]) 1884 sx = (V @ CM @ V.T) ** .5 1885 return sx
Compute standardization error for a given session and (δ47, Δ47) composition.
1888 @make_verbal 1889 def summary(self, 1890 dir = 'output', 1891 filename = None, 1892 save_to_file = True, 1893 print_out = True, 1894 ): 1895 ''' 1896 Print out an/or save to disk a summary of the standardization results. 1897 1898 **Parameters** 1899 1900 + `dir`: the directory in which to save the table 1901 + `filename`: the name to the csv file to write to 1902 + `save_to_file`: whether to save the table to disk 1903 + `print_out`: whether to print out the table 1904 ''' 1905 1906 out = [] 1907 out += [['N samples (anchors + unknowns)', f"{len(self.samples)} ({len(self.anchors)} + {len(self.unknowns)})"]] 1908 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])})"]] 1909 out += [['Repeatability of δ13C_VPDB', f"{1000 * self.repeatability['r_d13C_VPDB']:.1f} ppm"]] 1910 out += [['Repeatability of δ18O_VSMOW', f"{1000 * self.repeatability['r_d18O_VSMOW']:.1f} ppm"]] 1911 out += [[f'Repeatability of Δ{self._4x} (anchors)', f"{1000 * self.repeatability[f'r_D{self._4x}a']:.1f} ppm"]] 1912 out += [[f'Repeatability of Δ{self._4x} (unknowns)', f"{1000 * self.repeatability[f'r_D{self._4x}u']:.1f} ppm"]] 1913 out += [[f'Repeatability of Δ{self._4x} (all)', f"{1000 * self.repeatability[f'r_D{self._4x}']:.1f} ppm"]] 1914 out += [['Model degrees of freedom', f"{self.Nf}"]] 1915 out += [['Student\'s 95% t-factor', f"{self.t95:.2f}"]] 1916 out += [['Standardization method', self.standardization_method]] 1917 1918 if save_to_file: 1919 if not os.path.exists(dir): 1920 os.makedirs(dir) 1921 if filename is None: 1922 filename = f'D{self._4x}_summary.csv' 1923 with open(f'{dir}/{filename}', 'w') as fid: 1924 fid.write(make_csv(out)) 1925 if print_out: 1926 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
1929 @make_verbal 1930 def table_of_sessions(self, 1931 dir = 'output', 1932 filename = None, 1933 save_to_file = True, 1934 print_out = True, 1935 output = None, 1936 ): 1937 ''' 1938 Print out an/or save to disk a table of sessions. 1939 1940 **Parameters** 1941 1942 + `dir`: the directory in which to save the table 1943 + `filename`: the name to the csv file to write to 1944 + `save_to_file`: whether to save the table to disk 1945 + `print_out`: whether to print out the table 1946 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 1947 if set to `'raw'`: return a list of list of strings 1948 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 1949 ''' 1950 include_a2 = any([self.sessions[session]['scrambling_drift'] for session in self.sessions]) 1951 include_b2 = any([self.sessions[session]['slope_drift'] for session in self.sessions]) 1952 include_c2 = any([self.sessions[session]['wg_drift'] for session in self.sessions]) 1953 1954 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']] 1955 if include_a2: 1956 out[-1] += ['a2 ± SE'] 1957 if include_b2: 1958 out[-1] += ['b2 ± SE'] 1959 if include_c2: 1960 out[-1] += ['c2 ± SE'] 1961 for session in self.sessions: 1962 out += [[ 1963 session, 1964 f"{self.sessions[session]['Na']}", 1965 f"{self.sessions[session]['Nu']}", 1966 f"{self.sessions[session]['d13Cwg_VPDB']:.3f}", 1967 f"{self.sessions[session]['d18Owg_VSMOW']:.3f}", 1968 f"{self.sessions[session]['r_d13C_VPDB']:.4f}", 1969 f"{self.sessions[session]['r_d18O_VSMOW']:.4f}", 1970 f"{self.sessions[session][f'r_D{self._4x}']:.4f}", 1971 f"{self.sessions[session]['a']:.3f} ± {self.sessions[session]['SE_a']:.3f}", 1972 f"{1e3*self.sessions[session]['b']:.3f} ± {1e3*self.sessions[session]['SE_b']:.3f}", 1973 f"{self.sessions[session]['c']:.3f} ± {self.sessions[session]['SE_c']:.3f}", 1974 ]] 1975 if include_a2: 1976 if self.sessions[session]['scrambling_drift']: 1977 out[-1] += [f"{self.sessions[session]['a2']:.1e} ± {self.sessions[session]['SE_a2']:.1e}"] 1978 else: 1979 out[-1] += [''] 1980 if include_b2: 1981 if self.sessions[session]['slope_drift']: 1982 out[-1] += [f"{self.sessions[session]['b2']:.1e} ± {self.sessions[session]['SE_b2']:.1e}"] 1983 else: 1984 out[-1] += [''] 1985 if include_c2: 1986 if self.sessions[session]['wg_drift']: 1987 out[-1] += [f"{self.sessions[session]['c2']:.1e} ± {self.sessions[session]['SE_c2']:.1e}"] 1988 else: 1989 out[-1] += [''] 1990 1991 if save_to_file: 1992 if not os.path.exists(dir): 1993 os.makedirs(dir) 1994 if filename is None: 1995 filename = f'D{self._4x}_sessions.csv' 1996 with open(f'{dir}/{filename}', 'w') as fid: 1997 fid.write(make_csv(out)) 1998 if print_out: 1999 self.msg('\n' + pretty_table(out)) 2000 if output == 'raw': 2001 return out 2002 elif output == 'pretty': 2003 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']]
)
2006 @make_verbal 2007 def table_of_analyses( 2008 self, 2009 dir = 'output', 2010 filename = None, 2011 save_to_file = True, 2012 print_out = True, 2013 output = None, 2014 ): 2015 ''' 2016 Print out an/or save to disk a table of analyses. 2017 2018 **Parameters** 2019 2020 + `dir`: the directory in which to save the table 2021 + `filename`: the name to the csv file to write to 2022 + `save_to_file`: whether to save the table to disk 2023 + `print_out`: whether to print out the table 2024 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2025 if set to `'raw'`: return a list of list of strings 2026 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2027 ''' 2028 2029 out = [['UID','Session','Sample']] 2030 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}] 2031 for f in extra_fields: 2032 out[-1] += [f[0]] 2033 out[-1] += ['d13Cwg_VPDB','d18Owg_VSMOW','d45','d46','d47','d48','d49','d13C_VPDB','d18O_VSMOW','D47raw','D48raw','D49raw',f'D{self._4x}'] 2034 for r in self: 2035 out += [[f"{r['UID']}",f"{r['Session']}",f"{r['Sample']}"]] 2036 for f in extra_fields: 2037 out[-1] += [f"{r[f[0]]:{f[1]}}"] 2038 out[-1] += [ 2039 f"{r['d13Cwg_VPDB']:.3f}", 2040 f"{r['d18Owg_VSMOW']:.3f}", 2041 f"{r['d45']:.6f}", 2042 f"{r['d46']:.6f}", 2043 f"{r['d47']:.6f}", 2044 f"{r['d48']:.6f}", 2045 f"{r['d49']:.6f}", 2046 f"{r['d13C_VPDB']:.6f}", 2047 f"{r['d18O_VSMOW']:.6f}", 2048 f"{r['D47raw']:.6f}", 2049 f"{r['D48raw']:.6f}", 2050 f"{r['D49raw']:.6f}", 2051 f"{r[f'D{self._4x}']:.6f}" 2052 ] 2053 if save_to_file: 2054 if not os.path.exists(dir): 2055 os.makedirs(dir) 2056 if filename is None: 2057 filename = f'D{self._4x}_analyses.csv' 2058 with open(f'{dir}/{filename}', 'w') as fid: 2059 fid.write(make_csv(out)) 2060 if print_out: 2061 self.msg('\n' + pretty_table(out)) 2062 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']]
)
2064 @make_verbal 2065 def covar_table( 2066 self, 2067 correl = False, 2068 dir = 'output', 2069 filename = None, 2070 save_to_file = True, 2071 print_out = True, 2072 output = None, 2073 ): 2074 ''' 2075 Print out, save to disk and/or return the variance-covariance matrix of D4x 2076 for all unknown samples. 2077 2078 **Parameters** 2079 2080 + `dir`: the directory in which to save the csv 2081 + `filename`: the name of the csv file to write to 2082 + `save_to_file`: whether to save the csv 2083 + `print_out`: whether to print out the matrix 2084 + `output`: if set to `'pretty'`: return a pretty text matrix (see `pretty_table()`); 2085 if set to `'raw'`: return a list of list of strings 2086 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2087 ''' 2088 samples = sorted([u for u in self.unknowns]) 2089 out = [[''] + samples] 2090 for s1 in samples: 2091 out.append([s1]) 2092 for s2 in samples: 2093 if correl: 2094 out[-1].append(f'{self.sample_D4x_correl(s1, s2):.6f}') 2095 else: 2096 out[-1].append(f'{self.sample_D4x_covar(s1, s2):.8e}') 2097 2098 if save_to_file: 2099 if not os.path.exists(dir): 2100 os.makedirs(dir) 2101 if filename is None: 2102 if correl: 2103 filename = f'D{self._4x}_correl.csv' 2104 else: 2105 filename = f'D{self._4x}_covar.csv' 2106 with open(f'{dir}/{filename}', 'w') as fid: 2107 fid.write(make_csv(out)) 2108 if print_out: 2109 self.msg('\n'+pretty_table(out)) 2110 if output == 'raw': 2111 return out 2112 elif output == 'pretty': 2113 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']]
)
2115 @make_verbal 2116 def table_of_samples( 2117 self, 2118 dir = 'output', 2119 filename = None, 2120 save_to_file = True, 2121 print_out = True, 2122 output = None, 2123 ): 2124 ''' 2125 Print out, save to disk and/or return a table of samples. 2126 2127 **Parameters** 2128 2129 + `dir`: the directory in which to save the csv 2130 + `filename`: the name of the csv file to write to 2131 + `save_to_file`: whether to save the csv 2132 + `print_out`: whether to print out the table 2133 + `output`: if set to `'pretty'`: return a pretty text table (see `pretty_table()`); 2134 if set to `'raw'`: return a list of list of strings 2135 (e.g., `[['header1', 'header2'], ['0.1', '0.2']]`) 2136 ''' 2137 2138 out = [['Sample','N','d13C_VPDB','d18O_VSMOW',f'D{self._4x}','SE','95% CL','SD','p_Levene']] 2139 for sample in self.anchors: 2140 out += [[ 2141 f"{sample}", 2142 f"{self.samples[sample]['N']}", 2143 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2144 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2145 f"{self.samples[sample][f'D{self._4x}']:.4f}",'','', 2146 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', '' 2147 ]] 2148 for sample in self.unknowns: 2149 out += [[ 2150 f"{sample}", 2151 f"{self.samples[sample]['N']}", 2152 f"{self.samples[sample]['d13C_VPDB']:.2f}", 2153 f"{self.samples[sample]['d18O_VSMOW']:.2f}", 2154 f"{self.samples[sample][f'D{self._4x}']:.4f}", 2155 f"{self.samples[sample][f'SE_D{self._4x}']:.4f}", 2156 f"± {self.samples[sample][f'SE_D{self._4x}'] * self.t95:.4f}", 2157 f"{self.samples[sample][f'SD_D{self._4x}']:.4f}" if self.samples[sample]['N'] > 1 else '', 2158 f"{self.samples[sample]['p_Levene']:.3f}" if self.samples[sample]['N'] > 2 else '' 2159 ]] 2160 if save_to_file: 2161 if not os.path.exists(dir): 2162 os.makedirs(dir) 2163 if filename is None: 2164 filename = f'D{self._4x}_samples.csv' 2165 with open(f'{dir}/{filename}', 'w') as fid: 2166 fid.write(make_csv(out)) 2167 if print_out: 2168 self.msg('\n'+pretty_table(out)) 2169 if output == 'raw': 2170 return out 2171 elif output == 'pretty': 2172 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']]
)
2175 def plot_sessions(self, dir = 'output', figsize = (8,8)): 2176 ''' 2177 Generate session plots and save them to disk. 2178 2179 **Parameters** 2180 2181 + `dir`: the directory in which to save the plots 2182 + `figsize`: the width and height (in inches) of each plot 2183 ''' 2184 if not os.path.exists(dir): 2185 os.makedirs(dir) 2186 2187 for session in self.sessions: 2188 sp = self.plot_single_session(session, xylimits = 'constant') 2189 ppl.savefig(f'{dir}/D{self._4x}_plot_{session}.pdf') 2190 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
2193 @make_verbal 2194 def consolidate_samples(self): 2195 ''' 2196 Compile various statistics for each sample. 2197 2198 For each anchor sample: 2199 2200 + `D47` or `D48`: the nominal Δ4x value for this anchor, specified by `self.Nominal_D4x` 2201 + `SE_D47` or `SE_D48`: set to zero by definition 2202 2203 For each unknown sample: 2204 2205 + `D47` or `D48`: the standardized Δ4x value for this unknown 2206 + `SE_D47` or `SE_D48`: the standard error of Δ4x for this unknown 2207 2208 For each anchor and unknown: 2209 2210 + `N`: the total number of analyses of this sample 2211 + `SD_D47` or `SD_D48`: the “sample” (in the statistical sense) standard deviation for this sample 2212 + `d13C_VPDB`: the average δ13C_VPDB value for this sample 2213 + `d18O_VSMOW`: the average δ18O_VSMOW value for this sample (as CO2) 2214 + `p_Levene`: the p-value from a [Levene test](https://en.wikipedia.org/wiki/Levene%27s_test) of equal 2215 variance, indicating whether the Δ4x repeatability this sample differs significantly from 2216 that observed for the reference sample specified by `self.LEVENE_REF_SAMPLE`. 2217 ''' 2218 D4x_ref_pop = [r[f'D{self._4x}'] for r in self.samples[self.LEVENE_REF_SAMPLE]['data']] 2219 for sample in self.samples: 2220 self.samples[sample]['N'] = len(self.samples[sample]['data']) 2221 if self.samples[sample]['N'] > 1: 2222 self.samples[sample][f'SD_D{self._4x}'] = stdev([r[f'D{self._4x}'] for r in self.samples[sample]['data']]) 2223 2224 self.samples[sample]['d13C_VPDB'] = np.mean([r['d13C_VPDB'] for r in self.samples[sample]['data']]) 2225 self.samples[sample]['d18O_VSMOW'] = np.mean([r['d18O_VSMOW'] for r in self.samples[sample]['data']]) 2226 2227 D4x_pop = [r[f'D{self._4x}'] for r in self.samples[sample]['data']] 2228 if len(D4x_pop) > 2: 2229 self.samples[sample]['p_Levene'] = levene(D4x_ref_pop, D4x_pop, center = 'median')[1] 2230 2231 if self.standardization_method == 'pooled': 2232 for sample in self.anchors: 2233 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2234 self.samples[sample][f'SE_D{self._4x}'] = 0. 2235 for sample in self.unknowns: 2236 self.samples[sample][f'D{self._4x}'] = self.standardization.params.valuesdict()[f'D{self._4x}_{pf(sample)}'] 2237 try: 2238 self.samples[sample][f'SE_D{self._4x}'] = self.sample_D4x_covar(sample)**.5 2239 except ValueError: 2240 # when `sample` is constrained by self.standardize(constraints = {...}), 2241 # it is no longer listed in self.standardization.var_names. 2242 # Temporary fix: define SE as zero for now 2243 self.samples[sample][f'SE_D4{self._4x}'] = 0. 2244 2245 elif self.standardization_method == 'indep_sessions': 2246 for sample in self.anchors: 2247 self.samples[sample][f'D{self._4x}'] = self.Nominal_D4x[sample] 2248 self.samples[sample][f'SE_D{self._4x}'] = 0. 2249 for sample in self.unknowns: 2250 self.msg(f'Consolidating sample {sample}') 2251 self.unknowns[sample][f'session_D{self._4x}'] = {} 2252 session_avg = [] 2253 for session in self.sessions: 2254 sdata = [r for r in self.sessions[session]['data'] if r['Sample'] == sample] 2255 if sdata: 2256 self.msg(f'{sample} found in session {session}') 2257 avg_D4x = np.mean([r[f'D{self._4x}'] for r in sdata]) 2258 avg_d4x = np.mean([r[f'd{self._4x}'] for r in sdata]) 2259 # !! TODO: sigma_s below does not account for temporal changes in standardization error 2260 sigma_s = self.standardization_error(session, avg_d4x, avg_D4x) 2261 sigma_u = sdata[0][f'wD{self._4x}raw'] / self.sessions[session]['a'] / len(sdata)**.5 2262 session_avg.append([avg_D4x, (sigma_u**2 + sigma_s**2)**.5]) 2263 self.unknowns[sample][f'session_D{self._4x}'][session] = session_avg[-1] 2264 self.samples[sample][f'D{self._4x}'], self.samples[sample][f'SE_D{self._4x}'] = w_avg(*zip(*session_avg)) 2265 weights = {s: self.unknowns[sample][f'session_D{self._4x}'][s][1]**-2 for s in self.unknowns[sample][f'session_D{self._4x}']} 2266 wsum = sum([weights[s] for s in weights]) 2267 for s in weights: 2268 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
.
2271 def consolidate_sessions(self): 2272 ''' 2273 Compute various statistics for each session. 2274 2275 + `Na`: Number of anchor analyses in the session 2276 + `Nu`: Number of unknown analyses in the session 2277 + `r_d13C_VPDB`: δ13C_VPDB repeatability of analyses within the session 2278 + `r_d18O_VSMOW`: δ18O_VSMOW repeatability of analyses within the session 2279 + `r_D47` or `r_D48`: Δ4x repeatability of analyses within the session 2280 + `a`: scrambling factor 2281 + `b`: compositional slope 2282 + `c`: WG offset 2283 + `SE_a`: Model stadard erorr of `a` 2284 + `SE_b`: Model stadard erorr of `b` 2285 + `SE_c`: Model stadard erorr of `c` 2286 + `scrambling_drift` (boolean): whether to allow a temporal drift in the scrambling factor (`a`) 2287 + `slope_drift` (boolean): whether to allow a temporal drift in the compositional slope (`b`) 2288 + `wg_drift` (boolean): whether to allow a temporal drift in the WG offset (`c`) 2289 + `a2`: scrambling factor drift 2290 + `b2`: compositional slope drift 2291 + `c2`: WG offset drift 2292 + `Np`: Number of standardization parameters to fit 2293 + `CM`: model covariance matrix for (`a`, `b`, `c`, `a2`, `b2`, `c2`) 2294 + `d13Cwg_VPDB`: δ13C_VPDB of WG 2295 + `d18Owg_VSMOW`: δ18O_VSMOW of WG 2296 ''' 2297 for session in self.sessions: 2298 if 'd13Cwg_VPDB' not in self.sessions[session]: 2299 self.sessions[session]['d13Cwg_VPDB'] = self.sessions[session]['data'][0]['d13Cwg_VPDB'] 2300 if 'd18Owg_VSMOW' not in self.sessions[session]: 2301 self.sessions[session]['d18Owg_VSMOW'] = self.sessions[session]['data'][0]['d18Owg_VSMOW'] 2302 self.sessions[session]['Na'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.anchors]) 2303 self.sessions[session]['Nu'] = len([r for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns]) 2304 2305 self.msg(f'Computing repeatabilities for session {session}') 2306 self.sessions[session]['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors', sessions = [session]) 2307 self.sessions[session]['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors', sessions = [session]) 2308 self.sessions[session][f'r_D{self._4x}'] = self.compute_r(f'D{self._4x}', sessions = [session]) 2309 2310 if self.standardization_method == 'pooled': 2311 for session in self.sessions: 2312 2313 self.sessions[session]['a'] = self.standardization.params.valuesdict()[f'a_{pf(session)}'] 2314 i = self.standardization.var_names.index(f'a_{pf(session)}') 2315 self.sessions[session]['SE_a'] = self.standardization.covar[i,i]**.5 2316 2317 self.sessions[session]['b'] = self.standardization.params.valuesdict()[f'b_{pf(session)}'] 2318 i = self.standardization.var_names.index(f'b_{pf(session)}') 2319 self.sessions[session]['SE_b'] = self.standardization.covar[i,i]**.5 2320 2321 self.sessions[session]['c'] = self.standardization.params.valuesdict()[f'c_{pf(session)}'] 2322 i = self.standardization.var_names.index(f'c_{pf(session)}') 2323 self.sessions[session]['SE_c'] = self.standardization.covar[i,i]**.5 2324 2325 self.sessions[session]['a2'] = self.standardization.params.valuesdict()[f'a2_{pf(session)}'] 2326 if self.sessions[session]['scrambling_drift']: 2327 i = self.standardization.var_names.index(f'a2_{pf(session)}') 2328 self.sessions[session]['SE_a2'] = self.standardization.covar[i,i]**.5 2329 else: 2330 self.sessions[session]['SE_a2'] = 0. 2331 2332 self.sessions[session]['b2'] = self.standardization.params.valuesdict()[f'b2_{pf(session)}'] 2333 if self.sessions[session]['slope_drift']: 2334 i = self.standardization.var_names.index(f'b2_{pf(session)}') 2335 self.sessions[session]['SE_b2'] = self.standardization.covar[i,i]**.5 2336 else: 2337 self.sessions[session]['SE_b2'] = 0. 2338 2339 self.sessions[session]['c2'] = self.standardization.params.valuesdict()[f'c2_{pf(session)}'] 2340 if self.sessions[session]['wg_drift']: 2341 i = self.standardization.var_names.index(f'c2_{pf(session)}') 2342 self.sessions[session]['SE_c2'] = self.standardization.covar[i,i]**.5 2343 else: 2344 self.sessions[session]['SE_c2'] = 0. 2345 2346 i = self.standardization.var_names.index(f'a_{pf(session)}') 2347 j = self.standardization.var_names.index(f'b_{pf(session)}') 2348 k = self.standardization.var_names.index(f'c_{pf(session)}') 2349 CM = np.zeros((6,6)) 2350 CM[:3,:3] = self.standardization.covar[[i,j,k],:][:,[i,j,k]] 2351 try: 2352 i2 = self.standardization.var_names.index(f'a2_{pf(session)}') 2353 CM[3,[0,1,2,3]] = self.standardization.covar[i2,[i,j,k,i2]] 2354 CM[[0,1,2,3],3] = self.standardization.covar[[i,j,k,i2],i2] 2355 try: 2356 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2357 CM[3,4] = self.standardization.covar[i2,j2] 2358 CM[4,3] = self.standardization.covar[j2,i2] 2359 except ValueError: 2360 pass 2361 try: 2362 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2363 CM[3,5] = self.standardization.covar[i2,k2] 2364 CM[5,3] = self.standardization.covar[k2,i2] 2365 except ValueError: 2366 pass 2367 except ValueError: 2368 pass 2369 try: 2370 j2 = self.standardization.var_names.index(f'b2_{pf(session)}') 2371 CM[4,[0,1,2,4]] = self.standardization.covar[j2,[i,j,k,j2]] 2372 CM[[0,1,2,4],4] = self.standardization.covar[[i,j,k,j2],j2] 2373 try: 2374 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2375 CM[4,5] = self.standardization.covar[j2,k2] 2376 CM[5,4] = self.standardization.covar[k2,j2] 2377 except ValueError: 2378 pass 2379 except ValueError: 2380 pass 2381 try: 2382 k2 = self.standardization.var_names.index(f'c2_{pf(session)}') 2383 CM[5,[0,1,2,5]] = self.standardization.covar[k2,[i,j,k,k2]] 2384 CM[[0,1,2,5],5] = self.standardization.covar[[i,j,k,k2],k2] 2385 except ValueError: 2386 pass 2387 2388 self.sessions[session]['CM'] = CM 2389 2390 elif self.standardization_method == 'indep_sessions': 2391 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
2394 @make_verbal 2395 def repeatabilities(self): 2396 ''' 2397 Compute analytical repeatabilities for δ13C_VPDB, δ18O_VSMOW, Δ4x 2398 (for all samples, for anchors, and for unknowns). 2399 ''' 2400 self.msg('Computing reproducibilities for all sessions') 2401 2402 self.repeatability['r_d13C_VPDB'] = self.compute_r('d13C_VPDB', samples = 'anchors') 2403 self.repeatability['r_d18O_VSMOW'] = self.compute_r('d18O_VSMOW', samples = 'anchors') 2404 self.repeatability[f'r_D{self._4x}a'] = self.compute_r(f'D{self._4x}', samples = 'anchors') 2405 self.repeatability[f'r_D{self._4x}u'] = self.compute_r(f'D{self._4x}', samples = 'unknowns') 2406 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).
2409 @make_verbal 2410 def consolidate(self, tables = True, plots = True): 2411 ''' 2412 Collect information about samples, sessions and repeatabilities. 2413 ''' 2414 self.consolidate_samples() 2415 self.consolidate_sessions() 2416 self.repeatabilities() 2417 2418 if tables: 2419 self.summary() 2420 self.table_of_sessions() 2421 self.table_of_analyses() 2422 self.table_of_samples() 2423 2424 if plots: 2425 self.plot_sessions()
Collect information about samples, sessions and repeatabilities.
2428 @make_verbal 2429 def rmswd(self, 2430 samples = 'all samples', 2431 sessions = 'all sessions', 2432 ): 2433 ''' 2434 Compute the χ2, root mean squared weighted deviation 2435 (i.e. reduced χ2), and corresponding degrees of freedom of the 2436 Δ4x values for samples in `samples` and sessions in `sessions`. 2437 2438 Only used in `D4xdata.standardize()` with `method='indep_sessions'`. 2439 ''' 2440 if samples == 'all samples': 2441 mysamples = [k for k in self.samples] 2442 elif samples == 'anchors': 2443 mysamples = [k for k in self.anchors] 2444 elif samples == 'unknowns': 2445 mysamples = [k for k in self.unknowns] 2446 else: 2447 mysamples = samples 2448 2449 if sessions == 'all sessions': 2450 sessions = [k for k in self.sessions] 2451 2452 chisq, Nf = 0, 0 2453 for sample in mysamples : 2454 G = [ r for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2455 if len(G) > 1 : 2456 X, sX = w_avg([r[f'D{self._4x}'] for r in G], [r[f'wD{self._4x}'] for r in G]) 2457 Nf += (len(G) - 1) 2458 chisq += np.sum([ ((r[f'D{self._4x}']-X)/r[f'wD{self._4x}'])**2 for r in G]) 2459 r = (chisq / Nf)**.5 if Nf > 0 else 0 2460 self.msg(f'RMSWD of r["D{self._4x}"] is {r:.6f} for {samples}.') 2461 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'
.
2464 @make_verbal 2465 def compute_r(self, key, samples = 'all samples', sessions = 'all sessions'): 2466 ''' 2467 Compute the repeatability of `[r[key] for r in self]` 2468 ''' 2469 # NB: it's debatable whether rD47 should be computed 2470 # with Nf = len(self)-len(self.samples) instead of 2471 # Nf = len(self) - len(self.unknwons) - 3*len(self.sessions) 2472 2473 if samples == 'all samples': 2474 mysamples = [k for k in self.samples] 2475 elif samples == 'anchors': 2476 mysamples = [k for k in self.anchors] 2477 elif samples == 'unknowns': 2478 mysamples = [k for k in self.unknowns] 2479 else: 2480 mysamples = samples 2481 2482 if sessions == 'all sessions': 2483 sessions = [k for k in self.sessions] 2484 2485 if key in ['D47', 'D48']: 2486 chisq, Nf = 0, 0 2487 for sample in mysamples : 2488 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2489 if len(X) > 1 : 2490 chisq += np.sum([ (x-self.samples[sample][key])**2 for x in X ]) 2491 if sample in self.unknowns: 2492 Nf += len(X) - 1 2493 else: 2494 Nf += len(X) 2495 if samples in ['anchors', 'all samples']: 2496 Nf -= sum([self.sessions[s]['Np'] for s in sessions]) 2497 r = (chisq / Nf)**.5 if Nf > 0 else 0 2498 2499 else: # if key not in ['D47', 'D48'] 2500 chisq, Nf = 0, 0 2501 for sample in mysamples : 2502 X = [ r[key] for r in self if r['Sample'] == sample and r['Session'] in sessions ] 2503 if len(X) > 1 : 2504 Nf += len(X) - 1 2505 chisq += np.sum([ (x-np.mean(X))**2 for x in X ]) 2506 r = (chisq / Nf)**.5 if Nf > 0 else 0 2507 2508 self.msg(f'Repeatability of r["{key}"] is {1000*r:.1f} ppm for {samples}.') 2509 return r
Compute the repeatability of [r[key] for r in self]
2511 def sample_average(self, samples, weights = 'equal', normalize = True): 2512 ''' 2513 Weighted average Δ4x value of a group of samples, accounting for covariance. 2514 2515 Returns the weighed average Δ4x value and associated SE 2516 of a group of samples. Weights are equal by default. If `normalize` is 2517 true, `weights` will be rescaled so that their sum equals 1. 2518 2519 **Examples** 2520 2521 ```python 2522 self.sample_average(['X','Y'], [1, 2]) 2523 ``` 2524 2525 returns the value and SE of [Δ4x(X) + 2 Δ4x(Y)]/3, 2526 where Δ4x(X) and Δ4x(Y) are the average Δ4x 2527 values of samples X and Y, respectively. 2528 2529 ```python 2530 self.sample_average(['X','Y'], [1, -1], normalize = False) 2531 ``` 2532 2533 returns the value and SE of the difference Δ4x(X) - Δ4x(Y). 2534 ''' 2535 if weights == 'equal': 2536 weights = [1/len(samples)] * len(samples) 2537 2538 if normalize: 2539 s = sum(weights) 2540 if s: 2541 weights = [w/s for w in weights] 2542 2543 try: 2544# indices = [self.standardization.var_names.index(f'D47_{pf(sample)}') for sample in samples] 2545# C = self.standardization.covar[indices,:][:,indices] 2546 C = np.array([[self.sample_D4x_covar(x, y) for x in samples] for y in samples]) 2547 X = [self.samples[sample][f'D{self._4x}'] for sample in samples] 2548 return correlated_sum(X, C, weights) 2549 except ValueError: 2550 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).
2553 def sample_D4x_covar(self, sample1, sample2 = None): 2554 ''' 2555 Covariance between Δ4x values of samples 2556 2557 Returns the error covariance between the average Δ4x values of two 2558 samples. If if only `sample_1` is specified, or if `sample_1 == sample_2`), 2559 returns the Δ4x variance for that sample. 2560 ''' 2561 if sample2 is None: 2562 sample2 = sample1 2563 if self.standardization_method == 'pooled': 2564 i = self.standardization.var_names.index(f'D{self._4x}_{pf(sample1)}') 2565 j = self.standardization.var_names.index(f'D{self._4x}_{pf(sample2)}') 2566 return self.standardization.covar[i, j] 2567 elif self.standardization_method == 'indep_sessions': 2568 if sample1 == sample2: 2569 return self.samples[sample1][f'SE_D{self._4x}']**2 2570 else: 2571 c = 0 2572 for session in self.sessions: 2573 sdata1 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample1] 2574 sdata2 = [r for r in self.sessions[session]['data'] if r['Sample'] == sample2] 2575 if sdata1 and sdata2: 2576 a = self.sessions[session]['a'] 2577 # !! TODO: CM below does not account for temporal changes in standardization parameters 2578 CM = self.sessions[session]['CM'][:3,:3] 2579 avg_D4x_1 = np.mean([r[f'D{self._4x}'] for r in sdata1]) 2580 avg_d4x_1 = np.mean([r[f'd{self._4x}'] for r in sdata1]) 2581 avg_D4x_2 = np.mean([r[f'D{self._4x}'] for r in sdata2]) 2582 avg_d4x_2 = np.mean([r[f'd{self._4x}'] for r in sdata2]) 2583 c += ( 2584 self.unknowns[sample1][f'session_D{self._4x}'][session][2] 2585 * self.unknowns[sample2][f'session_D{self._4x}'][session][2] 2586 * np.array([[avg_D4x_1, avg_d4x_1, 1]]) 2587 @ CM 2588 @ np.array([[avg_D4x_2, avg_d4x_2, 1]]).T 2589 ) / a**2 2590 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.
2592 def sample_D4x_correl(self, sample1, sample2 = None): 2593 ''' 2594 Correlation between Δ4x errors of samples 2595 2596 Returns the error correlation between the average Δ4x values of two samples. 2597 ''' 2598 if sample2 is None or sample2 == sample1: 2599 return 1. 2600 return ( 2601 self.sample_D4x_covar(sample1, sample2) 2602 / self.unknowns[sample1][f'SE_D{self._4x}'] 2603 / self.unknowns[sample2][f'SE_D{self._4x}'] 2604 )
Correlation between Δ4x errors of samples
Returns the error correlation between the average Δ4x values of two samples.
2606 def plot_single_session(self, 2607 session, 2608 kw_plot_anchors = dict(ls='None', marker='x', mec=(.75, 0, 0), mew = .75, ms = 4), 2609 kw_plot_unknowns = dict(ls='None', marker='x', mec=(0, 0, .75), mew = .75, ms = 4), 2610 kw_plot_anchor_avg = dict(ls='-', marker='None', color=(.75, 0, 0), lw = .75), 2611 kw_plot_unknown_avg = dict(ls='-', marker='None', color=(0, 0, .75), lw = .75), 2612 kw_contour_error = dict(colors = [[0, 0, 0]], alpha = .5, linewidths = 0.75), 2613 xylimits = 'free', # | 'constant' 2614 x_label = None, 2615 y_label = None, 2616 error_contour_interval = 'auto', 2617 fig = 'new', 2618 ): 2619 ''' 2620 Generate plot for a single session 2621 ''' 2622 if x_label is None: 2623 x_label = f'δ$_{{{self._4x}}}$ (‰)' 2624 if y_label is None: 2625 y_label = f'Δ$_{{{self._4x}}}$ (‰)' 2626 2627 out = _SessionPlot() 2628 anchors = [a for a in self.anchors if [r for r in self.sessions[session]['data'] if r['Sample'] == a]] 2629 unknowns = [u for u in self.unknowns if [r for r in self.sessions[session]['data'] if r['Sample'] == u]] 2630 2631 if fig == 'new': 2632 out.fig = ppl.figure(figsize = (6,6)) 2633 ppl.subplots_adjust(.1,.1,.9,.9) 2634 2635 out.anchor_analyses, = ppl.plot( 2636 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2637 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.anchors], 2638 **kw_plot_anchors) 2639 out.unknown_analyses, = ppl.plot( 2640 [r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2641 [r[f'D{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] in self.unknowns], 2642 **kw_plot_unknowns) 2643 out.anchor_avg = ppl.plot( 2644 np.array([ np.array([ 2645 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2646 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2647 ]) for sample in anchors]).T, 2648 np.array([ np.array([0, 0]) + self.Nominal_D4x[sample] for sample in anchors]).T, 2649 **kw_plot_anchor_avg) 2650 out.unknown_avg = ppl.plot( 2651 np.array([ np.array([ 2652 np.min([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) - 1, 2653 np.max([r[f'd{self._4x}'] for r in self.sessions[session]['data'] if r['Sample'] == sample]) + 1 2654 ]) for sample in unknowns]).T, 2655 np.array([ np.array([0, 0]) + self.unknowns[sample][f'D{self._4x}'] for sample in unknowns]).T, 2656 **kw_plot_unknown_avg) 2657 if xylimits == 'constant': 2658 x = [r[f'd{self._4x}'] for r in self] 2659 y = [r[f'D{self._4x}'] for r in self] 2660 x1, x2, y1, y2 = np.min(x), np.max(x), np.min(y), np.max(y) 2661 w, h = x2-x1, y2-y1 2662 x1 -= w/20 2663 x2 += w/20 2664 y1 -= h/20 2665 y2 += h/20 2666 ppl.axis([x1, x2, y1, y2]) 2667 elif xylimits == 'free': 2668 x1, x2, y1, y2 = ppl.axis() 2669 else: 2670 x1, x2, y1, y2 = ppl.axis(xylimits) 2671 2672 if error_contour_interval != 'none': 2673 xi, yi = np.linspace(x1, x2), np.linspace(y1, y2) 2674 XI,YI = np.meshgrid(xi, yi) 2675 SI = np.array([[self.standardization_error(session, x, y) for x in xi] for y in yi]) 2676 if error_contour_interval == 'auto': 2677 rng = np.max(SI) - np.min(SI) 2678 if rng <= 0.01: 2679 cinterval = 0.001 2680 elif rng <= 0.03: 2681 cinterval = 0.004 2682 elif rng <= 0.1: 2683 cinterval = 0.01 2684 elif rng <= 0.3: 2685 cinterval = 0.03 2686 elif rng <= 1.: 2687 cinterval = 0.1 2688 else: 2689 cinterval = 0.5 2690 else: 2691 cinterval = error_contour_interval 2692 2693 cval = np.arange(np.ceil(SI.min() / .001) * .001, np.ceil(SI.max() / .001 + 1) * .001, cinterval) 2694 out.contour = ppl.contour(XI, YI, SI, cval, **kw_contour_error) 2695 out.clabel = ppl.clabel(out.contour) 2696 2697 ppl.xlabel(x_label) 2698 ppl.ylabel(y_label) 2699 ppl.title(session, weight = 'bold') 2700 ppl.grid(alpha = .2) 2701 out.ax = ppl.gca() 2702 2703 return out
Generate plot for a single session
2705 def plot_residuals( 2706 self, 2707 hist = False, 2708 binwidth = 2/3, 2709 dir = 'output', 2710 filename = None, 2711 highlight = [], 2712 colors = None, 2713 figsize = None, 2714 ): 2715 ''' 2716 Plot residuals of each analysis as a function of time (actually, as a function of 2717 the order of analyses in the `D4xdata` object) 2718 2719 + `hist`: whether to add a histogram of residuals 2720 + `histbins`: specify bin edges for the histogram 2721 + `dir`: the directory in which to save the plot 2722 + `highlight`: a list of samples to highlight 2723 + `colors`: a dict of `{<sample>: <color>}` for all samples 2724 + `figsize`: (width, height) of figure 2725 ''' 2726 # Layout 2727 fig = ppl.figure(figsize = (8,4) if figsize is None else figsize) 2728 if hist: 2729 ppl.subplots_adjust(left = .08, bottom = .05, right = .98, top = .8, wspace = -0.72) 2730 ax1, ax2 = ppl.subplot(121), ppl.subplot(1,15,15) 2731 else: 2732 ppl.subplots_adjust(.08,.05,.78,.8) 2733 ax1 = ppl.subplot(111) 2734 2735 # Colors 2736 N = len(self.anchors) 2737 if colors is None: 2738 if len(highlight) > 0: 2739 Nh = len(highlight) 2740 if Nh == 1: 2741 colors = {highlight[0]: (0,0,0)} 2742 elif Nh == 3: 2743 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0)])} 2744 elif Nh == 4: 2745 colors = {a: c for a,c in zip(highlight, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2746 else: 2747 colors = {a: hls_to_rgb(k/Nh, .4, 1) for k,a in enumerate(highlight)} 2748 else: 2749 if N == 3: 2750 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0)])} 2751 elif N == 4: 2752 colors = {a: c for a,c in zip(self.anchors, [(0,0,1), (1,0,0), (0,2/3,0), (.75,0,.75)])} 2753 else: 2754 colors = {a: hls_to_rgb(k/N, .4, 1) for k,a in enumerate(self.anchors)} 2755 2756 ppl.sca(ax1) 2757 2758 ppl.axhline(0, color = 'k', alpha = .25, lw = 0.75) 2759 2760 session = self[0]['Session'] 2761 x1 = 0 2762# ymax = np.max([1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self]) 2763 x_sessions = {} 2764 one_or_more_singlets = False 2765 one_or_more_multiplets = False 2766 multiplets = set() 2767 for k,r in enumerate(self): 2768 if r['Session'] != session: 2769 x2 = k-1 2770 x_sessions[session] = (x1+x2)/2 2771 ppl.axvline(k - 0.5, color = 'k', lw = .5) 2772 session = r['Session'] 2773 x1 = k 2774 singlet = len(self.samples[r['Sample']]['data']) == 1 2775 if not singlet: 2776 multiplets.add(r['Sample']) 2777 if r['Sample'] in self.unknowns: 2778 if singlet: 2779 one_or_more_singlets = True 2780 else: 2781 one_or_more_multiplets = True 2782 kw = dict( 2783 marker = 'x' if singlet else '+', 2784 ms = 4 if singlet else 5, 2785 ls = 'None', 2786 mec = colors[r['Sample']] if r['Sample'] in colors else (0,0,0), 2787 mew = 1, 2788 alpha = 0.2 if singlet else 1, 2789 ) 2790 if highlight and r['Sample'] not in highlight: 2791 kw['alpha'] = 0.2 2792 ppl.plot(k, 1e3 * (r['D47'] - self.samples[r['Sample']]['D47']), **kw) 2793 x2 = k 2794 x_sessions[session] = (x1+x2)/2 2795 2796 ppl.axhspan(-self.repeatability['r_D47']*1000, self.repeatability['r_D47']*1000, color = 'k', alpha = .05, lw = 1) 2797 ppl.axhspan(-self.repeatability['r_D47']*1000*self.t95, self.repeatability['r_D47']*1000*self.t95, color = 'k', alpha = .05, lw = 1) 2798 if not hist: 2799 ppl.text(len(self), self.repeatability['r_D47']*1000, f" SD = {self.repeatability['r_D47']*1000:.1f} ppm", size = 9, alpha = 1, va = 'center') 2800 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') 2801 2802 xmin, xmax, ymin, ymax = ppl.axis() 2803 for s in x_sessions: 2804 ppl.text( 2805 x_sessions[s], 2806 ymax +1, 2807 s, 2808 va = 'bottom', 2809 **( 2810 dict(ha = 'center') 2811 if len(self.sessions[s]['data']) > (0.15 * len(self)) 2812 else dict(ha = 'left', rotation = 45) 2813 ) 2814 ) 2815 2816 if hist: 2817 ppl.sca(ax2) 2818 2819 for s in colors: 2820 kw['marker'] = '+' 2821 kw['ms'] = 5 2822 kw['mec'] = colors[s] 2823 kw['label'] = s 2824 kw['alpha'] = 1 2825 ppl.plot([], [], **kw) 2826 2827 kw['mec'] = (0,0,0) 2828 2829 if one_or_more_singlets: 2830 kw['marker'] = 'x' 2831 kw['ms'] = 4 2832 kw['alpha'] = .2 2833 kw['label'] = 'other (N$\\,$=$\\,$1)' if one_or_more_multiplets else 'other' 2834 ppl.plot([], [], **kw) 2835 2836 if one_or_more_multiplets: 2837 kw['marker'] = '+' 2838 kw['ms'] = 4 2839 kw['alpha'] = 1 2840 kw['label'] = 'other (N$\\,$>$\\,$1)' if one_or_more_singlets else 'other' 2841 ppl.plot([], [], **kw) 2842 2843 if hist: 2844 leg = ppl.legend(loc = 'upper right', bbox_to_anchor = (1, 1), bbox_transform=fig.transFigure, borderaxespad = 1.5, fontsize = 9) 2845 else: 2846 leg = ppl.legend(loc = 'lower right', bbox_to_anchor = (1, 0), bbox_transform=fig.transFigure, borderaxespad = 1.5) 2847 leg.set_zorder(-1000) 2848 2849 ppl.sca(ax1) 2850 2851 ppl.ylabel('Δ$_{47}$ residuals (ppm)') 2852 ppl.xticks([]) 2853 ppl.axis([-1, len(self), None, None]) 2854 2855 if hist: 2856 ppl.sca(ax2) 2857 X = [1e3 * (r['D47'] - self.samples[r['Sample']]['D47']) for r in self if r['Sample'] in multiplets] 2858 ppl.hist( 2859 X, 2860 orientation = 'horizontal', 2861 histtype = 'stepfilled', 2862 ec = [.4]*3, 2863 fc = [.25]*3, 2864 alpha = .25, 2865 bins = np.linspace(-9e3*self.repeatability['r_D47'], 9e3*self.repeatability['r_D47'], int(18/binwidth+1)), 2866 ) 2867 ppl.axis([None, None, ymin, ymax]) 2868 ppl.text(0, 0, 2869 f" SD = {self.repeatability['r_D47']*1000:.1f} ppm\n 95% CL = ± {self.repeatability['r_D47']*1000*self.t95:.1f} ppm", 2870 size = 8, 2871 alpha = 1, 2872 va = 'center', 2873 ha = 'left', 2874 ) 2875 2876 ppl.xticks([]) 2877 ppl.yticks([]) 2878# ax2.spines['left'].set_visible(False) 2879 ax2.spines['right'].set_visible(False) 2880 ax2.spines['top'].set_visible(False) 2881 ax2.spines['bottom'].set_visible(False) 2882 2883 2884 if not os.path.exists(dir): 2885 os.makedirs(dir) 2886 if filename is None: 2887 return fig 2888 elif filename == '': 2889 filename = f'D{self._4x}_residuals.pdf' 2890 ppl.savefig(f'{dir}/{filename}') 2891 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
2894 def simulate(self, *args, **kwargs): 2895 ''' 2896 Legacy function with warning message pointing to `virtual_data()` 2897 ''' 2898 raise DeprecationWarning('D4xdata.simulate is deprecated and has been replaced by virtual_data()')
Legacy function with warning message pointing to virtual_data()
2900 def plot_distribution_of_analyses( 2901 self, 2902 dir = 'output', 2903 filename = None, 2904 vs_time = False, 2905 figsize = (6,4), 2906 subplots_adjust = (0.02, 0.13, 0.85, 0.8), 2907 output = None, 2908 ): 2909 ''' 2910 Plot temporal distribution of all analyses in the data set. 2911 2912 **Parameters** 2913 2914 + `vs_time`: if `True`, plot as a function of `TimeTag` rather than sequentially. 2915 ''' 2916 2917 asamples = [s for s in self.anchors] 2918 usamples = [s for s in self.unknowns] 2919 if output is None or output == 'fig': 2920 fig = ppl.figure(figsize = figsize) 2921 ppl.subplots_adjust(*subplots_adjust) 2922 Xmin = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2923 Xmax = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self)]) 2924 Xmax += (Xmax-Xmin)/40 2925 Xmin -= (Xmax-Xmin)/41 2926 for k, s in enumerate(asamples + usamples): 2927 if vs_time: 2928 X = [r['TimeTag'] for r in self if r['Sample'] == s] 2929 else: 2930 X = [x for x,r in enumerate(self) if r['Sample'] == s] 2931 Y = [-k for x in X] 2932 ppl.plot(X, Y, 'o', mec = None, mew = 0, mfc = 'b' if s in usamples else 'r', ms = 3, alpha = .75) 2933 ppl.axhline(-k, color = 'b' if s in usamples else 'r', lw = .5, alpha = .25) 2934 ppl.text(Xmax, -k, f' {s}', va = 'center', ha = 'left', size = 7, color = 'b' if s in usamples else 'r') 2935 ppl.axis([Xmin, Xmax, -k-1, 1]) 2936 ppl.xlabel('\ntime') 2937 ppl.gca().annotate('', 2938 xy = (0.6, -0.02), 2939 xycoords = 'axes fraction', 2940 xytext = (.4, -0.02), 2941 arrowprops = dict(arrowstyle = "->", color = 'k'), 2942 ) 2943 2944 2945 x2 = -1 2946 for session in self.sessions: 2947 x1 = min([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2948 if vs_time: 2949 ppl.axvline(x1, color = 'k', lw = .75) 2950 if x2 > -1: 2951 if not vs_time: 2952 ppl.axvline((x1+x2)/2, color = 'k', lw = .75, alpha = .5) 2953 x2 = max([r['TimeTag'] if vs_time else j for j,r in enumerate(self) if r['Session'] == session]) 2954# from xlrd import xldate_as_datetime 2955# print(session, xldate_as_datetime(x1, 0), xldate_as_datetime(x2, 0)) 2956 if vs_time: 2957 ppl.axvline(x2, color = 'k', lw = .75) 2958 ppl.axvspan(x1,x2,color = 'k', zorder = -100, alpha = .15) 2959 ppl.text((x1+x2)/2, 1, f' {session}', ha = 'left', va = 'bottom', rotation = 45, size = 8) 2960 2961 ppl.xticks([]) 2962 ppl.yticks([]) 2963 2964 if output is None: 2965 if not os.path.exists(dir): 2966 os.makedirs(dir) 2967 if filename == None: 2968 filename = f'D{self._4x}_distribution_of_analyses.pdf' 2969 ppl.savefig(f'{dir}/{filename}') 2970 ppl.close(fig) 2971 elif output == 'ax': 2972 return ppl.gca() 2973 elif output == 'fig': 2974 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
2977class D47data(D4xdata): 2978 ''' 2979 Store and process data for a large set of Δ47 analyses, 2980 usually comprising more than one analytical session. 2981 ''' 2982 2983 Nominal_D4x = { 2984 'ETH-1': 0.2052, 2985 'ETH-2': 0.2085, 2986 'ETH-3': 0.6132, 2987 'ETH-4': 0.4511, 2988 'IAEA-C1': 0.3018, 2989 'IAEA-C2': 0.6409, 2990 'MERCK': 0.5135, 2991 } # I-CDES (Bernasconi et al., 2021) 2992 ''' 2993 Nominal Δ47 values assigned to the Δ47 anchor samples, used by 2994 `D47data.standardize()` to normalize unknown samples to an absolute Δ47 2995 reference frame. 2996 2997 By default equal to (after [Bernasconi et al. (2021)](https://doi.org/10.1029/2020GC009588)): 2998 ```py 2999 { 3000 'ETH-1' : 0.2052, 3001 'ETH-2' : 0.2085, 3002 'ETH-3' : 0.6132, 3003 'ETH-4' : 0.4511, 3004 'IAEA-C1' : 0.3018, 3005 'IAEA-C2' : 0.6409, 3006 'MERCK' : 0.5135, 3007 } 3008 ``` 3009 ''' 3010 3011 3012 @property 3013 def Nominal_D47(self): 3014 return self.Nominal_D4x 3015 3016 3017 @Nominal_D47.setter 3018 def Nominal_D47(self, new): 3019 self.Nominal_D4x = dict(**new) 3020 self.refresh() 3021 3022 3023 def __init__(self, l = [], **kwargs): 3024 ''' 3025 **Parameters:** same as `D4xdata.__init__()` 3026 ''' 3027 D4xdata.__init__(self, l = l, mass = '47', **kwargs) 3028 3029 3030 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 3031 ''' 3032 Find all samples for which `Teq` is specified, compute equilibrium Δ47 3033 value for that temperature, and add treat these samples as additional anchors. 3034 3035 **Parameters** 3036 3037 + `fCo2eqD47`: Which CO2 equilibrium law to use 3038 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 3039 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 3040 + `priority`: if `replace`: forget old anchors and only use the new ones; 3041 if `new`: keep pre-existing anchors but update them in case of conflict 3042 between old and new Δ47 values; 3043 if `old`: keep pre-existing anchors but preserve their original Δ47 3044 values in case of conflict. 3045 ''' 3046 f = { 3047 'petersen': fCO2eqD47_Petersen, 3048 'wang': fCO2eqD47_Wang, 3049 }[fCo2eqD47] 3050 foo = {} 3051 for r in self: 3052 if 'Teq' in r: 3053 if r['Sample'] in foo: 3054 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 3055 else: 3056 foo[r['Sample']] = f(r['Teq']) 3057 else: 3058 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 3059 3060 if priority == 'replace': 3061 self.Nominal_D47 = {} 3062 for s in foo: 3063 if priority != 'old' or s not in self.Nominal_D47: 3064 self.Nominal_D47[s] = foo[s]
Store and process data for a large set of Δ47 analyses, usually comprising more than one analytical session.
3023 def __init__(self, l = [], **kwargs): 3024 ''' 3025 **Parameters:** same as `D4xdata.__init__()` 3026 ''' 3027 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,
}
3030 def D47fromTeq(self, fCo2eqD47 = 'petersen', priority = 'new'): 3031 ''' 3032 Find all samples for which `Teq` is specified, compute equilibrium Δ47 3033 value for that temperature, and add treat these samples as additional anchors. 3034 3035 **Parameters** 3036 3037 + `fCo2eqD47`: Which CO2 equilibrium law to use 3038 (`petersen`: [Petersen et al. (2019)](https://doi.org/10.1029/2018GC008127); 3039 `wang`: [Wang et al. (2019)](https://doi.org/10.1016/j.gca.2004.05.039)). 3040 + `priority`: if `replace`: forget old anchors and only use the new ones; 3041 if `new`: keep pre-existing anchors but update them in case of conflict 3042 between old and new Δ47 values; 3043 if `old`: keep pre-existing anchors but preserve their original Δ47 3044 values in case of conflict. 3045 ''' 3046 f = { 3047 'petersen': fCO2eqD47_Petersen, 3048 'wang': fCO2eqD47_Wang, 3049 }[fCo2eqD47] 3050 foo = {} 3051 for r in self: 3052 if 'Teq' in r: 3053 if r['Sample'] in foo: 3054 assert foo[r['Sample']] == f(r['Teq']), f'Different values of `Teq` provided for sample `{r["Sample"]}`.' 3055 else: 3056 foo[r['Sample']] = f(r['Teq']) 3057 else: 3058 assert r['Sample'] not in foo, f'`Teq` is inconsistently specified for sample `{r["Sample"]}`.' 3059 3060 if priority == 'replace': 3061 self.Nominal_D47 = {} 3062 for s in foo: 3063 if priority != 'old' or s not in self.Nominal_D47: 3064 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
3069class D48data(D4xdata): 3070 ''' 3071 Store and process data for a large set of Δ48 analyses, 3072 usually comprising more than one analytical session. 3073 ''' 3074 3075 Nominal_D4x = { 3076 'ETH-1': 0.138, 3077 'ETH-2': 0.138, 3078 'ETH-3': 0.270, 3079 'ETH-4': 0.223, 3080 'GU-1': -0.419, 3081 } # (Fiebig et al., 2019, 2021) 3082 ''' 3083 Nominal Δ48 values assigned to the Δ48 anchor samples, used by 3084 `D48data.standardize()` to normalize unknown samples to an absolute Δ48 3085 reference frame. 3086 3087 By default equal to (after [Fiebig et al. (2019)](https://doi.org/10.1016/j.chemgeo.2019.05.019), 3088 Fiebig et al. (in press)): 3089 3090 ```py 3091 { 3092 'ETH-1' : 0.138, 3093 'ETH-2' : 0.138, 3094 'ETH-3' : 0.270, 3095 'ETH-4' : 0.223, 3096 'GU-1' : -0.419, 3097 } 3098 ``` 3099 ''' 3100 3101 3102 @property 3103 def Nominal_D48(self): 3104 return self.Nominal_D4x 3105 3106 3107 @Nominal_D48.setter 3108 def Nominal_D48(self, new): 3109 self.Nominal_D4x = dict(**new) 3110 self.refresh() 3111 3112 3113 def __init__(self, l = [], **kwargs): 3114 ''' 3115 **Parameters:** same as `D4xdata.__init__()` 3116 ''' 3117 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.
3113 def __init__(self, l = [], **kwargs): 3114 ''' 3115 **Parameters:** same as `D4xdata.__init__()` 3116 ''' 3117 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