use super::GeometryRouter;
use crate::BoolFailure;
use rustc_hash::FxHashMap;
pub(crate) use super::diagnostics_recording::ClassificationKind;
pub use super::diagnostics_recording::{
ClassificationStats, HostOpeningDiagnostic, OpeningDiagnostic, OpeningKindDiag,
};
impl GeometryRouter {
pub(crate) fn record_host_failure_summary(&self, host_id: u32, failures: &[BoolFailure]) {
if failures.is_empty() {
return;
}
let mut log = self.host_opening_diagnostics.borrow_mut();
let entry = log.entry(host_id).or_default();
entry.csg_failure_count += failures.len();
if entry.first_failure_label.is_none() {
let label = failures[0].reason.label();
entry.first_failure_label = Some(label.to_string());
}
}
}
#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ClassificationSummary {
pub rectangular: u64,
pub diagonal: u64,
pub non_rectangular: u64,
pub total: u64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ReasonCount {
pub reason: String,
pub count: u64,
}
#[derive(Debug, Clone, Copy, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RectFastSummary {
pub fired: u64,
pub openings_cut: u64,
pub defer_host_not_box: u64,
pub defer_not_through: u64,
pub defer_off_face: u64,
pub defer_near_edge: u64,
pub defer_no_openings: u64,
pub defer_too_many_openings: u64,
}
#[derive(Debug, Clone, Copy, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct HostBbox {
pub min: [f32; 3],
pub max: [f32; 3],
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct WorstHost {
pub product_id: u32,
pub ifc_type: String,
pub openings: u64,
pub csg_failures: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub first_failure_label: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub bbox: Option<HostBbox>,
#[serde(skip_serializing_if = "Option::is_none")]
pub triangle_count: Option<u64>,
}
pub const GEOMETRY_DIAGNOSTICS_SCHEMA_VERSION: u32 = 3;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GeometryDiagnostics {
#[serde(default)]
pub schema_version: u32,
pub total_csg_failures: u64,
pub products_with_failures: u64,
pub hosts_with_openings: u64,
pub classification: ClassificationSummary,
pub failures_by_reason: Vec<ReasonCount>,
pub silent_no_ops: u64,
pub rect_fast: RectFastSummary,
pub worst_hosts: Vec<WorstHost>,
#[serde(default)]
pub oversized_ref_drops: u64,
#[serde(default)]
pub total_unsupported_items: u64,
#[serde(default)]
pub unsupported_items_by_type: Vec<ReasonCount>,
}
impl Default for GeometryDiagnostics {
fn default() -> Self {
Self {
schema_version: GEOMETRY_DIAGNOSTICS_SCHEMA_VERSION,
total_csg_failures: 0,
products_with_failures: 0,
hosts_with_openings: 0,
classification: ClassificationSummary::default(),
failures_by_reason: Vec::new(),
silent_no_ops: 0,
rect_fast: RectFastSummary::default(),
worst_hosts: Vec::new(),
oversized_ref_drops: 0,
total_unsupported_items: 0,
unsupported_items_by_type: Vec::new(),
}
}
}
impl GeometryDiagnostics {
pub fn has_issues(&self) -> bool {
self.total_csg_failures > 0 || self.silent_no_ops > 0 || self.total_unsupported_items > 0
}
pub fn is_empty(&self) -> bool {
self.total_csg_failures == 0
&& self.hosts_with_openings == 0
&& self.classification.total == 0
&& self.silent_no_ops == 0
&& self.rect_fast.fired == 0
&& self.oversized_ref_drops == 0
&& self.total_unsupported_items == 0
}
}
pub fn aggregate_diagnostics(
classification: ClassificationStats,
csg_failures: &FxHashMap<u32, Vec<BoolFailure>>,
host_diags: &FxHashMap<u32, HostOpeningDiagnostic>,
rect_fast: crate::rect_fast::RectFastStats,
worst_hosts_limit: usize,
oversized_ref_drops: u64,
unsupported_items: &FxHashMap<String, u64>,
) -> GeometryDiagnostics {
let total_csg_failures = csg_failures.values().map(Vec::len).sum::<usize>() as u64;
let products_with_failures = count_attributed_products(csg_failures);
let hosts_with_openings = host_diags.len() as u64;
let classification = ClassificationSummary {
rectangular: classification.rectangular as u64,
diagonal: classification.diagonal as u64,
non_rectangular: classification.non_rectangular as u64,
total: (classification.rectangular
+ classification.diagonal
+ classification.non_rectangular) as u64,
};
let mut by_reason: FxHashMap<&'static str, u64> = FxHashMap::default();
for fails in csg_failures.values() {
for f in fails {
*by_reason.entry(f.reason.label()).or_insert(0) += 1;
}
}
let mut failures_by_reason: Vec<ReasonCount> = by_reason
.into_iter()
.map(|(reason, count)| ReasonCount { reason: reason.to_string(), count })
.collect();
failures_by_reason
.sort_by(|a, b| b.count.cmp(&a.count).then_with(|| a.reason.cmp(&b.reason)));
let (total_unsupported_items, unsupported_items_by_type) =
unsupported_items::summarize(unsupported_items);
let silent_no_ops = host_diags
.values()
.filter(|hd| {
matches!((hd.tris_before, hd.tris_after), (Some(b), Some(a)) if b == a)
&& hd.rect_boxes_processed > 0
&& hd.csg_failure_count == 0
})
.count() as u64;
let mut worst: Vec<(&u32, &HostOpeningDiagnostic)> = host_diags
.iter()
.filter(|(_, hd)| hd.csg_failure_count > 0)
.collect();
worst.sort_by(|a, b| {
b.1.csg_failure_count.cmp(&a.1.csg_failure_count).then_with(|| a.0.cmp(b.0))
});
let worst_hosts: Vec<WorstHost> = worst
.into_iter()
.take(worst_hosts_limit)
.map(|(pid, hd)| WorstHost {
product_id: *pid,
ifc_type: hd.host_type.clone(),
openings: hd.openings.len() as u64,
csg_failures: hd.csg_failure_count as u64,
first_failure_label: hd.first_failure_label.clone(),
bbox: hd.host_bounds.map(|(min, max)| HostBbox {
min: [min.0, min.1, min.2],
max: [max.0, max.1, max.2],
}),
triangle_count: hd.tris_after.or(hd.tris_before).map(|t| t as u64),
})
.collect();
GeometryDiagnostics {
schema_version: GEOMETRY_DIAGNOSTICS_SCHEMA_VERSION,
total_csg_failures,
products_with_failures,
hosts_with_openings,
classification,
failures_by_reason,
silent_no_ops,
rect_fast: RectFastSummary {
fired: rect_fast.fired,
openings_cut: rect_fast.openings_cut,
defer_host_not_box: rect_fast.defer_host_not_box,
defer_not_through: rect_fast.defer_not_through,
defer_off_face: rect_fast.defer_off_face,
defer_near_edge: rect_fast.defer_near_edge,
defer_no_openings: rect_fast.defer_no_openings,
defer_too_many_openings: rect_fast.defer_too_many_openings,
},
worst_hosts,
oversized_ref_drops,
total_unsupported_items,
unsupported_items_by_type,
}
}
mod attribution;
mod processor_failures;
mod unsupported_items;
pub use unsupported_items::format_unsupported_breakdown;
pub(crate) use unsupported_items::UnsupportedItemState;
#[cfg(test)]
mod unsupported_items_tests;
pub use attribution::{count_attributed_products, UNATTRIBUTED_PRODUCT_ID};
#[cfg(test)]
mod diagnostics_contract_tests;