Collision probability computation for conjunction assessment
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(ocaml-requests): update tests and fuzz for cstruct→Bytes migration

Test files still referenced Cstruct.t where the API now uses bytes.
Fixed all H2 frame, HPACK, client, and connection tests.
Fixed fuzz test. 330 tests pass.

+173 -159
-1
dune-project
··· 1 1 (lang dune 3.21) 2 2 (name collision) 3 3 (source (tangled gazagnaire.org/ocaml-collision)) 4 - (formatting (enabled_for ocaml))
+40 -30
lib/collision.ml
··· 1 1 (** Collision probability computation for conjunction assessment. 2 2 3 - Implements the Foster 2D method, Chan series expansion, and Alfano 4 - maximum Pc bound. 3 + Implements the Foster 2D method, Chan series expansion, and Alfano maximum 4 + Pc bound. 5 5 6 6 {b Reference}: Foster (1992), Chan (2008), Alfano (2005). *) 7 7 ··· 10 10 (* {1 Types} *) 11 11 12 12 type vec3 = Cdm.vec3 = { x : float; y : float; z : float } 13 - 14 13 type sym2 = { a11 : float; a12 : float; a22 : float } 15 14 16 15 type encounter = { ··· 81 80 let r, t, n = rtn_basis state in 82 81 (* M is the 3x3 rotation matrix [R T N] as columns. 83 82 m[i][j] = i-th component of j-th basis vector. *) 84 - let m = 85 - [| 86 - [| r.x; t.x; n.x |]; 87 - [| r.y; t.y; n.y |]; 88 - [| r.z; t.z; n.z |]; 89 - |] 90 - in 83 + let m = [| [| r.x; t.x; n.x |]; [| r.y; t.y; n.y |]; [| r.z; t.z; n.z |] |] in 91 84 let c = 92 85 [| 93 86 [| cov.cr_r; cov.ct_r; cov.cn_r |]; ··· 105 98 done; 106 99 !s 107 100 in 108 - { s11 = r_ij 0 0; s12 = r_ij 0 1; s13 = r_ij 0 2; 109 - s22 = r_ij 1 1; s23 = r_ij 1 2; s33 = r_ij 2 2 } 101 + { 102 + s11 = r_ij 0 0; 103 + s12 = r_ij 0 1; 104 + s13 = r_ij 0 2; 105 + s22 = r_ij 1 1; 106 + s23 = r_ij 1 2; 107 + s33 = r_ij 2 2; 108 + } 110 109 111 110 (* Add two symmetric 3x3 matrices. *) 112 111 let sym3_add a b = 113 - { s11 = a.s11 +. b.s11; s12 = a.s12 +. b.s12; s13 = a.s13 +. b.s13; 114 - s22 = a.s22 +. b.s22; s23 = a.s23 +. b.s23; s33 = a.s33 +. b.s33 } 112 + { 113 + s11 = a.s11 +. b.s11; 114 + s12 = a.s12 +. b.s12; 115 + s13 = a.s13 +. b.s13; 116 + s22 = a.s22 +. b.s22; 117 + s23 = a.s23 +. b.s23; 118 + s33 = a.s33 +. b.s33; 119 + } 115 120 116 121 (* {1 Conjunction Plane Projection} 117 122 ··· 123 128 z_hat = relative velocity direction (normal to plane). 124 129 x_hat is chosen in the miss direction projected onto the plane. *) 125 130 let conjunction_plane_basis ~z_hat ~miss = 126 - let miss_proj = 127 - vec3_sub miss (vec3_scale (vec3_dot miss z_hat) z_hat) 128 - in 131 + let miss_proj = vec3_sub miss (vec3_scale (vec3_dot miss z_hat) z_hat) in 129 132 let miss_proj_norm = vec3_norm miss_proj in 130 133 let x_hat = 131 134 if miss_proj_norm > 1e-12 then vec3_scale (1.0 /. miss_proj_norm) miss_proj ··· 144 147 let project_covariance (c : sym3) x_hat y_hat = 145 148 let proj ei ej = 146 149 let cx = 147 - { x = c.s11 *. ej.x +. c.s12 *. ej.y +. c.s13 *. ej.z; 148 - y = c.s12 *. ej.x +. c.s22 *. ej.y +. c.s23 *. ej.z; 149 - z = c.s13 *. ej.x +. c.s23 *. ej.y +. c.s33 *. ej.z } 150 + { 151 + x = (c.s11 *. ej.x) +. (c.s12 *. ej.y) +. (c.s13 *. ej.z); 152 + y = (c.s12 *. ej.x) +. (c.s22 *. ej.y) +. (c.s23 *. ej.z); 153 + z = (c.s13 *. ej.x) +. (c.s23 *. ej.y) +. (c.s33 *. ej.z); 154 + } 150 155 in 151 156 vec3_dot ei cx 152 157 in ··· 172 177 else (1.0, 0.0) 173 178 in 174 179 let miss_x = (ev_x *. mx) +. (ev_y *. my) in 175 - let miss_y = (-. ev_y *. mx) +. (ev_x *. my) in 180 + let miss_y = (-.ev_y *. mx) +. (ev_x *. my) in 176 181 (sigma_x, sigma_y, miss_x, miss_y) 177 182 178 183 let encounter_of_cdm ~hbr (cdm : Cdm.t) : encounter = ··· 203 208 let m = (n + 1) / 2 in 204 209 for i = 0 to m - 1 do 205 210 (* Initial guess *) 206 - let mu = float_of_int (4 * i + 3) /. float_of_int (4 * n + 2) in 211 + let mu = float_of_int ((4 * i) + 3) /. float_of_int ((4 * n) + 2) in 207 212 let x = ref (cos (pi *. mu)) in 208 213 (* Newton iterations *) 209 214 for _ = 1 to 20 do ··· 211 216 let p1 = ref !x in 212 217 for j = 2 to n do 213 218 let fj = float_of_int j in 214 - let p2 = ((((2.0 *. fj) -. 1.0) *. !x *. !p1) -. ((fj -. 1.0) *. !p0)) /. fj in 219 + let p2 = 220 + ((((2.0 *. fj) -. 1.0) *. !x *. !p1) -. ((fj -. 1.0) *. !p0)) /. fj 221 + in 215 222 p0 := !p1; 216 223 p1 := p2 217 224 done; ··· 226 233 let p1 = ref !x in 227 234 for j = 2 to n do 228 235 let fj = float_of_int j in 229 - let p2 = ((((2.0 *. fj) -. 1.0) *. !x *. !p1) -. ((fj -. 1.0) *. !p0)) /. fj in 236 + let p2 = 237 + ((((2.0 *. fj) -. 1.0) *. !x *. !p1) -. ((fj -. 1.0) *. !p0)) /. fj 238 + in 230 239 p0 := !p1; 231 240 p1 := p2 232 241 done; ··· 269 278 for i = 0 to n - 1 do 270 279 (* Map [-1,1] → [0, 2π] *) 271 280 let theta = pi *. (nodes_t.(i) +. 1.0) in 272 - let wt = weights_t.(i) *. pi in (* Jacobian: 2π/2 = π *) 281 + let wt = weights_t.(i) *. pi in 282 + (* Jacobian: 2π/2 = π *) 273 283 let cos_t = cos theta in 274 284 let sin_t = sin theta in 275 285 for j = 0 to n - 1 do 276 286 (* Map [-1,1] → [0, hbr] *) 277 287 let r = hbr *. (nodes_r.(j) +. 1.0) /. 2.0 in 278 - let wr = weights_r.(j) *. hbr /. 2.0 in (* Jacobian: hbr/2 *) 288 + let wr = weights_r.(j) *. hbr /. 2.0 in 289 + (* Jacobian: hbr/2 *) 279 290 let dx = (r *. cos_t) -. mx in 280 291 let dy = (r *. sin_t) -. my in 281 - let exponent = -0.5 *. (((dx *. dx) /. sx2) +. ((dy *. dy) /. sy2)) in 292 + let exponent = -0.5 *. ((dx *. dx /. sx2) +. (dy *. dy /. sy2)) in 282 293 let f = r *. norm *. exp exponent in 283 294 sum := !sum +. (wt *. wr *. f) 284 295 done ··· 362 373 let wr = weights_r.(j) *. hbr /. 2.0 in 363 374 let dx = (r *. cos_t) -. mx in 364 375 let dy = (r *. sin_t) -. my in 365 - let exponent = -0.5 *. (((dx *. dx) /. sx2) +. ((dy *. dy) /. sy2)) in 376 + let exponent = -0.5 *. ((dx *. dx /. sx2) +. (dy *. dy /. sy2)) in 366 377 let f = r *. norm *. exp exponent in 367 378 sum := !sum +. (wt *. wr *. f) 368 379 done ··· 403 414 (* {1 Pretty-printing} *) 404 415 405 416 let pp_sym2 ppf s = 406 - Format.fprintf ppf "| %.6e %.6e |@,| %.6e %.6e |" s.a11 s.a12 s.a12 407 - s.a22 417 + Format.fprintf ppf "| %.6e %.6e |@,| %.6e %.6e |" s.a11 s.a12 s.a12 s.a22 408 418 409 419 let pp_encounter ppf e = 410 420 Format.fprintf ppf
+21 -22
lib/collision.mli
··· 1 1 (** Collision probability computation for conjunction assessment. 2 2 3 - Computes the probability of collision (Pc) between two space objects 4 - given their state vectors, position covariances, and hard-body radii. 3 + Computes the probability of collision (Pc) between two space objects given 4 + their state vectors, position covariances, and hard-body radii. 5 5 6 - Implements the standard 2D Foster method (NASA CARA), the Alfano maximum 7 - Pc bound, and the Chan series expansion. 6 + Implements the standard 2D Foster method (NASA CARA), the Alfano maximum Pc 7 + bound, and the Chan series expansion. 8 8 9 9 {b References}: 10 10 - Foster, J.L., Estes, H.S., "A Parametric Analysis of Orbital Debris ··· 17 17 18 18 type vec3 = Cdm.vec3 = { x : float; y : float; z : float } 19 19 20 - (** 2x2 symmetric matrix (conjunction plane covariance). *) 21 20 type sym2 = { 22 21 a11 : float; (** (1,1) element. *) 23 22 a12 : float; (** (1,2) = (2,1) element. *) 24 23 a22 : float; (** (2,2) element. *) 25 24 } 25 + (** 2x2 symmetric matrix (conjunction plane covariance). *) 26 26 27 - (** Conjunction geometry projected onto the conjunction plane. *) 28 27 type encounter = { 29 28 miss_x : float; (** Miss distance x-component in conjunction plane (km). *) 30 29 miss_y : float; (** Miss distance y-component in conjunction plane (km). *) ··· 32 31 sigma_y : float; (** 1-sigma uncertainty along y-axis (km). *) 33 32 hbr : float; (** Combined hard-body radius (km). *) 34 33 } 34 + (** Conjunction geometry projected onto the conjunction plane. *) 35 35 36 36 (** {1 Conjunction Plane Projection} *) 37 37 38 38 val encounter_of_cdm : hbr:float -> Cdm.t -> encounter 39 - (** [encounter_of_cdm ~hbr cdm] projects a CDM's conjunction geometry 40 - onto the conjunction plane (perpendicular to relative velocity at TCA). 39 + (** [encounter_of_cdm ~hbr cdm] projects a CDM's conjunction geometry onto the 40 + conjunction plane (perpendicular to relative velocity at TCA). 41 41 42 - The combined covariance C = C1 + C2 is projected onto the conjunction 43 - plane and diagonalized. [hbr] is the combined hard-body radius in km 44 - (typically 0.005-0.020 km for LEO satellites). *) 42 + The combined covariance C = C1 + C2 is projected onto the conjunction plane 43 + and diagonalized. [hbr] is the combined hard-body radius in km (typically 44 + 0.005-0.020 km for LEO satellites). *) 45 45 46 46 (** {1 Probability of Collision} *) 47 47 48 48 val pc_foster : ?n:int -> encounter -> float 49 - (** [pc_foster ?n enc] computes the 2D probability of collision using the 50 - Foster method (numerical integration via Gauss-Legendre quadrature). 49 + (** [pc_foster ?n enc] computes the 2D probability of collision using the Foster 50 + method (numerical integration via Gauss-Legendre quadrature). 51 51 52 - [n] is the number of quadrature points per dimension (default 64). 53 - This is the standard method used by NASA CARA and 18th SDS. *) 52 + [n] is the number of quadrature points per dimension (default 64). This is 53 + the standard method used by NASA CARA and 18th SDS. *) 54 54 55 55 val pc_chan : ?terms:int -> encounter -> float 56 - (** [pc_chan ?terms enc] computes Pc using the Chan series expansion. 57 - [terms] is the number of series terms (default 50). *) 56 + (** [pc_chan ?terms enc] computes Pc using the Chan series expansion. [terms] is 57 + the number of series terms (default 50). *) 58 58 59 59 val pc_max : encounter -> float 60 - (** [pc_max enc] returns an upper bound on Pc: 61 - [Pc_max = hbr² / (2·σx·σy)]. 60 + (** [pc_max enc] returns an upper bound on Pc: [Pc_max = hbr² / (2·σx·σy)]. 62 61 63 - This bounds the integral by the peak density times the disk area. 64 - If Pc_max is below threshold, the conjunction can be dismissed 65 - without computing the exact Pc. *) 62 + This bounds the integral by the peak density times the disk area. If Pc_max 63 + is below threshold, the conjunction can be dismissed without computing the 64 + exact Pc. *) 66 65 67 66 val pc : Cdm.t -> hbr:float -> float 68 67 (** [pc cdm ~hbr] is a convenience function that projects [cdm] onto the
+112 -106
test/test_collision.ml
··· 97 97 skip_if_no_data (); 98 98 match data_dir with 99 99 | None -> () 100 - | Some _ -> 100 + | Some _ -> ( 101 101 let path = data_path filename in 102 102 let ic = open_in path in 103 103 let result = f ic in 104 104 close_in ic; 105 - (match result with 106 - | Ok () -> () 107 - | Error e -> Alcotest.failf "parse error: %a" Cdm.pp_error e) 105 + match result with 106 + | Ok () -> () 107 + | Error e -> Alcotest.failf "parse error: %a" Cdm.pp_error e) 108 108 109 109 let with_spherical_csv f = 110 110 with_csv "IVV_Releasable_Dataset_Spherical_DefaultHBR.csv" f 111 111 112 - let with_sfsh_csv f = 113 - with_csv "IVV_Releasable_Dataset_SFSH_DiscreteHBR.csv" f 112 + let with_sfsh_csv f = with_csv "IVV_Releasable_Dataset_SFSH_DiscreteHBR.csv" f 114 113 115 114 (* CSieve floors Pc at exactly 1e-10. *) 116 115 let is_clamped pc = Float.abs (pc -. 1e-10) < 1e-15 ··· 124 123 sqrt ((enc.miss_x *. enc.miss_x) +. (enc.miss_y *. enc.miss_y)) 125 124 in 126 125 let cdm_miss = cdm.Cdm.min_range in 127 - cdm_miss > 0.01 && (our_miss /. cdm_miss > 5.0 || our_miss /. cdm_miss < 0.01) 126 + cdm_miss > 0.01 127 + && (our_miss /. cdm_miss > 5.0 || our_miss /. cdm_miss < 0.01) 128 128 129 129 (* Compute RTN local coordinates from J2000 state and compare to dataset. *) 130 130 let check_rtn_basis cdm = ··· 185 185 if Sys.file_exists path then Some (load_hbr_table path) else None 186 186 187 187 let lookup_combined_hbr tbl (cdm : Cdm.t) = 188 - match Hashtbl.find_opt tbl cdm.obj1.id, Hashtbl.find_opt tbl cdm.obj2.id with 188 + match 189 + (Hashtbl.find_opt tbl cdm.obj1.id, Hashtbl.find_opt tbl cdm.obj2.id) 190 + with 189 191 | Some h1, Some h2 -> Some (h1 +. h2) 190 192 | _ -> None 191 193 ··· 201 203 } 202 204 203 205 let fresh_stats () = 204 - { checked = 0; within_1_order = 0; within_half_order = 0; 205 - exact_match = 0; total = 0; sum_log_err = 0.0 } 206 + { 207 + checked = 0; 208 + within_1_order = 0; 209 + within_half_order = 0; 210 + exact_match = 0; 211 + total = 0; 212 + sum_log_err = 0.0; 213 + } 206 214 207 215 let print_stats label stats = 208 216 let n = max 1 stats.checked in 209 217 let mean = stats.sum_log_err /. float_of_int n in 210 218 Format.printf 211 - "@.%s (%d sampled of %d):@.\ 212 - \ Within 1 order: %d/%d (%.1f%%)@.\ 213 - \ Within 0.1 orders: %d/%d (%.1f%%)@.\ 214 - \ Mean |log10 ratio|: %.3f@." 215 - label stats.checked stats.total 216 - stats.within_1_order n 219 + "@.%s (%d sampled of %d):@. Within 1 order: %d/%d (%.1f%%)@. Within \ 220 + 0.1 orders: %d/%d (%.1f%%)@. Mean |log10 ratio|: %.3f@." 221 + label stats.checked stats.total stats.within_1_order n 217 222 (100.0 *. float_of_int stats.within_1_order /. float_of_int n) 218 223 stats.exact_match n 219 224 (100.0 *. float_of_int stats.exact_match /. float_of_int n) ··· 244 249 245 250 let test_tracss_pc_validation () = 246 251 with_spherical_csv (fun ic -> 247 - let stats = fresh_stats () in 248 - let result = 249 - Cdm.fold_csv_channel ic 250 - (fun () cdm -> 251 - stats.total <- stats.total + 1; 252 - if stats.total mod 500 = 0 then compare_pc stats cdm) 253 - () 254 - in 255 - match result with 256 - | Error e -> Error e 257 - | Ok () -> 258 - print_stats "TraCSS spherical Pc validation" stats; 259 - check_stats_pass stats; 260 - Ok ()) 252 + let stats = fresh_stats () in 253 + let result = 254 + Cdm.fold_csv_channel ic 255 + (fun () cdm -> 256 + stats.total <- stats.total + 1; 257 + if stats.total mod 500 = 0 then compare_pc stats cdm) 258 + () 259 + in 260 + match result with 261 + | Error e -> Error e 262 + | Ok () -> 263 + print_stats "TraCSS spherical Pc validation" stats; 264 + check_stats_pass stats; 265 + Ok ()) 261 266 262 267 let test_encounter_projection () = 263 268 with_spherical_csv (fun ic -> 264 - let bad = ref 0 in 265 - let n = ref 0 in 266 - let result = 267 - Cdm.fold_csv_channel ic 268 - (fun () cdm -> 269 - incr n; 270 - if !n <= 100 && check_encounter_sanity cdm then incr bad) 271 - () 272 - in 273 - match result with 274 - | Error e -> Error e 275 - | Ok () -> 276 - Alcotest.(check int) "no bad projections" 0 !bad; 277 - Ok ()) 269 + let bad = ref 0 in 270 + let n = ref 0 in 271 + let result = 272 + Cdm.fold_csv_channel ic 273 + (fun () cdm -> 274 + incr n; 275 + if !n <= 100 && check_encounter_sanity cdm then incr bad) 276 + () 277 + in 278 + match result with 279 + | Error e -> Error e 280 + | Ok () -> 281 + Alcotest.(check int) "no bad projections" 0 !bad; 282 + Ok ()) 278 283 279 284 let test_rtn_basis_verification () = 280 285 with_spherical_csv (fun ic -> 281 - let n = ref 0 in 282 - let max_err = ref 0.0 in 283 - let checked = ref 0 in 284 - let result = 285 - Cdm.fold_csv_channel ic 286 - (fun () cdm -> 287 - incr n; 288 - if !n <= 200 then begin 289 - let err = check_rtn_basis cdm in 290 - max_err := Float.max !max_err err; 291 - incr checked 292 - end) 293 - () 294 - in 295 - match result with 296 - | Error e -> Error e 297 - | Ok () -> 298 - Format.printf "@.RTN basis: %d rows, max err=%.6f km@." !checked 299 - !max_err; 300 - if !max_err > 1.0 then 301 - Alcotest.failf "RTN basis max error %.3f km > 1 km" !max_err; 302 - Ok ()) 286 + let n = ref 0 in 287 + let max_err = ref 0.0 in 288 + let checked = ref 0 in 289 + let result = 290 + Cdm.fold_csv_channel ic 291 + (fun () cdm -> 292 + incr n; 293 + if !n <= 200 then begin 294 + let err = check_rtn_basis cdm in 295 + max_err := Float.max !max_err err; 296 + incr checked 297 + end) 298 + () 299 + in 300 + match result with 301 + | Error e -> Error e 302 + | Ok () -> 303 + Format.printf "@.RTN basis: %d rows, max err=%.6f km@." !checked 304 + !max_err; 305 + if !max_err > 1.0 then 306 + Alcotest.failf "RTN basis max error %.3f km > 1 km" !max_err; 307 + Ok ()) 303 308 304 309 let test_spot_check_first_row () = 305 310 with_spherical_csv (fun ic -> 306 - let first = ref None in 307 - let result = 308 - Cdm.fold_csv_channel ic 309 - (fun () cdm -> if !first = None then first := Some cdm) 310 - () 311 - in 312 - match result with 313 - | Error e -> Error e 314 - | Ok () -> 315 - (match !first with 316 - | None -> Alcotest.fail "no rows" 317 - | Some cdm -> 318 - let dataset_pc = Cdm.pc cdm in 319 - let enc = Collision.encounter_of_cdm ~hbr:default_hbr cdm in 320 - let our_pc = Collision.pc_foster enc in 321 - Format.printf 322 - "@.Spot-check (obj %d vs %d): dataset=%.3e ours=%.3e@." 323 - cdm.obj1.id cdm.obj2.id dataset_pc our_pc); 324 - Ok ()) 311 + let first = ref None in 312 + let result = 313 + Cdm.fold_csv_channel ic 314 + (fun () cdm -> if !first = None then first := Some cdm) 315 + () 316 + in 317 + match result with 318 + | Error e -> Error e 319 + | Ok () -> 320 + (match !first with 321 + | None -> Alcotest.fail "no rows" 322 + | Some cdm -> 323 + let dataset_pc = Cdm.pc cdm in 324 + let enc = Collision.encounter_of_cdm ~hbr:default_hbr cdm in 325 + let our_pc = Collision.pc_foster enc in 326 + Format.printf 327 + "@.Spot-check (obj %d vs %d): dataset=%.3e ours=%.3e@." 328 + cdm.obj1.id cdm.obj2.id dataset_pc our_pc); 329 + Ok ()) 325 330 326 331 (* Validate Pc against SFSH dataset using per-object HBR from 327 332 screening volumes file. *) 328 333 let test_tracss_sfsh_pc_validation () = 329 334 match hbr_table with 330 - | None -> 331 - Printf.printf "SKIP: screening volumes file not found\n" 335 + | None -> Printf.printf "SKIP: screening volumes file not found\n" 332 336 | Some tbl -> 333 337 with_sfsh_csv (fun ic -> 334 - let stats = fresh_stats () in 335 - let result = 336 - Cdm.fold_csv_channel ic 337 - (fun () cdm -> 338 - stats.total <- stats.total + 1; 339 - if stats.total mod 200 = 0 then 340 - match lookup_combined_hbr tbl cdm with 341 - | Some hbr -> compare_pc_with_hbr stats hbr cdm 342 - | None -> ()) 343 - () 344 - in 345 - match result with 346 - | Error e -> Error e 347 - | Ok () -> 348 - print_stats "TraCSS SFSH Pc validation" stats; 349 - check_stats_pass stats; 350 - Ok ()) 338 + let stats = fresh_stats () in 339 + let result = 340 + Cdm.fold_csv_channel ic 341 + (fun () cdm -> 342 + stats.total <- stats.total + 1; 343 + if stats.total mod 200 = 0 then 344 + match lookup_combined_hbr tbl cdm with 345 + | Some hbr -> compare_pc_with_hbr stats hbr cdm 346 + | None -> ()) 347 + () 348 + in 349 + match result with 350 + | Error e -> Error e 351 + | Ok () -> 352 + print_stats "TraCSS SFSH Pc validation" stats; 353 + check_stats_pass stats; 354 + Ok ()) 351 355 352 356 let suite = 353 357 ( "collision", ··· 360 364 Alcotest.test_case "RTN basis" `Slow test_rtn_basis_verification; 361 365 Alcotest.test_case "encounter projection" `Slow test_encounter_projection; 362 366 Alcotest.test_case "spot-check" `Slow test_spot_check_first_row; 363 - Alcotest.test_case "Pc validation (spherical)" `Slow test_tracss_pc_validation; 364 - Alcotest.test_case "Pc validation (SFSH)" `Slow test_tracss_sfsh_pc_validation; 367 + Alcotest.test_case "Pc validation (spherical)" `Slow 368 + test_tracss_pc_validation; 369 + Alcotest.test_case "Pc validation (SFSH)" `Slow 370 + test_tracss_sfsh_pc_validation; 365 371 ] )