Problem: Porosity and Oil Saturation Calculation

The problem involves creating Python functions to calculate porosity (from bulk, matrix, and fluid densities) and oil saturation (from porosity, water resistivity, and formation resistivity) with input validation, numerical stability checks, iterative refinement for accuracy, physical value clamping, and output type preservation to ensure reliable, physically meaningful petrophysical results.

Background: ### Industry Background & Technical Context (English)
These functions belong to **Reservoir Petrophysics & Well Logging Interpretation**, a core discipline in oil and gas exploration focused on evaluating subsurface rock properties to estimate hydrocarbon reserves. The background below explains the physical, mathematical, and industry-standard constraints critical for reproducing the code correctly:

---

#### 1. Porosity Calculation (`calculate_porosity`)
##### Core Industry & Mathematical Foundations
- **Physical Definition**: Porosity (φ) is the fraction of void space in rock, defined as the ratio of pore volume to total rock volume. It is a key parameter to estimate how much fluid a rock can store.
- **Standard Field Formula**: The function uses the density-derived porosity equation, the most common method for porosity calculation from well log data:
  \[
  \phi = \frac{\rho_{matrix} - \rho_{bulk}}{\rho_{matrix} - \rho_{fluid}}
  \]
  Where:
  - \(\rho_{bulk}\): Bulk density of the rock (measured via density well logs, unit: g/cm³)
  - \(\rho_{matrix}\): Matrix (grain) density of the rock’s solid framework (default 2.65 g/cm³ is the typical density of quartz, the dominant mineral in clean sandstone reservoirs)
  - \(\rho_{fluid}\): Density of the pore-filling fluid (default 1.0 g/cm³ for freshwater, the most common pore fluid in shallow reservoirs)
- **Non-Negotiable Industry Constraints**:
  - Physical validity: \(\rho_{matrix} > \rho_{fluid}\) must hold—solid rock grains are always denser than pore fluids, so violating this makes porosity geologically meaningless.
  - Numerical stability: The denominator \(\rho_{matrix} - \rho_{fluid}\) cannot be near-zero (prevents division by singular values caused by noisy log measurements or mismatched input units).
  - Bounds clamping: Porosity is clamped to [0,1] because it represents a volume fraction—values outside this range are impossible (negative porosity implies rock is denser than its framework; values >1 imply more void space than total rock volume).
- **Iterative Refinement**: Damped iterative correction is applied to reduce numerical errors from noisy field-measured geophysical data, a universal challenge in well log interpretation.

---

#### 2. Oil Saturation Calculation (`calculate_oil_saturation`)
##### Core Industry & Mathematical Foundations
- **Physical Definition**: Oil saturation (\(S_o\)) is the fraction of pore space occupied by hydrocarbon oil. It is calculated as \(S_o = 1 - S_w\), where \(S_w\) is water saturation (fraction of pore space filled with formation water).
- **Standard Field Formula**: Uses **Archie’s Law**, the gold-standard model for water saturation in clean (shale-free) hydrocarbon-bearing reservoirs:
  \[
  S_w = \left( \frac{a \cdot R_w}{\phi^m \cdot R_t} \right)^{1/n}
  \]
  For water-wet reservoirs (the most common subsurface scenario), \(n=2\) (implied in the function’s iterative root-finding). Key parameters:
  - \(\phi\): Porosity (from prior calculation, e.g., `calculate_porosity`)
  - \(R_w\): Formation water resistivity (measured via downhole samples or resistivity logs, unit: ohm·m)
  - \(R_t\): True formation resistivity (measured via deep resistivity logs, unit: ohm·m)
  - \(a\): Lithology factor (default 1.0 for clean, unconsolidated sandstones; varies with rock type, e.g., 0.8–0.9 for consolidated sandstones)
  - \(m\): Cementation exponent (default 2.0 for moderately cemented sandstones; describes how rock framework disrupts electrical current flow)
