| """Unified `predict(scenario)` wrapper around GeoForce-Solver. |
| |
| The agent calls this with a plain dict describing the reservoir + wells + |
| time stepping, and gets back a common schema: |
| |
| { |
| "temperature": np.ndarray, # (nx, ny) deg C, final time step |
| "pressure": np.ndarray, # (nx, ny) Pa, final time step |
| "grid": { |
| "nx": int, "ny": int, |
| "dx": float, "dy": float, # meters |
| "extent_x": [x_min, x_max], # meters (cell edges) |
| "extent_y": [y_min, y_max], |
| }, |
| "engine": "solver", |
| "elapsed_seconds": float, |
| } |
| |
| The scenario dict may omit most keys; sensible defaults match the Indonesian |
| geothermal scenarios we demo. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import time |
| from typing import Any |
|
|
| import numpy as np |
|
|
| from solver.coupled import run_coupled_transient |
| from solver.grid import Grid |
| from solver.properties import WaterProperties |
| from solver.wells import WellSpec |
|
|
| DEFAULTS: dict[str, Any] = { |
| "nx": 40, |
| "ny": 20, |
| "dx": 10.0, |
| "dy": 10.0, |
| "porosity": 0.12, |
| "permeability": 5.0e-13, |
| "rho_rock": 2500.0, |
| "cp_rock": 1000.0, |
| "lam_rock": 2.5, |
| "T_initial": 200.0, |
| "P_initial": 1.5e7, |
| "dt": 5.0e5, |
| "n_steps": 60, |
| "wells": [], |
| } |
|
|
|
|
| def _coerce_wells(raw: list[dict[str, Any]]) -> list[WellSpec]: |
| out: list[WellSpec] = [] |
| for w in raw: |
| out.append( |
| WellSpec( |
| i=int(w["i"]), |
| j=int(w["j"]), |
| mass_rate=float(w["mass_rate"]), |
| injection_temperature=( |
| float(w["injection_temperature"]) |
| if w.get("injection_temperature") is not None |
| else None |
| ), |
| ) |
| ) |
| return out |
|
|
|
|
| def predict(scenario: dict[str, Any]) -> dict[str, Any]: |
| """Run the coupled GeoForce solver on a scenario dict. |
| |
| Args: |
| scenario: dict with any subset of the DEFAULTS keys overridden. |
| Special keys: |
| - ``wells``: list of {i, j, mass_rate, injection_temperature?}. |
| |
| Returns: |
| Dict with final-time T/P fields + grid metadata + ``engine``. |
| """ |
| cfg = {**DEFAULTS, **scenario} |
|
|
| grid = Grid(nx=int(cfg["nx"]), ny=int(cfg["ny"]), dx=float(cfg["dx"]), dy=float(cfg["dy"])) |
|
|
| props = WaterProperties.at_reference(t_c=float(cfg["T_initial"]), p_pa=float(cfg["P_initial"])) |
|
|
| phi = float(cfg["porosity"]) |
| rho_cp_eff = (1.0 - phi) * float(cfg["rho_rock"]) * float(cfg["cp_rock"]) + phi * props.rho * props.cp |
| lam_eff = (1.0 - phi) * float(cfg["lam_rock"]) + phi * 0.68 |
|
|
| t0 = np.full(grid.shape, float(cfg["T_initial"]), dtype=np.float64) |
| p0 = np.full(grid.shape, float(cfg["P_initial"]), dtype=np.float64) |
|
|
| wells = _coerce_wells(list(cfg["wells"])) |
|
|
| start = time.perf_counter() |
| p_hist, t_hist = run_coupled_transient( |
| grid=grid, |
| p_initial=p0, |
| t_initial=t0, |
| permeability=float(cfg["permeability"]), |
| porosity=phi, |
| total_compressibility=props.c_f + 1.0e-9, |
| mu=props.mu, |
| rho=props.rho, |
| cp_water=props.cp, |
| thermal_conductivity=lam_eff, |
| volumetric_heat_capacity=rho_cp_eff, |
| dt=float(cfg["dt"]), |
| n_steps=int(cfg["n_steps"]), |
| wells=wells, |
| ) |
| elapsed = time.perf_counter() - start |
|
|
| return { |
| "temperature": t_hist[-1], |
| "pressure": p_hist[-1], |
| "grid": { |
| "nx": grid.nx, |
| "ny": grid.ny, |
| "dx": grid.dx, |
| "dy": grid.dy, |
| "extent_x": [0.0, grid.nx * grid.dx], |
| "extent_y": [0.0, grid.ny * grid.dy], |
| }, |
| "engine": "solver", |
| "elapsed_seconds": elapsed, |
| } |
|
|