diff --git a/src/parcels/_core/spatialhash.py b/src/parcels/_core/spatialhash.py index 208cbfe95..b3e6aa677 100644 --- a/src/parcels/_core/spatialhash.py +++ b/src/parcels/_core/spatialhash.py @@ -1,4 +1,6 @@ +import sys import warnings +from typing import IO import numpy as np @@ -10,6 +12,7 @@ ) from parcels._core.warnings import FieldSetWarning from parcels._python import isinstance_noimport +from parcels._reprs import spatialhash_describe # Budget on the total number of (face, hash cell) pairs in the hash table: # max(_HASH_ENTRIES_PER_FACE * nfaces, _HASH_ENTRY_BUDGET_MIN). @@ -524,6 +527,22 @@ def query(self, y, x): coords_best.reshape((num_queries, coordinates.shape[1])), ) + def describe(self, buf: IO | None = None) -> None: + """ + Summary of the SpatialHash's hash-table statistics (resolution, occupancy, + entry counts). + + Parameters + ---------- + buf : file-like, default: sys.stdout + writable buffer + """ + if buf is None: + buf = sys.stdout + assert buf is not None + + buf.write(spatialhash_describe(self)) + def _dilate_bits(n): """ diff --git a/src/parcels/_reprs.py b/src/parcels/_reprs.py index 5a6ae2944..1eb1bde0d 100644 --- a/src/parcels/_reprs.py +++ b/src/parcels/_reprs.py @@ -18,6 +18,7 @@ from parcels import Field, FieldSet, ParticleSet from parcels._core.field import VectorField from parcels._core.model import ModelData + from parcels._core.spatialhash import SpatialHash from parcels._core.utils.time import TimeInterval @@ -280,6 +281,36 @@ def fieldset_describe(fieldset: FieldSet) -> str: ) +def spatialhash_describe(spatialhash: SpatialHash) -> str: + grid = spatialhash._source_grid + hash_table = spatialhash._hash_table + counts = hash_table["counts"] + + n_faces = int(np.size(spatialhash._xlow)) + n_entries = int(hash_table["faces"].size) + n_occupied_cells = int(hash_table["keys"].size) + n_total_cells = (spatialhash._bitwidth + 1) ** 3 + + rows = { + "Grid type": type(grid).__name__, + "Mesh": "spherical" if grid._mesh.is_spherical() else "flat", + "Total Mesh Faces": str(n_faces), + "Bitwidth (current / max)": f"{spatialhash._bitwidth} / 1023 (higher = finer resolution hash grid)", + "Total hash cells": str(n_total_cells), + "Occupied hash cells": str(n_occupied_cells), + "Total (cell --> face) entries": str(n_entries), + "Entries per occupied cell (avg)": f"{n_entries / n_occupied_cells:.2f}" if n_occupied_cells else "-", + "Entries per face (avg)": f"{n_entries / n_faces:.2f}" if n_faces else "-", + "Faces per occupied cell (min / mean / max)": ( + f"{counts.min()} / {counts.mean():.2f} / {counts.max()}" if n_occupied_cells else "-" + ), + } + key_width = max(len(k) for k in rows) + table = "\n".join(f"{k.ljust(key_width)} : {v}" for k, v in rows.items()) + + return "Spatial Hash Grid Statistics" + "\n" + table + "\n" + + def _get_parent_model(field: Field | VectorField) -> ModelData: if isinstance_noimport(field, "Field"): return field.model # type:ignore[union-attr]