.. _petrophysics-fluid-properties: Petrophysics and pore-fluid properties ====================================== Capabilities and inputs ----------------------- Use these Python helpers to connect pore-fluid chemistry, clay exchange capacity and effective grain-contact pressure to electrical resistivity and seismic velocity. They require NumPy and SciPy through the package, with no optional geophysical engine. Inputs may be scalars or broadcast-compatible arrays. These options are available through Python; the desktop controls do not yet expose them. For workflows, start with :doc:`/tutorials/index`; for signatures, see :doc:`/api/petrophysics`. The defaults remain unchanged: Hertz-Mindlin coordination number 4, water modulus 2.0 GPa, liquid density 1000 kg/m3, and effective pressure estimated as ``(mineral_density - 1000) * 9.8 * depth`` in Pa. A coordination number around 9 is a common idealized random-packing choice, not a universal recommendation; calibrate it to the material (Mavko et al., 2009, chapter 5). Set it through ``HertzMindlinModel(coordination_number=9)`` when appropriate. Brine and seismic velocity -------------------------- ``batzle_wang_brine(p, T, salinity)`` uses pressure in Pa, temperature in degrees Celsius and NaCl mass fraction in kg/kg of solution. It returns an object with ``bulk_modulus`` in GPa and ``density`` in kg/m3. The implementation uses the Batzle-Wang (1992) liquid polynomials with a conservative input domain of 0--100 MPa, 0--100 degrees Celsius and 0--0.35 kg/kg. These bounds do not check boiling or salt precipitation; ensure a liquid solution exists. At 5 MPa and 25 degrees Celsius, fresh water gives 2.25843 GPa and 998.19925 kg/m3. The latter is the empirical polynomial result, not a separate high-accuracy water equation of state. Increasing salinity at these conditions raises both density and modulus. .. code-block:: python import numpy as np from PyHydroGeophysX.petrophysics import HertzMindlinModel, batzle_wang_brine brine = batzle_wang_brine(p=5e6, T=25, salinity=0.035) vp_high, vp_low, elastic = HertzMindlinModel().calculate_velocity( porosity=np.array([0.2, 0.3]), saturation=1.0, bulk_modulus=36.6, shear_modulus=45.0, mineral_density=2650, effective_pressure=2e6, fluid_modulus=brine.bulk_modulus, fluid_density=brine.density, return_moduli=True, ) Pore-fluid pressure and effective grain-contact pressure are different physical inputs; do not equate them automatically. An explicit ``effective_pressure`` overrides depth, including at zero. Changing ``fluid_density`` affects bulk density only, retaining the historical pressure estimator unless an effective pressure is supplied. ``fluid_modulus`` replaces the liquid modulus in the existing Brie mixture (gas modulus 0.01 GPa, exponent 3). An explicit value at or below 1e-12 GPa instead selects the dry-frame limit at every saturation: ``K_sat = K_dry``. Density still follows the supplied saturation and densities. This permits dry-frame diagnostics without silently changing bulk mass. With ``return_moduli=False``, the return value remains ``(Vp_high, Vp_low)``. When True, a third item contains ``Vs_low``, ``Vs_high``, ``K_dry_low``, ``K_dry_high``, ``G_dry_low``, ``G_dry_high``, ``K_sat_low``, ``K_sat_high`` and ``density``. Moduli are GPa, velocities m/s and density kg/m3. The relations are ``Vp**2 = (K_sat + 4*G_dry/3)*1e9/density`` and ``Vs**2 = G_dry*1e9/density``. .. _hertz-mindlin-shear-fix: Hertz-Mindlin shear correction and legacy results ------------------------------------------------- The Hertz-Mindlin contact shear modulus is .. math:: G_{\mathrm{HM}} = \frac{5-4\nu}{5(2-\nu)} \left[\frac{3 C^2 (1-\phi_c)^2 G_m^2 P} {2\pi^2(1-\nu)^2}\right]^{1/3}. The prefactor is outside the cube root. Use consistent units for pressure and mineral moduli: this implementation converts the Pa input to GPa. The formula is verified against the authors' `Rock Physics Handbook companion routines `_ (``hertzmind.m``, supplied with Mavko, Mukerji and Dvorkin, 2009), and `rockphypy GM.hertzmindlin `_ at ``f=1``. rockphypy expects pressure in MPa, so 6.5502 MPa is supplied there as 6.5502 and to this API as 6.5502e6. Affected revisions before ``962a9ced`` used ``10 - 2*nu`` in the denominator and put the class method's prefactor inside the cube root. The older standalone ``vel_porous`` had the denominator error only. Those are bugs, not alternative modeling defaults. The equations were corrected in that commit; ``vel_porous`` now delegates to ``HertzMindlinModel.calculate_velocity`` so it cannot drift from the two-value and ``return_moduli=True`` paths. No option restores the incorrect formula. For the dt_framework notebook 01 section 7.2 baseline at x=2.51 m, use phi=0.2, S=1, Km=40.39369 GPa, Gm=43.10014 GPa, mineral density=2657.5 kg/m3, C=9, phi_c=0.4, effective pressure=6.5502 MPa, fluid modulus=2.260256 GPa and fluid density=998.4548 kg/m3. These are explicit supplied fluid properties, not values recomputed by a separate water-property model. .. list-table:: Lower-bound baseline at identical fluid properties :header-rows: 1 * - Quantity - Historical class equation - Standard equation * - Dry bulk modulus (GPa) - 5.691664 - 4.663967 * - Dry shear modulus (GPa) - 7.614628 - 5.103210 * - Saturated bulk modulus (GPa) - 12.733917 - 12.084909 * - Vp (m/s) - 3137.011 - 2849.907 * - Vs (m/s) - 1809.457 - 1481.309 Both columns use bulk density 2325.69096 kg/m3. The historical function did not accept fluid overrides: its dry moduli were evaluated from the original source and substituted with the supplied fluids for this comparison. Running the actual old API with its fixed water properties gives lower Vp 3088.660 m/s versus 2794.058 m/s from the corrected legacy API, at the same equivalent depth of 403.250531 m. Results change even when the default coordination number remains 4. For quartz (Km=36.6 GPa, Gm=45 GPa, mineral density=2650 kg/m3) with phi=0.2, S=1 and depth=5 m, the class's lower Vp decreases from 2109.732 to 2039.173 m/s (3.344%). At phi=0.3, S=0.5 and depth=5 m, it decreases from 1038.908 to 939.168 m/s (9.600%). Historical vel_porous instead increases slightly for the first case, from 2036.809 to 2039.173 m/s. Recompute outputs and review calibrations made with the affected equations. Ionic conductivity and charge ----------------------------- ``pore_fluid_conductivity`` takes a dictionary of dissolved ion concentrations in mol/L, with repeated trailing charge signs, such as ``Ca++`` and ``CO3--``. ``ion_charge`` returns 2 and -2 for these names. Missing or mixed signs are rejected; formula digits are not charge numbers (``NH4+`` has charge +1). The coefficient convention is **equivalent conductivity**, in S m2/eq: .. math:: \sigma_w = 1000 \sum_i |z_i|\lambda_i c_i. The following table shows the original values in S cm2/eq; multiply by ``1e-4`` for the optional ``equivalent_conductivities`` argument. This mapping overrides selected defaults and can add ions. Unknown ions without a coefficient are rejected. Source: Kanciruk et al. (1986), ORNL/TM-10153, Table 8, CONCAL, printed page 30. .. list-table:: Limiting equivalent conductivities at 25 degrees Celsius :header-rows: 1 * - Ion - S cm2/eq * - H+ - 349.65 * - OH- - 198.0 * - Na+ - 50.08 * - Cl- - 76.31 * - Ca++ - 59.47 * - HCO3- - 44.5 * - CO3-- - 69.3 The default table also includes Mg++, K+, NH4+, SO4--, NO3- and F-. A molar coefficient already includes the charge contribution: for Ca++ 118.94 S cm2/mol corresponds to 59.47 S cm2/eq. Supplying the former as an equivalent coefficient would double-count calcium's charge. .. code-block:: python from PyHydroGeophysX.petrophysics import pore_fluid_conductivity sigma_w, contributions = pore_fluid_conductivity({"Na+": 0.01, "Cl-": 0.01}) # sigma_w = 0.12639 S/m; contributions sum to sigma_w Use ``return_contributions=False`` to return only the total. Concentrations are dissolved species, not neutral salt totals; no speciation, ion pairing or automatic water dissociation is computed. Optional concentration correction --------------------------------- The default is the limiting-ion approximation. It overestimates the conductivity of finite-concentration salt solutions. For example, 0.005 M CaCl2 gives 0.13578 S/m, about 6.2% above the 0.1279 S/m conductometry example in Girault (2004). An optional calibrated Kohlrausch-type correction uses ionic strength ``I = 0.5 * sum(c_i*z_i**2)`` in mol/L and multiplies every contribution by ``1 - a*sqrt(I)``. Enable ``ionic_strength_correction=True`` and provide ``kohlrausch_coefficient=a`` in (mol/L)**(-1/2). There is no universal default coefficient. For a fixed electrolyte with ``I = q*c_salt``, the published Kohlrausch form ``Lambda = Lambda0 - A*sqrt(c_salt)`` corresponds to ``a = A/(Lambda0*sqrt(q))``. See Metrohm's conductometry monograph, page 14. This is a dilute, fixed-composition empirical correction. Calibrate the coefficient against data for the intended electrolyte and concentration range; it is not a predictive mixed-brine transport model. Corrected contributions retain their limiting proportions and sum to the corrected total. An absent coefficient or a negative correction factor raises ``ValueError``. CEC, surface conduction and clipping ------------------------------------ ``bulk_cec_to_qv`` converts CEC in eq/m3 **bulk material** to Qv in meq/cm3 **pore volume**: ``Qv = CEC/porosity * 1e-3``. Thus CEC 10 and porosity 0.2 give Qv 0.05. Mass-specific CEC in meq/100 g requires a separate conversion using the appropriate bulk mass per volume. ``waxman_smits_surface_conductivity(sigma_w, qv)`` implements the specified 25-degree-Celsius Waxman-Thomas relation: .. math:: \sigma_s = 4.6 [1 - 0.6\exp(-\sigma_w/1.3)] Q_v. Here ``sigma_w`` and ``sigma_s`` are S/m and Qv is meq/cm3; 1.3 has units S/m and 4.6 has units (S/m)/(meq/cm3). Pass the result as ``sigma_s`` to ``WS_Model``, which scales both fluid and surface terms by the formation factor. Other converters' ``sigma_sur`` is outside that scaling, so the coefficients are not interchangeable. .. code-block:: python from PyHydroGeophysX.petrophysics import ( bulk_cec_to_qv, waxman_smits_surface_conductivity, WS_Model, ) qv = bulk_cec_to_qv(10.0, porosity=0.2) sigma_s = waxman_smits_surface_conductivity(sigma_w, qv) resistivity = WS_Model( saturation=0.8, porosity=0.2, sigma_w=sigma_w, m=2, n=2, sigma_s=sigma_s, ) ``WS_Model`` retains saturation clipping to [0.001, 1], a conductivity floor of 1e-6 S/m, and resistivity clipping to [0.1, 1e6] ohm-m. It emits one ``RuntimeWarning`` per call when any of these limits activates, including the conductivity floor. Normal inputs emit no clipping warning. NaN values propagate. These limits are numerical safeguards, not inferred physical bounds on your sample. References ---------- Full BibTeX records are in :doc:`/citation`. Relevant sources are Batzle and Wang (1992), Waxman and Thomas (1974), Kanciruk et al. (1986), Metrohm's *Conductometry - Conductivity Measurement*, Girault (2004), and Mavko, Mukerji and Dvorkin (2009).