DiadFit 1.0.5__py3-none-any.whl → 1.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- DiadFit/CO2_EOS.py +7 -1
- DiadFit/_version.py +1 -1
- DiadFit/densimeter_fitting.py +1 -0
- DiadFit/densimeters.py +1 -1
- DiadFit/density_depth_crustal_profiles.py +197 -175
- DiadFit/diads.py +8 -47
- DiadFit/error_propagation.py +1 -1
- DiadFit/importing_data_files.py +152 -0
- DiadFit/molar_gas_proportions.py +203 -59
- DiadFit/ne_lines.py +5 -0
- DiadFit/relaxfi_PW.py +638 -0
- DiadFit/relaxifi.py +153 -28
- {DiadFit-1.0.5.dist-info → DiadFit-1.0.9.dist-info}/METADATA +1 -1
- {DiadFit-1.0.5.dist-info → DiadFit-1.0.9.dist-info}/RECORD +16 -15
- {DiadFit-1.0.5.dist-info → DiadFit-1.0.9.dist-info}/WHEEL +1 -1
- {DiadFit-1.0.5.dist-info → DiadFit-1.0.9.dist-info}/top_level.txt +0 -0
DiadFit/relaxfi_PW.py
ADDED
@@ -0,0 +1,638 @@
|
|
1
|
+
import matplotlib.pyplot as plt
|
2
|
+
import numpy as np
|
3
|
+
import pandas as pd
|
4
|
+
import math
|
5
|
+
from scipy.optimize import newton
|
6
|
+
import warnings
|
7
|
+
|
8
|
+
from DiadFit.density_depth_crustal_profiles import *
|
9
|
+
from DiadFit.CO2_EOS import *
|
10
|
+
|
11
|
+
## Functions to find P when the user chooses to start with a depth. It requires input of a crustal model
|
12
|
+
|
13
|
+
class config_crustalmodel:
|
14
|
+
"""
|
15
|
+
A configuration class for specifying parameters of the crustal model.
|
16
|
+
|
17
|
+
Attributes:
|
18
|
+
- crust_dens_kgm3 (float): The density of the crust in kilograms per cubic meter (kg/m^3).
|
19
|
+
- d1 (float): The depth boundary for the first layer in kilometers (km).
|
20
|
+
- d2 (float): The depth boundary for the second layer in kilometers (km).
|
21
|
+
- rho1 (float): The density of the first layer in kilograms per cubic meter (kg/m^3).
|
22
|
+
- rho2 (float): The density of the second layer in kilograms per cubic meter (kg/m^3).
|
23
|
+
- rho3 (float): The density of the third layer in kilograms per cubic meter (kg/m^3).
|
24
|
+
- model (str): The name of the model used for crustal calculations.
|
25
|
+
"""
|
26
|
+
def __init__(self, crust_dens_kgm3=None,
|
27
|
+
d1=None, d2=None, rho1=None, rho2=None, rho3=None, model=None):
|
28
|
+
self.crust_dens_kgm3 = crust_dens_kgm3
|
29
|
+
self.d1 = d1
|
30
|
+
self.d2 = d2
|
31
|
+
self.rho1 = rho1
|
32
|
+
self.rho2 = rho2
|
33
|
+
self.rho3 = rho3
|
34
|
+
self.model = model
|
35
|
+
|
36
|
+
def objective_function_depth(P_kbar, target_depth_km, crust_dens_kgm3,
|
37
|
+
d1, d2, rho1, rho2, rho3, model):
|
38
|
+
"""
|
39
|
+
Calculate the difference between the current depth and the target depth
|
40
|
+
given pressure (P_kbar) and other parameters.
|
41
|
+
|
42
|
+
Parameters:
|
43
|
+
- P_kbar (float): The pressure in kilobars (kbar) to be used in the depth calculation.
|
44
|
+
- target_depth_km (float): The desired depth in kilometers (km).
|
45
|
+
- crust_dens_kgm3 (float): The density of the crust in kilograms per cubic meter (kg/m^3).
|
46
|
+
- d1, d2 (float): Depth boundaries for different layers (km).
|
47
|
+
- rho1, rho2, rho3 (float): Densities for different layers (kg/m^3).
|
48
|
+
- model (str): The name of the model used for the depth calculation.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
- float: The difference between the current depth and the target depth.
|
52
|
+
"""
|
53
|
+
|
54
|
+
current_depth = convert_pressure_to_depth(P_kbar=P_kbar, crust_dens_kgm3=crust_dens_kgm3, g=9.81,
|
55
|
+
d1=d1, d2=d2, rho1=rho1, rho2=rho2, rho3=rho3, model=model)[0]
|
56
|
+
|
57
|
+
return current_depth - target_depth_km
|
58
|
+
|
59
|
+
def find_P_for_kmdepth(target_depth_km, crustal_model_config=config_crustalmodel(), initial_P_guess_kbar=0, tolerance=0.1):
|
60
|
+
"""
|
61
|
+
Approximate the pressure (P_kbar) based on the target depth using the Newton-Raphson method.
|
62
|
+
|
63
|
+
Parameters:
|
64
|
+
- target_depth_km (float, pd.Series, list): The desired depth(s) in kilometers (km).
|
65
|
+
- initial_P_guess_kbar (float, optional): Initial guess for the pressure in kilobars (kbar). Default is 0.
|
66
|
+
- crustal_model_config (object, optional): Configuration object containing crustal model parameters.
|
67
|
+
- crust_dens_kgm3 (float, optional): The density of the crust in kilograms per cubic meter (kg/m^3). Default is None.
|
68
|
+
- d1, d2 (float, optional): Depth boundaries for different layers (km). Default is None.
|
69
|
+
- rho1, rho2, rho3 (float, optional): Densities for different layers (kg/m^3). Default is None.
|
70
|
+
- model (str, optional): The name of the model used for depth calculation. Default is None.
|
71
|
+
- tolerance (float, optional): Tolerance for the Newton-Raphson method. The pressure estimate should be within this tolerance of the true value. Default is 0.1.
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
- float or pd.Series or list: The estimated pressure(s) (P_kbar) that correspond to the target depth(s).
|
75
|
+
|
76
|
+
Notes:
|
77
|
+
- If the target_depth_km is a single value, a float is returned.
|
78
|
+
- If the target_depth_km is a Pandas Series, a Pandas Series is returned.
|
79
|
+
- If the target_depth_km is a list, a list of floats is returned.
|
80
|
+
|
81
|
+
If crustal parameters are not provided in the crustal_model_config object, a single step model with a crustal density = 2750 kg/cm3 will be used.
|
82
|
+
|
83
|
+
"""
|
84
|
+
|
85
|
+
if isinstance(target_depth_km, (float, int)):
|
86
|
+
target_depth_km = [target_depth_km]
|
87
|
+
|
88
|
+
pressures = []
|
89
|
+
|
90
|
+
for depth in target_depth_km:
|
91
|
+
if all(v is None for v in [crustal_model_config.crust_dens_kgm3, crustal_model_config.d1, crustal_model_config.d2, crustal_model_config.rho1, crustal_model_config.rho2, crustal_model_config.rho3, crustal_model_config.model]):
|
92
|
+
crustal_model_config.crust_dens_kgm3 = 2750
|
93
|
+
warning_message = "\033[91mNo crustal parameters were provided, setting crust_dens_kgm3 to 2750. \nPlease use config_crustalmodel(...) to set your desired crustal model parameters.\033[0m"
|
94
|
+
warnings.simplefilter("always")
|
95
|
+
warnings.warn(warning_message, Warning, stacklevel=2)
|
96
|
+
|
97
|
+
# Use the Newton-Raphson method for each target depth
|
98
|
+
pressure = newton(objective_function_depth, initial_P_guess_kbar, args=(depth, crustal_model_config.crust_dens_kgm3, crustal_model_config.d1, crustal_model_config.d2, crustal_model_config.rho1, crustal_model_config.rho2, crustal_model_config.rho3, crustal_model_config.model), tol=tolerance)
|
99
|
+
pressures.append(pressure)
|
100
|
+
|
101
|
+
if isinstance(target_depth_km, (float, int)):
|
102
|
+
return pressures[0]
|
103
|
+
elif isinstance(target_depth_km, pd.Series):
|
104
|
+
return pd.Series(pressures)
|
105
|
+
else:
|
106
|
+
return pressures
|
107
|
+
|
108
|
+
## Auxilliary functions for the stretching models
|
109
|
+
|
110
|
+
# Calculate decompression steps for polybaric model (Pressure, Depth, dt)
|
111
|
+
|
112
|
+
def calculate_DPdt(ascent_rate_ms,crustal_model_config=config_crustalmodel(),D_initial_km=None,D_final_km=None,D_step=100,initial_P_guess_kbar=0, tolerance=0.001):
|
113
|
+
"""
|
114
|
+
Calculate the decompression rate (dP/dt) during ascent.
|
115
|
+
|
116
|
+
Parameters:
|
117
|
+
- ascent_rate_ms (float): Ascent rate in meters per second.
|
118
|
+
- D_initial_km (float, optional): Initial depth in kilometers. Default is 30 km.
|
119
|
+
- D_final_km (float, optional): Final depth in kilometers. Default is 0 km.
|
120
|
+
- D_step (int, optional): Number of depth steps for calculation. Default is 100.
|
121
|
+
- initial_P_guess_kbar (float, optional): Initial guess for pressure in kilobars (kbar). Default is 0.
|
122
|
+
- tolerance (float, optional): Tolerance for pressure estimation. Default is 0.001.
|
123
|
+
|
124
|
+
Returns:
|
125
|
+
- D (pd.Series): Depth values in kilometers.
|
126
|
+
- Pexternal_steps_MPa (list): Lithostatic pressure values in megapascals (MPa) at each depth step.
|
127
|
+
- dt (float): Time step for the integration.
|
128
|
+
"""
|
129
|
+
|
130
|
+
if D_initial_km is None or D_final_km is None or D_initial_km <= D_final_km:
|
131
|
+
raise ValueError("Both D_initial_km and D_final_km must be provided, and D_initial_km must be larger than D_final_km")
|
132
|
+
if D_initial_km>30 and D_step <= 80 and ascent_rate_ms <= 0.02:
|
133
|
+
raise Warning("Your D_step is too small, the minimum recommended for ascent rates below 0.02 m/s is 80")
|
134
|
+
D = pd.Series(list(np.linspace(D_initial_km, D_final_km, D_step))) # km
|
135
|
+
|
136
|
+
|
137
|
+
|
138
|
+
Pexternal_steps=find_P_for_kmdepth(D, crustal_model_config=crustal_model_config, initial_P_guess_kbar=initial_P_guess_kbar, tolerance=tolerance)
|
139
|
+
Pexternal_steps_MPa=Pexternal_steps*100
|
140
|
+
|
141
|
+
# Time steps of the ascent
|
142
|
+
ascent_rate = ascent_rate_ms / 1000 # km/s
|
143
|
+
D_change = abs(D.diff())
|
144
|
+
time_series = D_change / ascent_rate # calculates the time in between each step based on ascent rate
|
145
|
+
dt_s = time_series.max() # this sets the time step for the iterations later
|
146
|
+
|
147
|
+
return D, Pexternal_steps_MPa, dt_s
|
148
|
+
|
149
|
+
# Olivine creep constants
|
150
|
+
class power_creep_law_constants:
|
151
|
+
"""
|
152
|
+
Olivine power-law creep constants used in the stretching model (Wanamaker and Evans, 1989).
|
153
|
+
|
154
|
+
Attributes:
|
155
|
+
- A (float): Creep law constant A (default: 3.9e3).
|
156
|
+
- n (float): Creep law constant n (default: 3.6).
|
157
|
+
- Q (float): Activation energy for dislocation motions in J/mol (default: 523000).
|
158
|
+
- IgasR (float): Gas constant in J/(mol*K) (default: 8.314).
|
159
|
+
"""
|
160
|
+
def __init__(self):
|
161
|
+
self.A = 3.9*10**3 #7.0 * 10**4
|
162
|
+
self.n = 3.6 #3
|
163
|
+
self.Q = 523000 # 520 Activation energy for dislocation motions in J/mol
|
164
|
+
self.IgasR= 8.314 # Gas constant in J/(mol*K)
|
165
|
+
|
166
|
+
# Helper function to calculate change in radius over time (dR/dt)
|
167
|
+
def calculate_dR_dt(*,R_m, b_m, T_K, Pinternal_MPa, Pexternal_MPa):
|
168
|
+
"""
|
169
|
+
Calculate the rate of change of inclusion radius (dR/dt) based on power law creep.
|
170
|
+
|
171
|
+
Parameters:
|
172
|
+
- R_m (float): Inclusion radius in meters.
|
173
|
+
- b_m (float): Distance to the crystal defect structures. Wanamaker and Evans (1989) use R/b=1/1000.
|
174
|
+
- T_K (float): Temperature in Kelvin.
|
175
|
+
- Pinternal_MPa (float): Internal pressure in MPa.
|
176
|
+
- Pexternal_MPa (float): External pressure in MPa.
|
177
|
+
|
178
|
+
Returns:
|
179
|
+
- dR_dt (float): Rate of change of inclusion radius in meters per second.
|
180
|
+
"""
|
181
|
+
|
182
|
+
pl_Cs = power_creep_law_constants()
|
183
|
+
if Pinternal_MPa<Pexternal_MPa==True:
|
184
|
+
S=-1
|
185
|
+
else:
|
186
|
+
S=1
|
187
|
+
try:
|
188
|
+
dR_dt = 2 * (S * pl_Cs.A * math.exp(-pl_Cs.Q / (pl_Cs.IgasR * T_K))) * (((R_m * b_m)**3) / (((b_m**(3 / pl_Cs.n)) - (R_m**(3 / pl_Cs.n))))**pl_Cs.n) * (((3 * abs(Pinternal_MPa - Pexternal_MPa)) / (2 * pl_Cs.n))**pl_Cs.n) / R_m**2
|
189
|
+
return dR_dt
|
190
|
+
|
191
|
+
except FloatingPointError:
|
192
|
+
return np.nan
|
193
|
+
|
194
|
+
# Helper function to numerically solve for R (uses Runge-Kutta method, orders 1-4)
|
195
|
+
def get_R(R_m,b_m,T_K,Pinternal_MPa,Pexternal_MPa,dt_s,method='RK1'):
|
196
|
+
"""
|
197
|
+
Find the radius R of an FI over a time step using the Runge-Kutta numerical method.
|
198
|
+
Options are order 1 to 4 RK methods, such as RK1 (Euler), RK2 (Heun), RK3, or RK4.
|
199
|
+
|
200
|
+
Parameters:
|
201
|
+
- R_m (float): Initial FI Radius in meters.
|
202
|
+
- b_m (float): Distance to defect structures in meters.
|
203
|
+
- T_K (float): Temperature in Kelvin.
|
204
|
+
- Pinternal_MPa (float): Internal pressure in MPa.
|
205
|
+
- Pexternal_MPa (float): External pressure in MPa.
|
206
|
+
- dt_s (float): The time step for integration in seconds.
|
207
|
+
- method (str, optional): The numerical integration method to use. Default is 'RK1'.
|
208
|
+
|
209
|
+
Returns:
|
210
|
+
- tuple: A tuple containing the updated value of R_m and the derivative dR_dt (rate of change of R).
|
211
|
+
"""
|
212
|
+
if method == 'RK1'or 'Euler':
|
213
|
+
k1 = dt_s * calculate_dR_dt(R_m=R_m, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
214
|
+
dR=k1
|
215
|
+
dR_dt = dR / dt_s
|
216
|
+
R_m += dR
|
217
|
+
elif method == 'RK2' or 'Heun':
|
218
|
+
k1 = dt_s * calculate_dR_dt(R_m=R_m, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
219
|
+
k2 = dt_s * calculate_dR_dt(R_m=R_m + 0.5 * k1, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
220
|
+
dR = ((k1 + k2) / 2)
|
221
|
+
dR_dt = dR / dt_s
|
222
|
+
R_m += dR
|
223
|
+
elif method == 'RK3':
|
224
|
+
k1 = dt_s * calculate_dR_dt(R_m=R_m, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
225
|
+
k2 = dt_s * calculate_dR_dt(R_m=R_m + 0.5 * k1, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
226
|
+
k3 = dt_s * calculate_dR_dt(R_m=R_m + 0.5 * k2, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
227
|
+
dR = ((k1 + 4 * k2 + k3) / 6)
|
228
|
+
dR_dt = dR / dt_s
|
229
|
+
R_m += dR
|
230
|
+
elif method == 'RK4':
|
231
|
+
k1 = dt_s * calculate_dR_dt(R_m=R_m, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
232
|
+
k2 = dt_s * calculate_dR_dt(R_m=R_m + 0.5 * k1, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
233
|
+
k3 = dt_s * calculate_dR_dt(R_m=R_m + 0.5 * k2, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
234
|
+
k4 = dt_s * calculate_dR_dt(R_m=R_m + k3, b_m=b_m, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa)
|
235
|
+
dR = ((k1 + 2 * k2 + 2 * k3 + k4) / 6)
|
236
|
+
dR_dt = dR / dt_s
|
237
|
+
R_m += dR / 6
|
238
|
+
else:
|
239
|
+
raise ValueError("Unsupported numerical method. Choose from 'RK1' or 'Euler', 'RK2' or 'Huen', 'RK3', 'RK4'")
|
240
|
+
|
241
|
+
return R_m, dR_dt
|
242
|
+
|
243
|
+
## Functions to calculate P, CO2dens, CO2mass and V
|
244
|
+
|
245
|
+
# Calculate initial CO2 density in g/cm3 and CO2 mass in g
|
246
|
+
|
247
|
+
def get_initial_CO2(R_m, T_K, P_MPa, EOS='SW96', return_volume=False):
|
248
|
+
"""
|
249
|
+
Calculate the initial density and mass of CO2 inside a fluid inclusion (FI).
|
250
|
+
|
251
|
+
Parameters:
|
252
|
+
- R_m (float): The radius of the fluid inclusion (FI), in meters.
|
253
|
+
- T_K (float): The temperature, in Kelvin.
|
254
|
+
- P_MPa (float): The pressure, in MegaPascals (MPa).
|
255
|
+
- EOS (str, optional): The equation of state (EOS) to use for density calculations.
|
256
|
+
Can be one of: 'ideal' (ideal gas), 'SW96' (Span and Wagner EOS 1996), or 'SP94' (Sterner and Pitzer EOS 1994).
|
257
|
+
Defaults to 'SW96'.
|
258
|
+
- return_volume (bool, optional): Whether to return the volume of the FI along with density and mass. Defaults to False.
|
259
|
+
|
260
|
+
Returns:
|
261
|
+
- tuple or float: If return_volume is True, returns a tuple containing (V, CO2_dens_initial, CO2_mass_initial), where:
|
262
|
+
- V (float): The volume of the fluid inclusion (FI), in cubic meters (m³).
|
263
|
+
- CO2_dens_initial (float): The initial density of CO2 within the FI, in grams per cubic centimeter (g/cm³).
|
264
|
+
- CO2_mass_initial (float): The initial mass of CO2 within the FI, in grams (g).
|
265
|
+
|
266
|
+
- If return_volume is False, returns a tuple containing (CO2_dens_initial, CO2_mass_initial).
|
267
|
+
|
268
|
+
"""
|
269
|
+
|
270
|
+
valid_EOS = ['ideal', 'SW96', 'SP94']
|
271
|
+
|
272
|
+
try:
|
273
|
+
if EOS not in valid_EOS:
|
274
|
+
raise ValueError("EOS can only be 'ideal', 'SW96', or 'SP94'")
|
275
|
+
|
276
|
+
if EOS == 'ideal':
|
277
|
+
R_gas = 8.314 # J.mol/K J: kg·m²/s²
|
278
|
+
V = 4/3 * math.pi * R_m**3 # m3
|
279
|
+
P = P_MPa * 10**6 # convert MPa to Pa
|
280
|
+
M = 44.01 / 1000 # kg/mol
|
281
|
+
|
282
|
+
CO2_mass_kg = P * V * M / (R_gas * T_K) # CO2 mass in kg
|
283
|
+
rho = (CO2_mass_kg / V) # rho in kg/m3
|
284
|
+
|
285
|
+
CO2_dens_initial = rho / 1000 # CO2 density in g/cm3
|
286
|
+
CO2_mass_initial = CO2_mass_kg / 1000 # CO2 mass in g
|
287
|
+
|
288
|
+
else:
|
289
|
+
R_m = R_m * 10**2 # radius in cm
|
290
|
+
V = 4/3 * math.pi * R_m**3 # cm3, Volume of the FI, assume sphere
|
291
|
+
P_kbar = P_MPa / 100 # Internal pressure of the FI, convert to kbar
|
292
|
+
|
293
|
+
CO2_dens_initial = calculate_rho_for_P_T(EOS=EOS, P_kbar=P_kbar, T_K=T_K)[0] # CO2 density in g/cm3
|
294
|
+
CO2_mass_initial = CO2_dens_initial * V # CO2 mass in g
|
295
|
+
|
296
|
+
if return_volume:
|
297
|
+
return V, CO2_dens_initial, CO2_mass_initial
|
298
|
+
else:
|
299
|
+
return CO2_dens_initial, CO2_mass_initial
|
300
|
+
|
301
|
+
except ValueError as ve:
|
302
|
+
raise ve
|
303
|
+
|
304
|
+
# Calculate CO2 density in g/cm3 and P in MPa for fixed CO2 mass in g
|
305
|
+
|
306
|
+
def get_CO2dens_P(R_m,T_K,CO2_mass,EOS='SW96',return_volume=False):
|
307
|
+
"""
|
308
|
+
Calculate the density and pressure of CO2 inside a fluid inclusion (FI).
|
309
|
+
|
310
|
+
Parameters:
|
311
|
+
- R_m (float): The radius of the fluid inclusion (FI), in meters.
|
312
|
+
- T_K (float): The temperature, in Kelvin.
|
313
|
+
- CO2_mass (float): The mass of CO2 within the FI, in grams (g).
|
314
|
+
- EOS (str, optional): The equation of state (EOS) to use for density and pressure calculations.
|
315
|
+
Can be one of: 'ideal' (ideal gas), 'SW96' (Span and Wagner 1996), or 'SP94' (Sterner and Pitzer 1994).
|
316
|
+
Defaults to 'SW96'.
|
317
|
+
- return_volume (bool, optional): Whether to return the volume of the FI along with density and pressure. Defaults to False.
|
318
|
+
|
319
|
+
Returns:
|
320
|
+
- tuple or float: If return_volume is True, returns a tuple containing (V, CO2_dens, P), where:
|
321
|
+
- V (float): The volume of the fluid inclusion (FI), in cubic meters (m³).
|
322
|
+
- CO2_dens (float): The density of CO2 within the FI, in grams per cubic centimeter (g/cm³).
|
323
|
+
- P (float): The pressure of CO2 within the FI, in MegaPascals (MPa).
|
324
|
+
|
325
|
+
- If return_volume is False, returns a tuple containing (CO2_dens, P).
|
326
|
+
|
327
|
+
"""
|
328
|
+
valid_EOS = ['ideal', 'SW96', 'SP94']
|
329
|
+
|
330
|
+
try:
|
331
|
+
if EOS not in valid_EOS:
|
332
|
+
raise ValueError("EOS can only be 'ideal', 'SW96', or 'SP94'")
|
333
|
+
|
334
|
+
if EOS == 'ideal':
|
335
|
+
R_gas = 8.314 # J.mol/K J: kg·m²/s²
|
336
|
+
V = 4/3 * math.pi * R_m**3 # m3
|
337
|
+
M = 44.01 / 1000 # kg/mol
|
338
|
+
|
339
|
+
CO2_mass_kg=CO2_mass*1000
|
340
|
+
P=CO2_mass_kg*R_gas*T_K/(M*V) #P in Pa
|
341
|
+
CO2_dens=(CO2_mass_kg/V) # CO2 density in kg/m3
|
342
|
+
|
343
|
+
P=P/(10**6) #P in MPa
|
344
|
+
CO2_dens=CO2_dens/1000 #rho in g/cm3
|
345
|
+
|
346
|
+
else:
|
347
|
+
R_m=R_m*10**2 #FI radius, convert to cm
|
348
|
+
V=4/3*math.pi*R_m**3 #cm3, Volume of the FI, assume sphere
|
349
|
+
|
350
|
+
CO2_dens=CO2_mass/V # CO2 density in g/cm3
|
351
|
+
|
352
|
+
try:
|
353
|
+
P=calculate_P_for_rho_T(EOS=EOS,CO2_dens_gcm3=CO2_dens, T_K=T_K)['P_MPa'][0] #g/cm3, CO2 density
|
354
|
+
|
355
|
+
except ValueError:
|
356
|
+
P=np.nan
|
357
|
+
|
358
|
+
if return_volume:
|
359
|
+
return V, CO2_dens, P
|
360
|
+
else:
|
361
|
+
return CO2_dens,P
|
362
|
+
|
363
|
+
except ValueError as ve:
|
364
|
+
raise ve
|
365
|
+
|
366
|
+
## Stretching Models
|
367
|
+
|
368
|
+
# This function is to model FI stretching during decompression and ascent
|
369
|
+
def stretch_in_ascent(*, R_m, b_m, T_K, ascent_rate_ms, depth_path_ini_fin_step=[100, 0, 100],
|
370
|
+
crustal_model_config=config_crustalmodel(crust_dens_kgm3=2750),
|
371
|
+
EOS, plotfig=True, report_results='fullpath',
|
372
|
+
initial_P_guess_kbar=0, tolerance=0.001,method='RK4',update_b=False):
|
373
|
+
"""
|
374
|
+
Simulate the stretching of a CO2-dominated fluid inclusion (FI) during ascent.
|
375
|
+
|
376
|
+
Parameters:
|
377
|
+
- R_m (float): The initial radius of the fluid inclusion (FI), in meters.
|
378
|
+
- b_m (float): The initial distance to a crystal defect (rim, crack, etc) from the FI center, in meters.
|
379
|
+
- T_K (float): The temperature, in Kelvin.
|
380
|
+
- ascent_rate_ms (float): The ascent rate, in meters per second (m/s).
|
381
|
+
- depth_path_ini_fin_step (list, optional): A list containing [initial_depth_km, final_depth_km, depth_step].
|
382
|
+
Defaults to [100, 0, 100], representing the depth path from initial to final depth in a number of steps.
|
383
|
+
- crustal_model_config (dict, optional): Configuration parameters for the crustal model.
|
384
|
+
Defaults to a predefined configuration with a crustal density of 2750 kg/m³.
|
385
|
+
- EOS (str): The equation of state (EOS) to use for density calculations. Can be one of: 'ideal' (ideal gas),
|
386
|
+
'SW96' (Span and Wagner 1996), or 'SP94' (Sterner and Pitzer 1994).
|
387
|
+
- plotfig (bool, optional): Whether to plot figures showing the changes in depth and CO2 density. Defaults to True.
|
388
|
+
- report_results (str, optional): The type of results to report. Can be 'fullpath', 'startendonly', or 'endonly'.
|
389
|
+
Defaults to 'fullpath'.
|
390
|
+
- initial_P_guess_kbar (float, optional): Initial guess for internal pressure (Pinternal_MPa) in MPa. Defaults to 0.
|
391
|
+
- tolerance (float, optional): Tolerance for pressure calculations. Defaults to 0.001.
|
392
|
+
- method (str, optional): The numerical integration method to use for change in FI radius. Can be 'RK1' (also 'Euler'), 'RK2' (also 'Heun'), 'RK3', or 'RK4'.
|
393
|
+
Defaults to 'RK4'.
|
394
|
+
- update_b (bool, optional): Whether to update 'b' during the ascent. Defaults to False.
|
395
|
+
|
396
|
+
Returns:
|
397
|
+
- pandas.DataFrame: A DataFrame containing the simulation results, including time, depth, pressure, radius changes,
|
398
|
+
and CO2 density.
|
399
|
+
|
400
|
+
"""
|
401
|
+
|
402
|
+
D, Pexternal_steps, dt_s = calculate_DPdt(ascent_rate_ms=ascent_rate_ms, crustal_model_config=crustal_model_config,
|
403
|
+
D_initial_km=depth_path_ini_fin_step[0], D_final_km=depth_path_ini_fin_step[1],
|
404
|
+
D_step=depth_path_ini_fin_step[2],
|
405
|
+
initial_P_guess_kbar=initial_P_guess_kbar, tolerance=tolerance)
|
406
|
+
Pinternal_MPa = Pexternal_steps[0]
|
407
|
+
|
408
|
+
CO2_dens_initial, CO2_mass_initial = get_initial_CO2(R_m=R_m, T_K=T_K, P_MPa=Pinternal_MPa, EOS=EOS)
|
409
|
+
|
410
|
+
|
411
|
+
results = pd.DataFrame([{'Time(s)': 0,
|
412
|
+
'Step':0,
|
413
|
+
'dt(s)':0,
|
414
|
+
'Pexternal(MPa)': Pinternal_MPa,
|
415
|
+
'Pinternal(MPa)': Pinternal_MPa,
|
416
|
+
'dR/dt(m/s)': calculate_dR_dt(R_m=R_m, b_m=b_m, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pinternal_MPa, T_K=T_K),
|
417
|
+
'Fi_radius(μm)': R_m*10**6,
|
418
|
+
'b (distance to xtal rim -μm)':b_m*10**6,
|
419
|
+
'\u0394R/R0 (fractional change in radius)':np.nan,
|
420
|
+
'CO2_dens_gcm3': CO2_dens_initial,
|
421
|
+
'Depth(km)':D.iloc[0]}], index=range(len(Pexternal_steps)))
|
422
|
+
|
423
|
+
|
424
|
+
for i in range(1,len(Pexternal_steps)):
|
425
|
+
|
426
|
+
Pexternal = Pexternal_steps[i]
|
427
|
+
|
428
|
+
R_m,dR_dt = get_R(R_m=R_m,b_m=b_m,T_K=T_K,Pinternal_MPa=Pinternal_MPa,Pexternal_MPa=Pexternal,dt_s=dt_s,method=method)
|
429
|
+
CO2_dens_new,P_new = get_CO2dens_P(R_m=R_m,T_K=T_K,CO2_mass=CO2_mass_initial,EOS=EOS)
|
430
|
+
|
431
|
+
Pinternal_MPa = P_new
|
432
|
+
|
433
|
+
if update_b==True:
|
434
|
+
b_m=1000*R_m
|
435
|
+
|
436
|
+
results.loc[i] = [dt_s*i, i, dt_s, Pexternal, Pinternal_MPa, dR_dt, R_m * 10 ** 6, b_m * 10 ** 6,
|
437
|
+
(R_m * 10 ** 6 - results.loc[0, 'Fi_radius(μm)']) / results.loc[0, 'Fi_radius(μm)'],
|
438
|
+
CO2_dens_new, D.iloc[i]]
|
439
|
+
|
440
|
+
if report_results == 'startendonly':
|
441
|
+
results.drop(index=list(range(1, results.shape[0] - 1)), inplace=True) # Drop all rows except first and last
|
442
|
+
|
443
|
+
if report_results == 'endonly':
|
444
|
+
results.drop(index=list(range(0, results.shape[0] - 1)), inplace=True) # Drop all rows except last
|
445
|
+
|
446
|
+
if plotfig==True:
|
447
|
+
fig, (ax0,ax1) = plt.subplots(1,2, figsize=(10,3))
|
448
|
+
ax0.plot(results['Depth(km)'],results['\u0394R/R0 (fractional change in radius)'],marker='s',label=f"Ascent Rate = {ascent_rate_ms} m/s")
|
449
|
+
ax0.set_xlim([depth_path_ini_fin_step[0],depth_path_ini_fin_step[1]])
|
450
|
+
ax0.set_xlabel("Depth (km)")
|
451
|
+
ax0.set_ylabel('\u0394R/R0 (fractional change in radius)')
|
452
|
+
|
453
|
+
ax1.plot(results['Depth(km)'],results['CO2_dens_gcm3'],marker='s',label=f"Ascent Rate = {ascent_rate_ms} m/s")
|
454
|
+
ax1.set_xlim([depth_path_ini_fin_step[0],depth_path_ini_fin_step[1]])
|
455
|
+
ax1.set_xlabel("Depth (km)")
|
456
|
+
ax1.set_ylabel("CO$_2$ density (g/cm$^{3}$)")
|
457
|
+
ax0.legend(loc='best')
|
458
|
+
ax1.legend(loc='best')
|
459
|
+
fig.tight_layout()
|
460
|
+
plt.show()
|
461
|
+
|
462
|
+
return results
|
463
|
+
|
464
|
+
# This function is to model stretching at fixed External Pressure (e.g., during stalling or upon eruption)
|
465
|
+
def stretch_at_constant_Pext(*,R_m,b_m,T_K,EOS='SW96',Pinternal_MPa,Pexternal_MPa,totaltime_s,steps,method='RK4',report_results='fullpath',plotfig=False,update_b=False):
|
466
|
+
"""
|
467
|
+
Simulate the stretching of a CO2 fluid inclusion (FI) under constant external pressure (e.g., quenching or storage).
|
468
|
+
|
469
|
+
Parameters:
|
470
|
+
- R_m (float): The initial radius of the fluid inclusion (FI), in meters.
|
471
|
+
- b_m (float): The initial distance to a crystal defect (rim, crack, etc) from the FI center, in meters.
|
472
|
+
- T_K (float): The temperature, in Kelvin.
|
473
|
+
- Pinternal_MPa (float): The initial internal pressure of the FI, in MegaPascals (MPa).
|
474
|
+
- Pexternal_MPa (float): The constant external pressure applied to the FI, in MegaPascals (MPa).
|
475
|
+
- totaltime_s (float): The total simulation time, in seconds.
|
476
|
+
- steps (int): The number of simulation steps.
|
477
|
+
- EOS (str): The equation of state (EOS) to use for density calculations. Can be one of: 'ideal' (ideal gas),
|
478
|
+
'SW96' (Span and Wagner 1996), or 'SP94' (Sterner and Pitzer 1994).
|
479
|
+
- method (str, optional): The numerical integration method to use for change in FI radius. Can be 'RK1' (also 'Euler'), 'RK2' (also 'Heun'), 'RK3', or 'RK4'.
|
480
|
+
Defaults to 'RK4'.
|
481
|
+
- report_results (str, optional): The type of results to report. Can be 'fullpath' (all steps reported), 'startendonly' (only initial and end steps), or 'endonly' (only last step).
|
482
|
+
Defaults to 'fullpath'.
|
483
|
+
- plotfig (bool, optional): Whether to plot figures showing the changes in time, radius, and CO2 density. Defaults to False.
|
484
|
+
- update_b (bool, optional): Whether to update 'b' during the simulation. Defaults to False.
|
485
|
+
|
486
|
+
Returns:
|
487
|
+
- pandas.DataFrame: A DataFrame containing the simulation results, including time, pressure, radius changes, and CO2 density.
|
488
|
+
|
489
|
+
Raises:
|
490
|
+
- ValueError: If an unsupported EOS is specified.
|
491
|
+
"""
|
492
|
+
|
493
|
+
CO2_dens_initial,CO2_mass_initial=get_initial_CO2(R_m=R_m,T_K=T_K,P_MPa=Pinternal_MPa,EOS=EOS)
|
494
|
+
|
495
|
+
results = pd.DataFrame([{'Time(s)': 0,
|
496
|
+
'Step':0,
|
497
|
+
'dt(s)':0,
|
498
|
+
'Pexternal(MPa)': float(Pexternal_MPa),
|
499
|
+
'Pinternal(MPa)': float(Pinternal_MPa),
|
500
|
+
'dR/dt(m/s)': float(calculate_dR_dt(R_m=R_m, b_m=b_m, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa, T_K=T_K)),
|
501
|
+
'Fi_radius(μm)': float(R_m*10**6),
|
502
|
+
'b (distance to xtal rim -μm)':float(b_m*10**6),
|
503
|
+
'\u0394R/R0 (fractional change in radius)':0,
|
504
|
+
'CO2_dens_gcm3': float(CO2_dens_initial)}], index=range(steps))
|
505
|
+
|
506
|
+
results = results.astype({
|
507
|
+
'Time(s)': 'float64',
|
508
|
+
'Step': 'int64',
|
509
|
+
'dt(s)': 'float64',
|
510
|
+
'Pexternal(MPa)': 'float64',
|
511
|
+
'Pinternal(MPa)': 'float64',
|
512
|
+
'dR/dt(m/s)': 'float64',
|
513
|
+
'Fi_radius(μm)': 'float64',
|
514
|
+
'b (distance to xtal rim -μm)': 'float64',
|
515
|
+
'\u0394R/R0 (fractional change in radius)': 'float64',
|
516
|
+
'CO2_dens_gcm3': 'float64'
|
517
|
+
})
|
518
|
+
|
519
|
+
|
520
|
+
dt_s=totaltime_s/steps
|
521
|
+
|
522
|
+
for step in range(1,steps):
|
523
|
+
|
524
|
+
R_new,dR_dt = get_R(R_m=R_m,b_m=b_m,T_K=T_K,Pinternal_MPa=Pinternal_MPa,Pexternal_MPa=Pexternal_MPa,dt_s=dt_s,method=method)
|
525
|
+
|
526
|
+
CO2_dens_new,P_new = get_CO2dens_P(R_m=R_new,T_K=T_K,CO2_mass=CO2_mass_initial,EOS=EOS)
|
527
|
+
|
528
|
+
Pinternal_MPa = P_new
|
529
|
+
R_m=R_new
|
530
|
+
|
531
|
+
if update_b==True:
|
532
|
+
b_m=1000*R_m
|
533
|
+
|
534
|
+
results.loc[step] = [float(step * dt_s), int(step), float(dt_s), float(Pexternal_MPa), float(Pinternal_MPa),
|
535
|
+
float(dR_dt), float(R_m * 10 ** 6), float(b_m * 10 ** 6),
|
536
|
+
float((R_m * 10 ** 6 - results.loc[0, 'Fi_radius(μm)']) / results.loc[0, 'Fi_radius(μm)']),
|
537
|
+
float(CO2_dens_new)]
|
538
|
+
|
539
|
+
if report_results == 'startendonly':
|
540
|
+
results.drop(index=list(range(1, results.shape[0] - 1)), inplace=True) # Drop all rows except first and last
|
541
|
+
|
542
|
+
if report_results == 'endonly':
|
543
|
+
results.drop(index=list(range(0, results.shape[0] - 1)), inplace=True) # Drop all rows except last
|
544
|
+
|
545
|
+
if plotfig==True:
|
546
|
+
if totaltime_s < 60:
|
547
|
+
x_time = results['Time(s)']
|
548
|
+
xlabel = 'Time(s)'
|
549
|
+
elif 60 <= totaltime_s < 3600:
|
550
|
+
x_time = results['Time(s)'] / 60
|
551
|
+
xlabel = 'Time(min)'
|
552
|
+
results[xlabel]=x_time
|
553
|
+
elif 3600 <= totaltime_s < 86400:
|
554
|
+
x_time = results['Time(s)'] / 3600
|
555
|
+
xlabel = 'Time(hr)'
|
556
|
+
results[xlabel]=x_time
|
557
|
+
elif 86400 <= totaltime_s < 31536000:
|
558
|
+
x_time = results['Time(s)'] / (3600 * 24)
|
559
|
+
xlabel = 'Time(days)'
|
560
|
+
results[xlabel]=x_time
|
561
|
+
elif totaltime_s >= 31536000:
|
562
|
+
x_time = results['Time(s)'] / (3600 * 24 * 365)
|
563
|
+
xlabel = 'Time(years)'
|
564
|
+
results[xlabel]=x_time
|
565
|
+
|
566
|
+
fig, (ax0,ax1) = plt.subplots(1,2, figsize=(10,3))
|
567
|
+
ax0.plot(x_time,results['\u0394R/R0 (fractional change in radius)'],marker='s')
|
568
|
+
ax0.set_xlabel(xlabel)
|
569
|
+
ax0.set_ylabel("\u0394R/R0 (fractional change in radius)")
|
570
|
+
|
571
|
+
ax1.plot(x_time,results['CO2_dens_gcm3'],marker='s')
|
572
|
+
ax1.set_xlabel(xlabel)
|
573
|
+
ax1.set_ylabel("CO2_density_gmL")
|
574
|
+
fig.tight_layout()
|
575
|
+
plt.show()
|
576
|
+
|
577
|
+
return results
|
578
|
+
|
579
|
+
# This function can loop through different R and b value sets using stretch at constant Pext
|
580
|
+
|
581
|
+
def loop_R_b_constant_Pext(*,R_m_values, b_m_values, T_K, EOS, Pinternal_MPa, Pexternal_MPa, totaltime_s, steps, T4endcalc_PD, method='RK4',
|
582
|
+
plotfig=False, crustal_model_config=config_crustalmodel(crust_dens_kgm3=2750)):
|
583
|
+
|
584
|
+
"""
|
585
|
+
Perform multiple simulations under constant external pressure with various R and b values.
|
586
|
+
|
587
|
+
Parameters:
|
588
|
+
- R_m_values (list): A list of initial radius values for fluid inclusions (FI), in meters.
|
589
|
+
- b_m_values (list): A list of initial distance values to a crystal defect (rim, crack, etc) from the FI center, in meters.
|
590
|
+
- T_K (float): The temperature, in Kelvin.
|
591
|
+
- EOS (str): The equation of state (EOS) to use for density calculations. Can be one of: 'ideal' (ideal gas),
|
592
|
+
'SW96' (Span and Wagner 1996), or 'SP94' (Sterner and Pitzer 1994).
|
593
|
+
- Pinternal_MPa (float): The initial internal pressure of the FI, in MegaPascals (MPa).
|
594
|
+
- Pexternal_MPa (float): The constant external pressure applied to the FI, in MegaPascals (MPa).
|
595
|
+
- totaltime_s (float): The total simulation time, in seconds.
|
596
|
+
- steps (int): The number of simulation steps.
|
597
|
+
- T4endcalc_PD (float): The temperature at which to calculate the depths (Kelvin) at the end of the simulations.
|
598
|
+
- method (str, optional): The numerical integration method to use for change in FI radius. Can be 'RK1' (also 'Euler'), 'RK2' (also 'Heun'), 'RK3', or 'RK4'.
|
599
|
+
Defaults to 'RK4'.
|
600
|
+
- plotfig (bool, optional): Whether to plot figures showing the changes in time, radius, and CO2 density. Defaults to False.
|
601
|
+
- crustal_model_config (dict, optional): Configuration parameters for the crustal model. Defaults to a predefined
|
602
|
+
configuration with a crustal density of 2750 kg/m³.
|
603
|
+
|
604
|
+
Returns:
|
605
|
+
- dict: A dictionary containing simulation results for varying R and b values. The keys are in the format 'R{index}_b{index}',
|
606
|
+
where 'index' corresponds to the index of R_values and b_values, respectively. The values are DataFrames with
|
607
|
+
simulation results.
|
608
|
+
"""
|
609
|
+
|
610
|
+
results_dict = {}
|
611
|
+
for idx_R, R in enumerate(R_m_values): # Use enumerate to get the index of R_values
|
612
|
+
R_key = f'R{idx_R}' # Use 'R' followed by the index
|
613
|
+
results_dict[R_key] = {}
|
614
|
+
|
615
|
+
for idx_b, b in enumerate(b_m_values): # Use enumerate to get the index of b_values
|
616
|
+
b_key = f'b{idx_b}' # Use 'b' followed by the index
|
617
|
+
results = stretch_at_constant_Pext(R_m=R, b_m=b, T_K=T_K, Pinternal_MPa=Pinternal_MPa, Pexternal_MPa=Pexternal_MPa,
|
618
|
+
totaltime_s=totaltime_s, steps=steps, EOS=EOS, method=method,
|
619
|
+
plotfig=plotfig)
|
620
|
+
results['Calculated depths (km)_StorageT'] = convert_pressure_to_depth(
|
621
|
+
P_kbar=results['Pinternal(MPa)'] / 100,
|
622
|
+
crust_dens_kgm3=crustal_model_config.crust_dens_kgm3, g=9.81,
|
623
|
+
d1=crustal_model_config.d1, d2=crustal_model_config.d2,
|
624
|
+
rho1=crustal_model_config.rho1, rho2=crustal_model_config.rho2, rho3=crustal_model_config.rho3,
|
625
|
+
model=crustal_model_config.model)
|
626
|
+
results['Calculated P from rho (MPa)_TrappingT'] = calculate_P_for_rho_T(
|
627
|
+
EOS='SW96', CO2_dens_gcm3=results['CO2_dens_gcm3'], T_K=T4endcalc_PD + 273.15)['P_MPa']
|
628
|
+
|
629
|
+
results['Calculated depths (km)_TrappingT'] = convert_pressure_to_depth(
|
630
|
+
P_kbar=results['Calculated P from rho (MPa)_TrappingT'] / 100,
|
631
|
+
crust_dens_kgm3=crustal_model_config.crust_dens_kgm3, g=9.81,
|
632
|
+
d1=crustal_model_config.d1, d2=crustal_model_config.d2,
|
633
|
+
rho1=crustal_model_config.rho1, rho2=crustal_model_config.rho2, rho3=crustal_model_config.rho3,
|
634
|
+
model=crustal_model_config.model)
|
635
|
+
|
636
|
+
results_dict[R_key][b_key] = results
|
637
|
+
|
638
|
+
return results_dict
|