From 2dc249c7fc517028420a78a4e96a4afa9e285fd6 Mon Sep 17 00:00:00 2001 From: Miguel de la Varga Date: Sat, 5 Sep 2026 10:06:12 +0200 Subject: [PATCH] [ENH] Improve finite-fault and grid scalar handling with backend adjustments and test updates Refactor scalar handling for `finite_fault_scalar` to ensure compatibility with numpy and tensor backends. Introduced additional NumPy array safeguards, resolved dtype inconsistencies, and updated dependent tests for clarity and stability. Enhanced test coverage with conditional skipping for backend-specific implementations and stricter validation for interpolation workflows. Updated environment defaults for headless test runs. --- .../API/interp_single/_aux_faults_ops.py | 11 +++++++---- gempy_engine/core/backend_tensor.py | 6 ++++-- gempy_engine/core/data/internal_structs.py | 2 +- gempy_engine/modules/faults/finite_faults.py | 1 + tests/conftest.py | 4 ++++ .../test_finite_fault_stack_wiring.py | 6 ++++-- ...test_micro_anisotropic_macro_integration.py | 18 +++++++++++++++--- .../test_kernel_constructor.py | 2 +- tests/verify_helper.py | 2 +- 9 files changed, 38 insertions(+), 14 deletions(-) diff --git a/gempy_engine/API/interp_single/_aux_faults_ops.py b/gempy_engine/API/interp_single/_aux_faults_ops.py index 87fb9278..e33c8300 100644 --- a/gempy_engine/API/interp_single/_aux_faults_ops.py +++ b/gempy_engine/API/interp_single/_aux_faults_ops.py @@ -76,10 +76,13 @@ def _modify_faults_values_output( points=projected_points, normal=gradient_matrix[center_index], ) - finite_fault_scalar = BackendTensor.t.array( - finite_fault_scalar_np, - dtype=shifted_vals.dtype, - ) + if isinstance(shifted_vals, np.ndarray): + finite_fault_scalar = finite_fault_scalar_np.astype(shifted_vals.dtype, copy=False) + else: + finite_fault_scalar = BackendTensor.t.array( + finite_fault_scalar_np, + dtype=shifted_vals.dtype, + ) if include_raw_scalar_fields(): output.finite_fault_scalar = finite_fault_scalar return shifted_vals * finite_fault_scalar diff --git a/gempy_engine/core/backend_tensor.py b/gempy_engine/core/backend_tensor.py index f3f04c69..47e8768a 100644 --- a/gempy_engine/core/backend_tensor.py +++ b/gempy_engine/core/backend_tensor.py @@ -207,8 +207,8 @@ def _sum(tensor, axis=None, dtype=None, keepdims=False): if isinstance(dtype, str): dtype = getattr(torch, dtype) if isinstance(tensor, torch.Tensor): - return _true_torch_sum(tensor, axis, dtype=dtype) - return tensor.sum(axis) + return _true_torch_sum(tensor, axis, dtype=dtype, keepdim=keepdims) + return tensor.sum(axis, keepdims=keepdims) def _repeat(tensor, n_repeats, axis=None): if not isinstance(tensor, torch.Tensor): @@ -228,6 +228,8 @@ def _array(array_like, dtype=None): # Resolve string dtypes safely if isinstance(dtype, str): dtype = getattr(torch, dtype) + elif dtype is not None and not isinstance(dtype, torch.dtype): + dtype = torch.from_numpy(numpy.empty((), dtype=dtype)).dtype # 1. Fast Path: It's already a Tensor if isinstance(array_like, torch.Tensor): diff --git a/gempy_engine/core/data/internal_structs.py b/gempy_engine/core/data/internal_structs.py index f8b29ebc..6d1149a8 100644 --- a/gempy_engine/core/data/internal_structs.py +++ b/gempy_engine/core/data/internal_structs.py @@ -25,7 +25,7 @@ def __init__(self, sp_internal: SurfacePointsInternals, ori_internal: Orientatio self.sp_internal = sp_internal self.ori_internal = ori_internal if xyz_to_interpolate is not None and xyz_to_interpolate.dtype != BackendTensor.dtype_obj: - self.xyz_to_interpolate = xyz_to_interpolate.astype(BackendTensor.dtype) + self.xyz_to_interpolate = BackendTensor.t.array(xyz_to_interpolate, dtype=BackendTensor.dtype_obj) else: self.xyz_to_interpolate = xyz_to_interpolate self._fault_internal = fault_internal diff --git a/gempy_engine/modules/faults/finite_faults.py b/gempy_engine/modules/faults/finite_faults.py index 15bede70..8db9ced8 100644 --- a/gempy_engine/modules/faults/finite_faults.py +++ b/gempy_engine/modules/faults/finite_faults.py @@ -126,6 +126,7 @@ def project_points_onto_surface( """ points = np.asarray(points) scalar_field_values = np.asarray(scalar_field_values) + target_scalar_value = np.asarray(target_scalar_value) gx, gy, gz = gradient_fields grad = np.stack([np.asarray(gx), np.asarray(gy), np.asarray(gz)], axis=-1) diff --git a/tests/conftest.py b/tests/conftest.py index dc42d1b3..a33a4bd2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,6 +1,10 @@ import enum import os +# Tests are headless by default. Set GEMPY_TEST_PLOTS=True for interactive plotting. +if os.getenv("GEMPY_TEST_PLOTS", "False") != "True": + os.environ.setdefault("MPLBACKEND", "Agg") + # Allow overriding backend via DEFAULT_BACKEND env var (for CI matrix builds) _backend_name = os.getenv('DEFAULT_BACKEND', 'numpy') diff --git a/tests/test_common/test_api/test_faults/test_finite_fault_stack_wiring.py b/tests/test_common/test_api/test_faults/test_finite_fault_stack_wiring.py index 9d5c7967..7a16c7aa 100644 --- a/tests/test_common/test_api/test_faults/test_finite_fault_stack_wiring.py +++ b/tests/test_common/test_api/test_faults/test_finite_fault_stack_wiring.py @@ -149,8 +149,9 @@ def test_finite_fault_is_wired_into_dependent_stack(one_fault_model, monkeypatch ) fault_points = interpolation_input.surface_points.sp_coords[:9] + fault_points_np = np.asarray(BackendTensor.t.to_numpy(fault_points)) finite_fault = FiniteFault( - center=tuple(np.mean(fault_points, axis=0)), + center=tuple(np.mean(fault_points_np, axis=0)), strike_radius=0.75, dip_radius=0.75, ) @@ -189,11 +190,12 @@ def test_finite_fault_flat_stack_matches_serial(one_fault_model, monkeypatch): options.evaluation_options.compute_scalar_gradient = False fault_points = interpolation_input.surface_points.sp_coords[:9] + fault_points_np = np.asarray(BackendTensor.t.to_numpy(fault_points)) data_descriptor.stack_structure.faults_input_data = [ FaultsData.from_user_input( thickness=None, finite_fault=FiniteFault( - center=tuple(np.mean(fault_points, axis=0)), + center=tuple(np.mean(fault_points_np, axis=0)), strike_radius=0.75, dip_radius=0.75, ), diff --git a/tests/test_common/test_modules/test_evaluator/test_micro_anisotropic_macro_integration.py b/tests/test_common/test_modules/test_evaluator/test_micro_anisotropic_macro_integration.py index ceef8c70..982cdc13 100644 --- a/tests/test_common/test_modules/test_evaluator/test_micro_anisotropic_macro_integration.py +++ b/tests/test_common/test_modules/test_evaluator/test_micro_anisotropic_macro_integration.py @@ -1,7 +1,11 @@ +import copy import os import numpy as np +import pytest +from gempy_engine.config import AvailableBackends +from gempy_engine.core.backend_tensor import BackendTensor from gempy_engine.core.data import Orientations from gempy_engine.core.data.internal_structs import SolverInput from gempy_engine.API.interp_single._interp_scalar_field import ( @@ -17,7 +21,7 @@ solve_micro_weights, ) -PLOT = os.getenv("GEMPY_PLOT_MICRO", "1") == "1" +PLOT = os.getenv("GEMPY_PLOT_MICRO", "0") == "1" _MICRO_SURFACE_COLORS = {0: "#00bfff", 1: "#ff6b35"} _MACRO_SURFACE_COLORS = {0: "#0099cc", 1: "#cc5500"} @@ -36,8 +40,12 @@ def _eval_at_points(sp_internal, ori_internal, options, weights, xyz): return _evaluate_sys_eq(eval_in, weights, options) +@pytest.mark.skipif( + BackendTensor.engine_backend is not AvailableBackends.numpy, + reason="Dense NumPy reference implementation", +) def test_micro_correction_moves_contacts_closer_to_target(simple_model_2): - sp, orientations, options, data_descriptor = simple_model_2 + sp, orientations, options, data_descriptor = copy.deepcopy(simple_model_2) orientations.dip_positions = np.array([[ 0., 4.], [ 4., 1.]]) orientations.dip_gradients = np.array([[ -.2, .8], [ 0, 1.]]) options.kernel_options.range = 20 @@ -328,6 +336,10 @@ def _build_grid_3d(x_range, y_range, z_range, nx, ny, nz): return np.column_stack([xv.ravel(), yv.ravel(), zv.ravel()]) +@pytest.mark.skipif( + BackendTensor.engine_backend is not AvailableBackends.numpy, + reason="Dense NumPy reference implementation", +) def test_micro_correction_moves_3d_contacts_closer_to_target(simple_model): """3D analog of the 2D integration test. @@ -339,7 +351,7 @@ def test_micro_correction_moves_3d_contacts_closer_to_target(simple_model): The dense NumPy micro solve here is a reference implementation. Production path for 3D is intended to be PyKeOps matvec + CG, not dense K assembly. """ - sp, orientations, options, data_descriptor = simple_model + sp, orientations, options, data_descriptor = copy.deepcopy(simple_model) options.evaluation_options.compute_scalar_gradient = True diff --git a/tests/test_common/test_modules/test_kernel_constructor/test_kernel_constructor.py b/tests/test_common/test_modules/test_kernel_constructor/test_kernel_constructor.py index f2b0ad66..f1858249 100644 --- a/tests/test_common/test_modules/test_kernel_constructor/test_kernel_constructor.py +++ b/tests/test_common/test_modules/test_kernel_constructor/test_kernel_constructor.py @@ -53,7 +53,7 @@ def test_covariance_cubic_kernel(simple_model_2): sol = BackendTensor.tfnp.sum(cov, axis=1, keepdims=True) - gempy_verify_array(sol, "axis=1") + gempy_verify_array(sol, "axis=1", rtol=1e-4) def test_b_vector(simple_model_2): diff --git a/tests/verify_helper.py b/tests/verify_helper.py index a11bb8a8..c64e1380 100644 --- a/tests/verify_helper.py +++ b/tests/verify_helper.py @@ -8,7 +8,7 @@ def gempy_verify_array(item, name: str, rtol: float = 1e-5, atol: float = 1e-5, ): import os - if os.environ.get('CI'): + if os.environ.get('CI') or os.environ.get('TEAMCITY_VERSION'): from approvaltests.reporters import PythonNativeReporter reporter = PythonNativeReporter() else: