1111import subprocess
1212import tempfile
1313from dataclasses import dataclass , field
14- from typing import Any , Optional
14+ from pathlib import Path
15+ from typing import TYPE_CHECKING , Any
1516
1617import numpy as np
17- from numpy .typing import NDArray
1818
1919from khisto import KHISTO_BIN_DIR , logger
2020
21+ if TYPE_CHECKING :
22+ from numpy .typing import NDArray
23+
2124
2225@dataclass
2326class _HistogramPayload :
@@ -68,18 +71,18 @@ class _KhistoOutput:
6871
6972 tool : str = ""
7073 version : str = ""
71- bestHistogram : Optional [ _HistogramPayload ] = None
72- histogramSeries : Optional [ _SeriesPayload ] = None
74+ bestHistogram : _HistogramPayload = field ( default_factory = _HistogramPayload )
75+ histogramSeries : _SeriesPayload = field ( default_factory = _SeriesPayload )
7376
7477 @classmethod
7578 def from_dict (cls , data : dict [str , Any ]) -> _KhistoOutput :
76- best = data . get ( "bestHistogram" )
77- series = data . get ( " histogramSeries" )
79+ if "bestHistogram" not in data or "histogramSeries" not in data :
80+ raise ValueError ( "Missing required fields: bestHistogram, histogramSeries" )
7881 return cls (
7982 tool = data .get ("tool" , "" ),
8083 version = data .get ("version" , "" ),
81- bestHistogram = _HistogramPayload .from_dict (best ) if best else None ,
82- histogramSeries = _SeriesPayload .from_dict (series ) if series else None ,
84+ bestHistogram = _HistogramPayload .from_dict (data [ "bestHistogram" ]) ,
85+ histogramSeries = _SeriesPayload .from_dict (data [ "histogramSeries" ]) ,
8386 )
8487
8588
@@ -128,9 +131,42 @@ class HistogramResult:
128131 spike_interval_number : int = 0
129132 empty_interval_number : int = 0
130133
134+ def __post_init__ (self ) -> None :
135+ """Validate the histogram data."""
136+
137+ # Verify all arrays have the same length
138+ n_bins = {
139+ len (self .lower_bounds ),
140+ len (self .upper_bounds ),
141+ len (self .frequencies ),
142+ len (self .probabilities ),
143+ len (self .densities ),
144+ }
145+ if len (n_bins ) != 1 :
146+ raise ValueError ("all array sizes must be equal" )
147+ elif len (self .lower_bounds ) < 1 :
148+ raise ValueError ("all arrays must have at least one element" )
149+
150+ if not np .all (self .lower_bounds <= self .upper_bounds ):
151+ raise ValueError ("lower_bounds must be less than upper_bounds" )
152+ if not np .all (self .frequencies >= 0 ):
153+ raise ValueError ("frequencies must be non-negative" )
154+ if not np .all (self .probabilities >= 0 ):
155+ raise ValueError ("probabilities must be non-negative" )
156+ if not np .all (self .densities >= 0 ):
157+ raise ValueError ("densities must be non-negative" )
158+
159+ # Verify lower_bounds are equal to upper_bounds for adjacent bins
160+ print (self .lower_bounds , self .upper_bounds )
161+ if not np .all (self .lower_bounds [1 :] == self .upper_bounds [:- 1 ]):
162+ raise ValueError (
163+ "lower_bounds must be equal to upper_bounds for adjacent bins"
164+ )
165+
131166 @property
132167 def bin_edges (self ) -> NDArray [np .float64 ]:
133- """Return bin edges array (n_bins + 1 values)."""
168+ """Return bin edges array (n_bins + 1 values).
169+ lower_bounds and upper_bounds are equal for adjacent bins."""
134170 return np .concatenate ([self .lower_bounds , [self .upper_bounds [- 1 ]]])
135171
136172 @property
@@ -148,18 +184,6 @@ def __len__(self) -> int:
148184 return len (self .lower_bounds )
149185
150186
151- def _to_result (h : _HistogramPayload , ** kwargs : Any ) -> HistogramResult :
152- """Convert a JSON histogram payload to a HistogramResult."""
153- return HistogramResult (
154- lower_bounds = np .asarray (h .lowerBounds , dtype = np .float64 ),
155- upper_bounds = np .asarray (h .upperBounds , dtype = np .float64 ),
156- frequencies = np .asarray (h .frequencies , dtype = np .int64 ),
157- probabilities = np .asarray (h .probabilities , dtype = np .float64 ),
158- densities = np .asarray (h .densities , dtype = np .float64 ),
159- ** kwargs ,
160- )
161-
162-
163187def _format_runtime_error (
164188 summary : str , cmd : list [str ], details : str | None = None
165189) -> str :
@@ -170,21 +194,21 @@ def _format_runtime_error(
170194 return message
171195
172196
173- def _process_histogram_file (
174- file_path : str ,
175- ) -> list [HistogramResult ]:
197+ def _process_histogram_file (file_path : Path ) -> list [HistogramResult ]:
176198 """Process exploratory JSON generated by khisto CLI."""
177- with open (file_path , "r" ) as temp_output_file :
178- khisto_output : _KhistoOutput = _KhistoOutput .from_dict (
179- json .load (temp_output_file )
180- )
199+ with open (file_path , "r" ) as file :
200+ khisto_output : _KhistoOutput = _KhistoOutput .from_dict (json .load (file ))
181201
182202 histogram_series = khisto_output .histogramSeries
183203 best_idx = histogram_series .interpretableHistogramNumber - 1
184204
185205 return [
186- _to_result (
187- h ,
206+ HistogramResult (
207+ lower_bounds = np .asarray (h .lowerBounds , dtype = np .float64 ),
208+ upper_bounds = np .asarray (h .upperBounds , dtype = np .float64 ),
209+ frequencies = np .asarray (h .frequencies , dtype = np .int64 ),
210+ probabilities = np .asarray (h .probabilities , dtype = np .float64 ),
211+ densities = np .asarray (h .densities , dtype = np .float64 ),
188212 is_best = (i == best_idx ),
189213 granularity = histogram_series .granularities [i ],
190214 level = histogram_series .levels [i ],
@@ -268,7 +292,7 @@ def compute_histograms(x: np.ndarray) -> list[HistogramResult]:
268292 raise RuntimeError (message ) from e
269293
270294 try :
271- return _process_histogram_file (temp_output_file .name )
295+ return _process_histogram_file (Path ( temp_output_file .name ) )
272296 except json .JSONDecodeError as e :
273297 message = _format_runtime_error (
274298 "khisto produced invalid JSON output" ,
0 commit comments