DiadFit 1.0.5__py3-none-any.whl → 1.0.8__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/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