@@ -1095,32 +1095,51 @@ def plot_climate_filled_hull(
10951095 verts = np .asarray (hull .verts_km )
10961096 tris = np .asarray (hull .tris )
10971097
1098- # Scalar values are attached per-vertex by ring/time-layer order (not by
1099- # absolute z value). This keeps the climate series aligned with mesh layout
1100- # even when t_day uses non-consecutive values.
1098+ n_vertices = int (verts .shape [0 ])
1099+ layer_days = np .unique (np .asarray (hull .t_days_vert , dtype = float ))
1100+ n_layers = int (layer_days .size )
1101+ if n_layers <= 0 :
1102+ raise ValueError ("TimeHull has no time layers to color." )
1103+ if n_vertices % n_layers != 0 :
1104+ raise ValueError (
1105+ "Cannot align hull vertices to time layers: "
1106+ f"{ n_vertices } vertices not divisible by { n_layers } layers."
1107+ )
1108+ verts_per_layer = n_vertices // n_layers
1109+
1110+ # Scalar values are attached per-vertex, expanded from per-layer climate
1111+ # means using the same ring/time layer order used to build verts_km.
11011112 intensities = None
11021113 if isinstance (summary , HullClimateSummary ) and summary .per_day_mean .size :
1103- day_vals = np .asarray (summary .per_day_mean .sort_index ().values , dtype = float )
1104- n_vertices = int (verts .shape [0 ])
1105- M = int (round (hull .metrics .get ("days" , 0 ) or 0 ))
1106- if M <= 0 and day_vals .size :
1107- M = int (day_vals .size )
1108- if M > 0 and day_vals .size and n_vertices % M == 0 :
1109- verts_per_layer = n_vertices // M
1110- if len (day_vals ) < M :
1111- day_vals = np .pad (day_vals , (0 , M - len (day_vals )), mode = "edge" )
1112- elif len (day_vals ) > M :
1113- day_vals = day_vals [:M ]
1114- intensities = np .repeat (day_vals , verts_per_layer )
1115- if intensities .shape [0 ] != n_vertices :
1116- raise ValueError (
1117- "Hull climate scalar/vertex mismatch: "
1118- f"{ intensities .shape [0 ]} intensities for { n_vertices } vertices."
1119- )
1114+ per_day = summary .per_day_mean .copy ()
1115+ per_day .index = normalize_dates (per_day .index )
1116+ per_day = per_day .sort_index ()
1117+
1118+ # Map each hull time layer (event_day-like z) to a calendar date so
1119+ # climate values are selected by the true layer date, not by truncating
1120+ # an arbitrarily sorted climate series.
1121+ layer_date_index = normalize_dates (
1122+ hull .event .t0 + pd .to_timedelta (layer_days - np .nanmin (layer_days ), unit = "D" )
1123+ )
1124+ layer_vals = per_day .reindex (layer_date_index )
1125+ if layer_vals .isna ().any ():
1126+ # Prefer forward-fill because perimeters are cumulative in time; if
1127+ # the earliest layer is missing, backfill once to avoid all-NaN.
1128+ layer_vals = layer_vals .ffill ().bfill ()
1129+ day_vals = np .asarray (layer_vals .values , dtype = float )
1130+ intensities = np .repeat (day_vals , verts_per_layer )
1131+ if intensities .shape [0 ] != n_vertices :
1132+ raise ValueError (
1133+ "Hull climate scalar/vertex mismatch: "
1134+ f"{ intensities .shape [0 ]} intensities for { n_vertices } vertices."
1135+ )
11201136
11211137 # Developer diagnostic mode: color by z/time to confirm vertical banding.
11221138 if scalar_debug_mode == "z" :
11231139 intensities = verts [:, 2 ].astype (float )
1140+ elif scalar_debug_mode == "slice" :
1141+ # Diagnostic mode to verify explicit slice->vertex alignment.
1142+ intensities = np .repeat (np .arange (n_layers , dtype = float ), verts_per_layer )
11241143
11251144 if intensities is not None :
11261145 finite = intensities [np .isfinite (intensities )]
@@ -1147,11 +1166,15 @@ def plot_climate_filled_hull(
11471166 "verts_shape" : tuple (verts .shape ),
11481167 "tris_shape" : tuple (tris .shape ),
11491168 "scalar_shape" : tuple (intensities .shape ),
1169+ "scalar_dtype" : str (intensities .dtype ),
11501170 "nan_count" : int (np .isnan (intensities ).sum ()),
1171+ "unique_count" : int (np .unique (intensities [np .isfinite (intensities )]).size ),
11511172 "min" : float (np .nanmin (finite )) if finite .size else float ("nan" ),
11521173 "max" : float (np .nanmax (finite )) if finite .size else float ("nan" ),
11531174 "percentiles" : dict (zip ([str (p ) for p in pct ], pct_vals )),
11541175 "mode" : scalar_debug_mode or "climate" ,
1176+ "n_layers" : n_layers ,
1177+ "verts_per_layer" : verts_per_layer ,
11551178 },
11561179 )
11571180
0 commit comments