- **Non-Negotiable Industry Constraints**:
  - Physical validity: All input parameters (\(\phi, R_w, R_t, a, m\)) must be positive—resistivity and porosity cannot be zero or negative in valid reservoir rocks.
  - Iterative Root-Finding (Newton’s Method): Used to solve for \(S_w\) with high precision, especially when \(m\) is non-integer (common in complex lithologies like carbonates). Damped updates prevent divergence in noisy log data.
  - Saturation Clamping: \(S_w\) is clamped to [0,1] before calculating \(S_o\) to enforce physical realism. Invalid input values are masked out, with 0 returned for non-physical entries (a standard practice to handle bad log data).

---

#### Key Guidelines for Reproducing the Code
1. **Input Data Handling**: Geophysical data often comes in scalars, lists, tuples, or NumPy arrays—vectorized operations are critical for processing large well log datasets efficiently.
2. **Unit Consistency**: All density inputs must use g/cm³, and resistivity inputs must use ohm·m to ensure valid physical results.
3. **Default Parameter Rationale**: Default values are industry standards for clean sandstone reservoirs; adjust these for other lithologies (e.g., carbonate rocks use matrix density ~2.71 g/cm³, \(m=2.1–2.5\)).
4. **Robustness Practices**: Incorporate relative epsilon checks for near-zero denominators and iterative refinement to handle the noise inherent in field-measured geophysical data.

Input/Output: The problem receives bulk_density (scalar/array-like, numeric), matrix_density (scalar/array-like, numeric, optional, default 2.65), fluid_density (scalar/array-like, numeric, optional, default 1.0), rw (scalar/array-like, numeric), rt (scalar/array-like, numeric), a (scalar/array-like, numeric, optional, default 1), m (scalar/array-like, numeric, optional, default 2) as inputs. The problem first computes porosity from bulk_density, matrix_density, and fluid_density by converting to float arrays, validating numeric types and constraints (matrix>fluid, non-near-zero denominator), applying iterative refinement, clamping to [0,1], and returning a scalar or array matching input type. It then uses this porosity, along with rw, rt, a, and m, to compute oil saturation: validating inputs, creating a mask for physically valid entries, calculating water saturation via Archie's law with Newton's method for m-th root refinement, clamping water saturation to [0,1], deriving oil saturation as 1 minus water saturation, and returning a scalar or array matching the input type of the original inputs.

Required dependencies:
from typing import List, Tuple, Union
import numpy as np
import numbers
import math

Sub-steps:
step_number: 1
step_description_prompt: Please write a Python function named calculate_porosity. It accepts bulk_density (scalar/array-like), matrix_density (default 2.65), fluid_density (default1.0). Convert inputs to float arrays, validate numeric types and constraints (matrix>fluid, denominator not near zero). Compute porosity with iterative refinement, clamp to [0,1], return scalar/array as input type, raise proper errors for invalid cases.
function_header: def calculate_porosity(bulk_density, matrix_density=2.65, fluid_density=1.0):
test_cases: for bulk, matrix, fluid, expected in [
    (2.0, 2.65, 1.0, 0.3939393939393939),
    (1.65, 2.65, 1.0, 0.606060606060606060606),
    (2.65, 2.65, 1.0, 0.0)
]:
    # print(calculate_porosity(bulk, matrix, fluid))
    assert math.isclose(calculate_porosity(bulk, matrix, fluid), expected)

step_number: 2
step_description_prompt: Please write a Python function named calculate_oil_saturation. The function takes porosity, rw, rt (optional a, m), validates input types and converts to float arrays, creates a mask for physically valid entries, computes water saturation using Archie's law with Newton's method for m-th root refinement, clamps values to bounds, calculates oil saturation as 1 minus water saturation, and returns scalar or array matching input type.
function_header: def calculate_oil_saturation(porosity, rw, rt, a=1, m=2):
test_cases: for phi, rw, rt, a, m, expected in [
    (0.2, 0.1, 10.0, 1.0, 2.0, 1.0 - math.sqrt((1.0 * 0.1) / ((0.2 ** 2) * 10.0))),
    (0.25, 0.2, 8.0, 1.0, 2.0, 1.0 - math.sqrt((1.0 * 0.2) / ((0.25 ** 2) * 8.0))),
    (0.1, 1.0, 1.0, 1.0, 2.0, 0.0)  # Sw > 1 → capped → So = 0
]:
    assert math.isclose(calculate_oil_saturation(phi, rw, rt, a, m), expected, abs_tol=1e-9)

General solution:

General tests: