Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 41 additions & 6 deletions src/cubedynamics/fire_time_hull.py
Original file line number Diff line number Diff line change
Expand Up @@ -1092,14 +1092,50 @@ def plot_climate_filled_hull(
scalar_debug_mode: Optional[str] = None,
debug: bool = False,
) -> go.Figure:
def _build_vertex_slice_index() -> tuple[np.ndarray, np.ndarray, dict[str, Any]]:
t_days_vert = np.asarray(hull.t_days_vert, dtype=float)
if t_days_vert.shape[0] != verts.shape[0]:
def _sanitize_mesh_inputs(
verts_in: np.ndarray,
tris_in: np.ndarray,
t_days_in: np.ndarray,
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
verts_arr = np.asarray(verts_in, dtype=float)
tris_arr = np.asarray(tris_in, dtype=int)
t_days_arr = np.asarray(t_days_in, dtype=float)

if verts_arr.ndim != 2 or verts_arr.shape[1] != 3:
raise ValueError("TimeHull verts_km must have shape (N, 3).")
if tris_arr.ndim != 2 or tris_arr.shape[1] != 3:
raise ValueError("TimeHull tris must have shape (M, 3).")
if t_days_arr.shape[0] != verts_arr.shape[0]:
raise ValueError(
"Per-vertex time coordinate mismatch: "
f"{t_days_vert.shape[0]} t_days values for {verts.shape[0]} vertices."
f"{t_days_arr.shape[0]} t_days values for {verts_arr.shape[0]} vertices."
)

valid_vertex_mask = np.isfinite(verts_arr).all(axis=1) & np.isfinite(t_days_arr)
if not np.all(valid_vertex_mask):
old_to_new = np.full(verts_arr.shape[0], -1, dtype=int)
old_to_new[valid_vertex_mask] = np.arange(int(valid_vertex_mask.sum()))
verts_arr = verts_arr[valid_vertex_mask]
t_days_arr = t_days_arr[valid_vertex_mask]
remapped = old_to_new[tris_arr]
tri_valid = (remapped >= 0).all(axis=1)
tris_arr = remapped[tri_valid]

if verts_arr.shape[0] == 0:
raise ValueError("TimeHull contains no finite vertices to render.")
if tris_arr.shape[0] == 0:
raise ValueError("TimeHull contains no valid triangles to render.")

tri_bounds_ok = (tris_arr >= 0).all() and (tris_arr < verts_arr.shape[0]).all()
if not tri_bounds_ok:
tri_valid = ((tris_arr >= 0) & (tris_arr < verts_arr.shape[0])).all(axis=1)
tris_arr = tris_arr[tri_valid]
if tris_arr.shape[0] == 0:
raise ValueError("TimeHull contains no in-bounds triangles to render.")

return verts_arr, tris_arr, t_days_arr

def _build_vertex_slice_index() -> tuple[np.ndarray, np.ndarray, dict[str, Any]]:
layer_days, vertex_slice_index = np.unique(t_days_vert, return_inverse=True)
n_layers_local = int(layer_days.size)
if n_layers_local <= 0:
Expand Down Expand Up @@ -1166,8 +1202,7 @@ def _scalar_stats(values: np.ndarray) -> dict[str, Any]:
"approx_unique_count": int(np.unique(finite).size),
}

verts = np.asarray(hull.verts_km)
tris = np.asarray(hull.tris)
verts, tris, t_days_vert = _sanitize_mesh_inputs(hull.verts_km, hull.tris, hull.t_days_vert)
n_vertices = int(verts.shape[0])
layer_days, vertex_slice_index, alignment = _build_vertex_slice_index()
n_layers = int(layer_days.size)
Expand Down
36 changes: 36 additions & 0 deletions tests/test_fire_hull_viewer_scalars.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,39 @@ def test_plot_climate_filled_hull_debug_output_reports_stats_and_alignment(_plot
assert "face_alignment" in out
assert re.search(r"'min_slice_span':\s*1", out)
assert re.search(r"'max_slice_span':\s*1", out)


def test_plot_climate_filled_hull_drops_nonfinite_vertices_and_remaps_tris(_plotly_stub):
hull = _synthetic_hull(days=3, verts_per_layer=4)
verts = hull.verts_km.copy()
verts[0, 0] = np.nan
hull = TimeHull(
event=hull.event,
verts_km=verts,
tris=hull.tris,
t_days_vert=hull.t_days_vert,
t_norm_vert=hull.t_norm_vert,
metrics=hull.metrics,
)
summary = HullClimateSummary(
values_inside=np.array([1.0]),
values_outside=np.array([0.0]),
per_day_mean=pd.Series([1.0, 5.0, 9.0], index=pd.date_range("2020-07-01", periods=3, freq="D")),
)

fig = plot_climate_filled_hull(hull, summary, color_limits=None)

x = np.asarray(fig.data[0].x, dtype=float)
y = np.asarray(fig.data[0].y, dtype=float)
z = np.asarray(fig.data[0].z, dtype=float)
i = np.asarray(fig.data[0].i, dtype=int)
j = np.asarray(fig.data[0].j, dtype=int)
k = np.asarray(fig.data[0].k, dtype=int)
intensity = np.asarray(fig.data[0].intensity, dtype=float)

assert x.shape[0] == verts.shape[0] - 1
assert np.isfinite(x).all() and np.isfinite(y).all() and np.isfinite(z).all()
assert i.size > 0 and j.size > 0 and k.size > 0
assert (i >= 0).all() and (j >= 0).all() and (k >= 0).all()
assert (i < x.shape[0]).all() and (j < x.shape[0]).all() and (k < x.shape[0]).all()
assert intensity.shape[0] == x.shape[0]
Loading