geoforce / solver /grid.py
Ubuntu
GeoForce-Solver pressure solver passes Theis benchmark (0.38% err)
2b07453
"""Structured 2D grid for GeoForce-Solver.
The grid is logically rectangular with uniform spacing in each direction.
Two conventional orientations are supported:
* **Horizontal (x, y):** used by the Theis benchmark (no gravity).
* **Vertical (x, z):** used by real geothermal demos (gravity + density).
The grid object itself is orientation-agnostic — it just provides cell
volumes, face areas, inter-cell distances, and center coordinates.
The PDE modules (darcy.py, energy.py) choose whether to enable gravity
based on the scenario dictionary, not on the grid type.
For a 2D slab simulation, we fix the out-of-plane thickness to 1 m, so
"volumetric" and "per-unit-thickness" rates coincide.
"""
from __future__ import annotations
from dataclasses import dataclass
import numpy as np
THICKNESS = 1.0 # meters; out-of-plane extent of the 2D slab
@dataclass(frozen=True)
class Grid:
"""Uniform structured 2D grid.
Attributes:
nx: number of cells along the first axis.
ny: number of cells along the second axis (y or z depending on orientation).
dx: cell size along axis 0 (m).
dy: cell size along axis 1 (m).
"""
nx: int
ny: int
dx: float
dy: float
@property
def shape(self) -> tuple[int, int]:
return (self.nx, self.ny)
@property
def n_cells(self) -> int:
return self.nx * self.ny
@property
def cell_volume(self) -> float:
"""Constant cell volume (m^3) assuming unit out-of-plane thickness."""
return self.dx * self.dy * THICKNESS
@property
def face_area_x(self) -> float:
"""Area of a face normal to axis 0 (m^2)."""
return self.dy * THICKNESS
@property
def face_area_y(self) -> float:
"""Area of a face normal to axis 1 (m^2)."""
return self.dx * THICKNESS
def cell_centers(self) -> tuple[np.ndarray, np.ndarray]:
"""Return (X, Y) coordinate arrays of shape (nx, ny) at cell centers."""
x = (np.arange(self.nx) + 0.5) * self.dx
y = (np.arange(self.ny) + 0.5) * self.dy
X, Y = np.meshgrid(x, y, indexing="ij")
return X, Y
def flat_index(self, i: int, j: int) -> int:
"""Row-major flattening: flat = i * ny + j."""
return i * self.ny + j
def radial_distance_from(self, i0: int, j0: int) -> np.ndarray:
"""Return an (nx, ny) array of distances from the center of cell (i0, j0)."""
X, Y = self.cell_centers()
x0 = (i0 + 0.5) * self.dx
y0 = (j0 + 0.5) * self.dy
return np.sqrt((X - x0) ** 2 + (Y - y0) ** 2)