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