this repo has no description

tessera-viz: scaffold with percentile utilities

Add the tessera-viz library skeleton with dune-project, .mli interface,
and a working percentile function with linear interpolation. Other API
functions are stubbed with failwith for TDD iteration.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+123
+12
tessera-viz/dune-project
··· 1 + (lang dune 3.17) 2 + (name tessera-viz) 3 + (generate_opam_files true) 4 + (license ISC) 5 + (package 6 + (name tessera-viz) 7 + (synopsis "Visualization utilities for GeoTessera embeddings") 8 + (description "Convert PCA components and classification results to RGBA pixel arrays. Portable — no platform-specific encoding.") 9 + (depends 10 + (ocaml (>= 5.2)) 11 + (tessera-linalg (>= 0.1)) 12 + (alcotest (and :with-test (>= 0.8)))))
+4
tessera-viz/lib/dune
··· 1 + (library 2 + (name viz) 3 + (public_name tessera-viz) 4 + (libraries tessera-linalg))
+27
tessera-viz/lib/viz.ml
··· 1 + type rgba_image = { 2 + data : (int, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t; 3 + width : int; 4 + height : int; 5 + } 6 + 7 + let percentile arr p = 8 + let sorted = Array.copy arr in 9 + Array.sort Float.compare sorted; 10 + let n = Array.length sorted in 11 + let idx = p /. 100.0 *. Float.of_int (n - 1) in 12 + let lo = int_of_float (Float.floor idx) in 13 + let hi = int_of_float (Float.ceil idx) in 14 + let frac = idx -. Float.of_int lo in 15 + if lo = hi then sorted.(lo) 16 + else sorted.(lo) *. (1.0 -. frac) +. sorted.(hi) *. frac 17 + 18 + type color = { r : int; g : int; b : int } 19 + 20 + let pca_to_rgba _mat ~width:_ ~height:_ ?low_pct:_ ?high_pct:_ = 21 + failwith "TODO: pca_to_rgba" 22 + 23 + let color_of_hex _s = 24 + failwith "TODO: color_of_hex" 25 + 26 + let classification_to_rgba ~predictions:_ ~colors:_ ~width:_ ~height:_ ?alpha:_ = 27 + failwith "TODO: classification_to_rgba"
+51
tessera-viz/lib/viz.mli
··· 1 + (** Visualization utilities for GeoTessera embeddings. 2 + 3 + Converts PCA components and classification results to raw RGBA pixel 4 + arrays. Does no encoding — callers handle PNG/base64 conversion. *) 5 + 6 + type rgba_image = { 7 + data : (int, Bigarray.int8_unsigned_elt, Bigarray.c_layout) Bigarray.Array1.t; 8 + width : int; 9 + height : int; 10 + } 11 + (** An RGBA image. [data] has length [width * height * 4], with pixels in 12 + row-major order: R, G, B, A for each pixel. *) 13 + 14 + (** {1 Percentile utilities} *) 15 + 16 + val percentile : float array -> float -> float 17 + (** [percentile arr p] returns the [p]-th percentile (0-100) of [arr]. 18 + [arr] is not modified. *) 19 + 20 + (** {1 PCA visualization} *) 21 + 22 + val pca_to_rgba : Linalg.mat -> width:int -> height:int -> ?low_pct:float -> ?high_pct:float -> rgba_image 23 + (** Convert a 3-component PCA result to a false-color RGBA image. 24 + 25 + Input: mat with shape [(height*width, 3)] from [Linalg.pca_transform]. 26 + Each component is independently clipped to [\[low_pct, high_pct\]] percentiles 27 + (default: 2.0, 98.0) and scaled to [0-255]. 28 + Alpha is always 255. 29 + 30 + Output: RGBA image of size [width * height]. *) 31 + 32 + (** {1 Classification visualization} *) 33 + 34 + type color = { r : int; g : int; b : int } 35 + (** An RGB color. *) 36 + 37 + val color_of_hex : string -> color 38 + (** Parse a hex color string like ["#ff0000"] or ["ff0000"]. *) 39 + 40 + val classification_to_rgba : 41 + predictions:int array -> 42 + colors:(int * color) list -> 43 + width:int -> 44 + height:int -> 45 + ?alpha:int -> 46 + rgba_image 47 + (** Convert class predictions to a colored RGBA image. 48 + 49 + [predictions] has length [height * width]. 50 + [colors] maps class IDs to colors. Unknown classes are black. 51 + [alpha] defaults to 200. *)
+3
tessera-viz/test/dune
··· 1 + (test 2 + (name test_viz) 3 + (libraries tessera-viz tessera-linalg alcotest))
+26
tessera-viz/test/test_viz.ml
··· 1 + let check_float msg expected actual = 2 + Alcotest.(check (float 1e-6)) msg expected actual 3 + 4 + let test_percentile_min () = 5 + check_float "p0" 1.0 (Viz.percentile [|1.0; 2.0; 3.0; 4.0; 5.0|] 0.0) 6 + 7 + let test_percentile_max () = 8 + check_float "p100" 5.0 (Viz.percentile [|1.0; 2.0; 3.0; 4.0; 5.0|] 100.0) 9 + 10 + let test_percentile_median () = 11 + check_float "p50" 3.0 (Viz.percentile [|1.0; 2.0; 3.0; 4.0; 5.0|] 50.0) 12 + 13 + let test_percentile_unsorted () = 14 + check_float "p50 unsorted" 3.0 (Viz.percentile [|5.0; 1.0; 3.0; 2.0; 4.0|] 50.0) 15 + 16 + let percentile_tests = 17 + [ Alcotest.test_case "min (p0)" `Quick test_percentile_min 18 + ; Alcotest.test_case "max (p100)" `Quick test_percentile_max 19 + ; Alcotest.test_case "median (p50)" `Quick test_percentile_median 20 + ; Alcotest.test_case "unsorted input" `Quick test_percentile_unsorted 21 + ] 22 + 23 + let () = 24 + Alcotest.run "tessera-viz" 25 + [ ("percentile", percentile_tests) 26 + ]