1use crate::ViolationComputable;
10use std::collections::HashMap;
11use std::fmt;
12
13#[derive(Debug, Clone)]
15pub struct ViolationStats {
16 pub num_samples: usize,
18 pub num_violations: usize,
20 pub mean_violation: f32,
22 pub max_violation: f32,
24 pub min_violation: f32,
26 pub std_violation: f32,
28}
29
30impl ViolationStats {
31 pub fn from_values<C: ViolationComputable>(constraint: &C, values: &[f32]) -> Self {
33 let num_samples = values.len();
34 let mut num_violations = 0;
35 let mut violation_sum = 0.0f32;
36 let mut max_violation = 0.0f32;
37 let mut min_violation = f32::MAX;
38 let mut violations = Vec::new();
39
40 for &val in values {
41 let viol = constraint.violation(&[val]);
42 if viol > 1e-6 {
43 num_violations += 1;
44 violation_sum += viol;
45 max_violation = max_violation.max(viol);
46 min_violation = min_violation.min(viol);
47 violations.push(viol);
48 }
49 }
50
51 let mean_violation = if num_violations > 0 {
52 violation_sum / num_violations as f32
53 } else {
54 0.0
55 };
56
57 let std_violation = if num_violations > 0 {
58 let variance: f32 = violations
59 .iter()
60 .map(|&v| (v - mean_violation).powi(2))
61 .sum::<f32>()
62 / num_violations as f32;
63 variance.sqrt()
64 } else {
65 0.0
66 };
67
68 if min_violation == f32::MAX {
69 min_violation = 0.0;
70 }
71
72 Self {
73 num_samples,
74 num_violations,
75 mean_violation,
76 max_violation,
77 min_violation,
78 std_violation,
79 }
80 }
81
82 pub fn violation_rate(&self) -> f32 {
84 if self.num_samples == 0 {
85 0.0
86 } else {
87 self.num_violations as f32 / self.num_samples as f32
88 }
89 }
90
91 pub fn satisfaction_rate(&self) -> f32 {
93 1.0 - self.violation_rate()
94 }
95}
96
97impl fmt::Display for ViolationStats {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 writeln!(f, "Violation Statistics:")?;
100 writeln!(f, " Samples: {}", self.num_samples)?;
101 writeln!(
102 f,
103 " Violations: {} ({:.1}%)",
104 self.num_violations,
105 self.violation_rate() * 100.0
106 )?;
107 writeln!(
108 f,
109 " Satisfactions: {} ({:.1}%)",
110 self.num_samples - self.num_violations,
111 self.satisfaction_rate() * 100.0
112 )?;
113 writeln!(f, " Mean violation: {:.4}", self.mean_violation)?;
114 writeln!(f, " Max violation: {:.4}", self.max_violation)?;
115 writeln!(f, " Min violation: {:.4}", self.min_violation)?;
116 writeln!(f, " Std violation: {:.4}", self.std_violation)?;
117 Ok(())
118 }
119}
120
121pub struct ConstraintTimeSeries {
123 name: String,
124 values: Vec<f32>,
125 violations: Vec<f32>,
126 timestamps: Vec<usize>,
127}
128
129impl ConstraintTimeSeries {
130 pub fn new(name: impl Into<String>) -> Self {
132 Self {
133 name: name.into(),
134 values: Vec::new(),
135 violations: Vec::new(),
136 timestamps: Vec::new(),
137 }
138 }
139
140 pub fn add_sample<C: ViolationComputable>(
142 &mut self,
143 constraint: &C,
144 value: f32,
145 timestamp: usize,
146 ) {
147 let violation = constraint.violation(&[value]);
148 self.values.push(value);
149 self.violations.push(violation);
150 self.timestamps.push(timestamp);
151 }
152
153 pub fn stats(&self) -> ViolationStats {
155 let num_samples = self.values.len();
156 let mut num_violations = 0;
157 let mut violation_sum = 0.0f32;
158 let mut max_violation = 0.0f32;
159 let mut min_violation = f32::MAX;
160 let mut active_violations = Vec::new();
161
162 for &viol in &self.violations {
163 if viol > 1e-6 {
164 num_violations += 1;
165 violation_sum += viol;
166 max_violation = max_violation.max(viol);
167 min_violation = min_violation.min(viol);
168 active_violations.push(viol);
169 }
170 }
171
172 let mean_violation = if num_violations > 0 {
173 violation_sum / num_violations as f32
174 } else {
175 0.0
176 };
177
178 let std_violation = if num_violations > 0 {
179 let variance: f32 = active_violations
180 .iter()
181 .map(|&v| (v - mean_violation).powi(2))
182 .sum::<f32>()
183 / num_violations as f32;
184 variance.sqrt()
185 } else {
186 0.0
187 };
188
189 if min_violation == f32::MAX {
190 min_violation = 0.0;
191 }
192
193 ViolationStats {
194 num_samples,
195 num_violations,
196 mean_violation,
197 max_violation,
198 min_violation,
199 std_violation,
200 }
201 }
202
203 pub fn name(&self) -> &str {
205 &self.name
206 }
207
208 pub fn violations(&self) -> &[f32] {
210 &self.violations
211 }
212
213 pub fn values(&self) -> &[f32] {
215 &self.values
216 }
217
218 pub fn timestamps(&self) -> &[usize] {
220 &self.timestamps
221 }
222
223 pub fn violation_periods(&self) -> Vec<(usize, usize, f32)> {
225 let mut periods = Vec::new();
226 let mut in_violation = false;
227 let mut start = 0;
228 let mut max_viol_in_period = 0.0f32;
229
230 for (i, &viol) in self.violations.iter().enumerate() {
231 if viol > 1e-6 {
232 if !in_violation {
233 in_violation = true;
234 start = i;
235 max_viol_in_period = viol;
236 } else {
237 max_viol_in_period = max_viol_in_period.max(viol);
238 }
239 } else if in_violation {
240 periods.push((start, i - 1, max_viol_in_period));
241 in_violation = false;
242 max_viol_in_period = 0.0;
243 }
244 }
245
246 if in_violation {
248 periods.push((start, self.violations.len() - 1, max_viol_in_period));
249 }
250
251 periods
252 }
253}
254
255pub struct ConstraintReport {
257 constraint_names: Vec<String>,
258 stats: HashMap<String, ViolationStats>,
259}
260
261impl ConstraintReport {
262 pub fn new() -> Self {
264 Self {
265 constraint_names: Vec::new(),
266 stats: HashMap::new(),
267 }
268 }
269
270 pub fn add_constraint(&mut self, name: impl Into<String>, stats: ViolationStats) {
272 let name = name.into();
273 self.constraint_names.push(name.clone());
274 self.stats.insert(name, stats);
275 }
276
277 pub fn get_stats(&self, name: &str) -> Option<&ViolationStats> {
279 self.stats.get(name)
280 }
281
282 pub fn constraint_names(&self) -> &[String] {
284 &self.constraint_names
285 }
286
287 pub fn summary(&self) -> String {
289 let mut report = String::from("Constraint Report\n");
290 report.push_str("=================\n\n");
291
292 for name in &self.constraint_names {
293 if let Some(stats) = self.stats.get(name) {
294 report.push_str(&format!("Constraint: {}\n", name));
295 report.push_str(&format!(
296 " Violation rate: {:.1}%\n",
297 stats.violation_rate() * 100.0
298 ));
299 report.push_str(&format!(" Mean violation: {:.4}\n", stats.mean_violation));
300 report.push_str(&format!(" Max violation: {:.4}\n", stats.max_violation));
301 report.push('\n');
302 }
303 }
304
305 report
306 }
307
308 pub fn most_violated(&self) -> Option<(&str, &ViolationStats)> {
310 self.stats
311 .iter()
312 .max_by(|a, b| {
313 a.1.violation_rate()
314 .partial_cmp(&b.1.violation_rate())
315 .unwrap_or(std::cmp::Ordering::Equal)
316 })
317 .map(|(name, stats)| (name.as_str(), stats))
318 }
319
320 pub fn largest_violations(&self) -> Option<(&str, &ViolationStats)> {
322 self.stats
323 .iter()
324 .max_by(|a, b| {
325 a.1.max_violation
326 .partial_cmp(&b.1.max_violation)
327 .unwrap_or(std::cmp::Ordering::Equal)
328 })
329 .map(|(name, stats)| (name.as_str(), stats))
330 }
331}
332
333impl Default for ConstraintReport {
334 fn default() -> Self {
335 Self::new()
336 }
337}
338
339impl fmt::Display for ConstraintReport {
340 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341 write!(f, "{}", self.summary())
342 }
343}
344
345pub struct ConstraintInspector {
347 samples: Vec<f32>,
348}
349
350impl ConstraintInspector {
351 pub fn new() -> Self {
353 Self {
354 samples: Vec::new(),
355 }
356 }
357
358 pub fn add_sample(&mut self, value: f32) {
360 self.samples.push(value);
361 }
362
363 pub fn clear(&mut self) {
365 self.samples.clear();
366 }
367
368 pub fn inspect<C: ViolationComputable + ?Sized>(
370 &self,
371 constraint: &C,
372 name: &str,
373 ) -> InspectionResult {
374 let mut satisfied = Vec::new();
375 let mut violated = Vec::new();
376
377 for (i, &val) in self.samples.iter().enumerate() {
378 let viol = constraint.violation(&[val]);
379 if viol <= 1e-6 {
380 satisfied.push((i, val));
381 } else {
382 violated.push((i, val, viol));
383 }
384 }
385
386 InspectionResult {
387 constraint_name: name.to_string(),
388 total_samples: self.samples.len(),
389 satisfied,
390 violated,
391 }
392 }
393
394 pub fn sample_count(&self) -> usize {
396 self.samples.len()
397 }
398
399 pub fn samples(&self) -> &[f32] {
401 &self.samples
402 }
403}
404
405impl Default for ConstraintInspector {
406 fn default() -> Self {
407 Self::new()
408 }
409}
410
411pub struct InspectionResult {
413 constraint_name: String,
414 total_samples: usize,
415 satisfied: Vec<(usize, f32)>,
416 violated: Vec<(usize, f32, f32)>,
417}
418
419impl InspectionResult {
420 pub fn constraint_name(&self) -> &str {
422 &self.constraint_name
423 }
424
425 pub fn total_samples(&self) -> usize {
427 self.total_samples
428 }
429
430 pub fn satisfied(&self) -> &[(usize, f32)] {
432 &self.satisfied
433 }
434
435 pub fn violated(&self) -> &[(usize, f32, f32)] {
437 &self.violated
438 }
439
440 pub fn violation_rate(&self) -> f32 {
442 if self.total_samples == 0 {
443 0.0
444 } else {
445 self.violated.len() as f32 / self.total_samples as f32
446 }
447 }
448
449 pub fn print_summary(&self) {
451 println!("Inspection: {}", self.constraint_name);
452 println!(" Total samples: {}", self.total_samples);
453 println!(
454 " Satisfied: {} ({:.1}%)",
455 self.satisfied.len(),
456 (self.satisfied.len() as f32 / self.total_samples as f32) * 100.0
457 );
458 println!(
459 " Violated: {} ({:.1}%)",
460 self.violated.len(),
461 self.violation_rate() * 100.0
462 );
463 }
464
465 pub fn print_violations(&self, max_items: usize) {
467 println!("\nViolations for {}:", self.constraint_name);
468 let n = self.violated.len().min(max_items);
469 for (i, &(idx, val, viol)) in self.violated.iter().take(n).enumerate() {
470 println!(
471 " [{}] Sample {}: value={:.4}, violation={:.4}",
472 i + 1,
473 idx,
474 val,
475 viol
476 );
477 }
478 if self.violated.len() > max_items {
479 println!(
480 " ... and {} more violations",
481 self.violated.len() - max_items
482 );
483 }
484 }
485}
486
487#[derive(Debug, Clone, Copy, Default)]
493pub enum Colormap {
494 #[default]
495 Viridis,
497 RdGn,
499 Grayscale,
501}
502
503impl Colormap {
504 pub fn map(&self, t: f64) -> (u8, u8, u8) {
506 let t = t.clamp(0.0, 1.0);
507 match self {
508 Colormap::Viridis => {
509 let r = (255.0 * (t * t * t * 0.5 + t * 0.5)).min(255.0) as u8;
510 let g = (255.0 * (0.5 * (1.0 - (2.0 * t - 1.0).abs()))).min(255.0) as u8;
511 let b = (255.0 * ((1.0 - t) * 0.9)).min(255.0) as u8;
512 (r, g, b)
513 }
514 Colormap::RdGn => {
515 let r = (255.0 * (1.0 - t)).min(255.0) as u8;
516 let g = (255.0 * t).min(255.0) as u8;
517 (r, g, 0)
518 }
519 Colormap::Grayscale => {
520 let v = (255.0 * t) as u8;
521 (v, v, v)
522 }
523 }
524 }
525}
526
527#[derive(Debug, Clone)]
529pub struct PlotConfig {
530 pub width: usize,
532 pub height: usize,
534 pub x_range: (f64, f64),
536 pub y_range: (f64, f64),
538 pub title: String,
540 pub grid: bool,
542 pub colormap: Colormap,
544}
545
546impl Default for PlotConfig {
547 fn default() -> Self {
548 Self {
549 width: 600,
550 height: 600,
551 x_range: (-3.0, 3.0),
552 y_range: (-3.0, 3.0),
553 title: String::new(),
554 grid: true,
555 colormap: Colormap::Viridis,
556 }
557 }
558}
559
560pub struct SvgBuilder {
566 width: usize,
567 height: usize,
568 elements: Vec<String>,
569}
570
571impl SvgBuilder {
572 pub fn new(width: usize, height: usize) -> Self {
574 Self {
575 width,
576 height,
577 elements: Vec::new(),
578 }
579 }
580
581 pub fn rect(&mut self, x: f64, y: f64, w: f64, h: f64, fill: &str, opacity: f64) -> &mut Self {
583 self.elements.push(format!(
584 r#"<rect x="{:.3}" y="{:.3}" width="{:.3}" height="{:.3}" fill="{}" opacity="{:.3}"/>"#,
585 x, y, w, h, fill, opacity
586 ));
587 self
588 }
589
590 pub fn circle(&mut self, cx: f64, cy: f64, r: f64, fill: &str, stroke: &str) -> &mut Self {
592 self.elements.push(format!(
593 r#"<circle cx="{:.3}" cy="{:.3}" r="{:.3}" fill="{}" stroke="{}"/>"#,
594 cx, cy, r, fill, stroke
595 ));
596 self
597 }
598
599 pub fn line(
601 &mut self,
602 x1: f64,
603 y1: f64,
604 x2: f64,
605 y2: f64,
606 stroke: &str,
607 width: f64,
608 ) -> &mut Self {
609 self.elements.push(format!(
610 r#"<line x1="{:.3}" y1="{:.3}" x2="{:.3}" y2="{:.3}" stroke="{}" stroke-width="{:.3}"/>"#,
611 x1, y1, x2, y2, stroke, width
612 ));
613 self
614 }
615
616 pub fn text(&mut self, x: f64, y: f64, content: &str, size: usize, anchor: &str) -> &mut Self {
618 self.elements.push(format!(
619 r#"<text x="{:.3}" y="{:.3}" font-size="{}" text-anchor="{}">{}</text>"#,
620 x,
621 y,
622 size,
623 anchor,
624 escape_xml(content)
625 ));
626 self
627 }
628
629 pub fn build(&self) -> String {
631 let mut svg = format!(
632 r#"<svg xmlns="http://www.w3.org/2000/svg" width="{}" height="{}" viewBox="0 0 {} {}">"#,
633 self.width, self.height, self.width, self.height
634 );
635 for elem in &self.elements {
636 svg.push('\n');
637 svg.push_str(elem);
638 }
639 svg.push_str("\n</svg>");
640 svg
641 }
642}
643
644fn escape_xml(s: &str) -> String {
646 s.replace('&', "&")
647 .replace('<', "<")
648 .replace('>', ">")
649 .replace('"', """)
650}
651
652pub struct ConstraintPlotter {
658 config: PlotConfig,
659}
660
661impl ConstraintPlotter {
662 pub fn new(config: PlotConfig) -> Self {
664 Self { config }
665 }
666
667 fn world_to_svg(&self, x: f64, y: f64) -> (f64, f64) {
671 let px = (x - self.config.x_range.0) / (self.config.x_range.1 - self.config.x_range.0)
672 * self.config.width as f64;
673 let py = (1.0
674 - (y - self.config.y_range.0) / (self.config.y_range.1 - self.config.y_range.0))
675 * self.config.height as f64;
676 (px, py)
677 }
678
679 fn push_background(&self, svg: &mut SvgBuilder) {
682 svg.rect(
683 0.0,
684 0.0,
685 self.config.width as f64,
686 self.config.height as f64,
687 "#ffffff",
688 1.0,
689 );
690 }
691
692 fn push_grid(&self, svg: &mut SvgBuilder) {
693 if !self.config.grid {
694 return;
695 }
696 let n_lines = 10usize;
697 let w = self.config.width as f64;
698 let h = self.config.height as f64;
699 for i in 0..=n_lines {
700 let frac = i as f64 / n_lines as f64;
701 let x = frac * w;
702 let y = frac * h;
703 svg.line(x, 0.0, x, h, "#e0e0e0", 0.5);
704 svg.line(0.0, y, w, y, "#e0e0e0", 0.5);
705 }
706 }
707
708 fn push_title(&self, svg: &mut SvgBuilder) {
709 if !self.config.title.is_empty() {
710 svg.text(
711 self.config.width as f64 / 2.0,
712 16.0,
713 &self.config.title,
714 14,
715 "middle",
716 );
717 }
718 }
719
720 pub fn plot_constraint_region<F>(&self, constraint_fn: F, resolution: usize) -> String
730 where
731 F: Fn(f64, f64) -> f64,
732 {
733 let res = resolution.max(1);
734 let (x0, x1) = self.config.x_range;
735 let (y0, y1) = self.config.y_range;
736 let dx = (x1 - x0) / res as f64;
737 let dy = (y1 - y0) / res as f64;
738
739 let mut values = Vec::with_capacity(res * res);
741 let mut max_viol = 1e-9_f64;
742 for row in 0..res {
743 for col in 0..res {
744 let cx = x0 + (col as f64 + 0.5) * dx;
745 let cy = y0 + (row as f64 + 0.5) * dy;
746 let v = constraint_fn(cx, cy);
747 if v > max_viol {
748 max_viol = v;
749 }
750 values.push((cx, cy, v));
751 }
752 }
753
754 let cell_w = self.config.width as f64 / res as f64;
755 let cell_h = self.config.height as f64 / res as f64;
756
757 let mut svg = SvgBuilder::new(self.config.width, self.config.height);
758 self.push_background(&mut svg);
759 self.push_grid(&mut svg);
760
761 for (cx, cy, v) in &values {
762 let t = if *v <= 0.0 {
764 0.0
765 } else {
766 (*v / max_viol).clamp(0.0, 1.0)
767 };
768 let (r, g, b) = self.config.colormap.map(t);
769 let fill = format!("#{:02X}{:02X}{:02X}", r, g, b);
770 let (px, py) = self.world_to_svg(*cx - dx * 0.5, *cy + dy * 0.5);
771 svg.rect(px, py, cell_w, cell_h, &fill, 0.85);
772 }
773
774 self.push_title(&mut svg);
775 svg.build()
776 }
777
778 pub fn plot_feasible_region<F>(&self, constraints: &[F], resolution: usize) -> String
783 where
784 F: Fn(f64, f64) -> f64,
785 {
786 if constraints.is_empty() {
788 let mut svg = SvgBuilder::new(self.config.width, self.config.height);
789 self.push_background(&mut svg);
790 self.push_grid(&mut svg);
791 svg.rect(
792 0.0,
793 0.0,
794 self.config.width as f64,
795 self.config.height as f64,
796 "#00cc00",
797 0.4,
798 );
799 self.push_title(&mut svg);
800 return svg.build();
801 }
802
803 let res = resolution.max(1);
804 let (x0, x1) = self.config.x_range;
805 let (y0, y1) = self.config.y_range;
806 let dx = (x1 - x0) / res as f64;
807 let dy = (y1 - y0) / res as f64;
808 let cell_w = self.config.width as f64 / res as f64;
809 let cell_h = self.config.height as f64 / res as f64;
810
811 let mut svg = SvgBuilder::new(self.config.width, self.config.height);
812 self.push_background(&mut svg);
813 self.push_grid(&mut svg);
814
815 for row in 0..res {
816 for col in 0..res {
817 let cx = x0 + (col as f64 + 0.5) * dx;
818 let cy = y0 + (row as f64 + 0.5) * dy;
819
820 let max_viol = constraints
821 .iter()
822 .map(|f| f(cx, cy))
823 .fold(f64::NEG_INFINITY, f64::max);
824
825 let fill = if max_viol <= 0.0 {
826 "#00cc00".to_string()
827 } else {
828 "#cc0000".to_string()
829 };
830 let (px, py) = self.world_to_svg(cx - dx * 0.5, cy + dy * 0.5);
831 svg.rect(px, py, cell_w, cell_h, &fill, 0.6);
832 }
833 }
834
835 self.push_title(&mut svg);
836 svg.build()
837 }
838
839 pub fn plot_trajectory(
844 &self,
845 points: &[(f64, f64)],
846 constraint_fn: Option<&dyn Fn(f64, f64) -> f64>,
847 ) -> String {
848 let mut svg = SvgBuilder::new(self.config.width, self.config.height);
849 self.push_background(&mut svg);
850 self.push_grid(&mut svg);
851
852 if points.is_empty() {
853 self.push_title(&mut svg);
854 return svg.build();
855 }
856
857 let svg_pts: Vec<(f64, f64)> = points
859 .iter()
860 .map(|&(x, y)| self.world_to_svg(x, y))
861 .collect();
862
863 for i in 1..svg_pts.len() {
864 let (x1, y1) = svg_pts[i - 1];
865 let (x2, y2) = svg_pts[i];
866 svg.line(x1, y1, x2, y2, "#888888", 1.5);
867 }
868
869 for (idx, &(x, y)) in points.iter().enumerate() {
871 let violated = constraint_fn.is_some_and(|f| f(x, y) > 0.0);
872 let fill = if violated { "#cc0000" } else { "#00aa44" };
873 let (px, py) = svg_pts[idx];
874 svg.circle(px, py, 4.0, fill, "#333333");
875 }
876
877 self.push_title(&mut svg);
878 svg.build()
879 }
880}
881
882#[derive(Debug, Clone)]
888pub struct ConstraintNetworkNode {
889 pub id: usize,
891 pub label: String,
893 pub constraint_type: String,
895 pub satisfied: Option<bool>,
898}
899
900#[derive(Debug, Clone)]
902pub struct ConstraintNetworkEdge {
903 pub from: usize,
905 pub to: usize,
907 pub label: String,
909}
910
911pub fn render_constraint_network(
915 nodes: &[ConstraintNetworkNode],
916 edges: &[ConstraintNetworkEdge],
917 width: usize,
918 height: usize,
919) -> String {
920 let mut svg = SvgBuilder::new(width, height);
921
922 svg.rect(0.0, 0.0, width as f64, height as f64, "#ffffff", 1.0);
924
925 if nodes.is_empty() {
926 return svg.build();
927 }
928
929 let center_x = width as f64 / 2.0;
930 let center_y = height as f64 / 2.0;
931 let radius = (width.min(height) as f64 / 2.0 - 60.0).max(20.0);
932 let node_r = 22.0_f64;
933
934 let positions: Vec<(f64, f64)> = nodes
936 .iter()
937 .enumerate()
938 .map(|(i, _)| {
939 let angle = 2.0 * std::f64::consts::PI * (i as f64) / (nodes.len() as f64);
940 let cx = center_x + radius * angle.cos();
941 let cy = center_y + radius * angle.sin();
942 (cx, cy)
943 })
944 .collect();
945
946 let id_to_idx: std::collections::HashMap<usize, usize> = nodes
948 .iter()
949 .enumerate()
950 .map(|(idx, n)| (n.id, idx))
951 .collect();
952
953 for edge in edges {
955 let from_idx = match id_to_idx.get(&edge.from) {
956 Some(&i) => i,
957 None => continue,
958 };
959 let to_idx = match id_to_idx.get(&edge.to) {
960 Some(&i) => i,
961 None => continue,
962 };
963 let (x1, y1) = positions[from_idx];
964 let (x2, y2) = positions[to_idx];
965 svg.line(x1, y1, x2, y2, "#666666", 1.5);
966
967 if !edge.label.is_empty() {
969 svg.text(
970 (x1 + x2) / 2.0,
971 (y1 + y2) / 2.0 - 4.0,
972 &edge.label,
973 10,
974 "middle",
975 );
976 }
977 }
978
979 for (i, node) in nodes.iter().enumerate() {
981 let (cx, cy) = positions[i];
982 let fill = match node.satisfied {
983 Some(true) => "#44bb77",
984 Some(false) => "#cc4444",
985 None => "#8888cc",
986 };
987 svg.circle(cx, cy, node_r, fill, "#333333");
988 svg.text(cx, cy + 4.0, &node.label, 10, "middle");
989 }
990
991 svg.build()
992}
993
994pub fn render_violation_heatmap(
1004 points: &[(f64, f64)],
1005 violations: &[f64],
1006 config: &PlotConfig,
1007) -> String {
1008 let mut svg = SvgBuilder::new(config.width, config.height);
1009 svg.rect(
1010 0.0,
1011 0.0,
1012 config.width as f64,
1013 config.height as f64,
1014 "#ffffff",
1015 1.0,
1016 );
1017
1018 if config.grid {
1019 let n_lines = 10usize;
1020 let w = config.width as f64;
1021 let h = config.height as f64;
1022 for i in 0..=n_lines {
1023 let frac = i as f64 / n_lines as f64;
1024 svg.line(frac * w, 0.0, frac * w, h, "#e0e0e0", 0.5);
1025 svg.line(0.0, frac * h, w, frac * h, "#e0e0e0", 0.5);
1026 }
1027 }
1028
1029 let max_viol = violations.iter().cloned().fold(1e-9_f64, f64::max);
1030
1031 let n = points.len().min(violations.len());
1032 for i in 0..n {
1033 let (x, y) = points[i];
1034 let v = violations[i];
1035
1036 let px =
1038 (x - config.x_range.0) / (config.x_range.1 - config.x_range.0) * config.width as f64;
1039 let py = (1.0 - (y - config.y_range.0) / (config.y_range.1 - config.y_range.0))
1040 * config.height as f64;
1041
1042 let t = (v / max_viol).clamp(0.0, 1.0);
1043 let (r, g, b) = config.colormap.map(t);
1044 let fill = format!("#{:02X}{:02X}{:02X}", r, g, b);
1045
1046 svg.circle(px, py, 6.0, &fill, "#333333");
1047 }
1048
1049 if !config.title.is_empty() {
1050 svg.text(config.width as f64 / 2.0, 16.0, &config.title, 14, "middle");
1051 }
1052
1053 svg.build()
1054}
1055
1056#[cfg(test)]
1057mod tests {
1058 use super::*;
1059 use crate::ConstraintBuilder;
1060
1061 #[test]
1062 fn test_violation_stats() {
1063 let constraint = ConstraintBuilder::new()
1064 .name("test")
1065 .less_than(10.0)
1066 .build()
1067 .unwrap();
1068
1069 let values = vec![5.0, 8.0, 12.0, 15.0, 9.0];
1070 let stats = ViolationStats::from_values(&constraint, &values);
1071
1072 assert_eq!(stats.num_samples, 5);
1073 assert_eq!(stats.num_violations, 2); assert!(stats.violation_rate() > 0.39 && stats.violation_rate() < 0.41);
1075 }
1076
1077 #[test]
1078 fn test_constraint_time_series() {
1079 let constraint = ConstraintBuilder::new()
1080 .name("test")
1081 .less_than(10.0)
1082 .build()
1083 .unwrap();
1084
1085 let mut ts = ConstraintTimeSeries::new("test_series");
1086 ts.add_sample(&constraint, 5.0, 0);
1087 ts.add_sample(&constraint, 12.0, 1);
1088 ts.add_sample(&constraint, 15.0, 2);
1089 ts.add_sample(&constraint, 8.0, 3);
1090
1091 let stats = ts.stats();
1092 assert_eq!(stats.num_samples, 4);
1093 assert_eq!(stats.num_violations, 2);
1094
1095 let periods = ts.violation_periods();
1096 assert_eq!(periods.len(), 1); assert_eq!(periods[0].0, 1); assert_eq!(periods[0].1, 2); }
1100
1101 #[test]
1102 fn test_constraint_report() {
1103 let constraint1 = ConstraintBuilder::new()
1104 .name("c1")
1105 .less_than(10.0)
1106 .build()
1107 .unwrap();
1108
1109 let constraint2 = ConstraintBuilder::new()
1110 .name("c2")
1111 .less_than(5.0)
1112 .build()
1113 .unwrap();
1114
1115 let values = vec![3.0, 7.0, 12.0];
1116
1117 let mut report = ConstraintReport::new();
1118 report.add_constraint("c1", ViolationStats::from_values(&constraint1, &values));
1119 report.add_constraint("c2", ViolationStats::from_values(&constraint2, &values));
1120
1121 assert_eq!(report.constraint_names().len(), 2);
1122
1123 let most_violated = report.most_violated();
1124 assert!(most_violated.is_some());
1125 }
1126
1127 #[test]
1128 fn test_constraint_inspector() {
1129 let constraint = ConstraintBuilder::new()
1130 .name("test")
1131 .less_than(10.0)
1132 .build()
1133 .unwrap();
1134
1135 let mut inspector = ConstraintInspector::new();
1136 inspector.add_sample(5.0);
1137 inspector.add_sample(12.0);
1138 inspector.add_sample(8.0);
1139
1140 let result = inspector.inspect(&constraint, "test");
1141 assert_eq!(result.total_samples(), 3);
1142 assert_eq!(result.satisfied().len(), 2);
1143 assert_eq!(result.violated().len(), 1);
1144 }
1145
1146 #[test]
1149 fn test_svg_builder_produces_valid_xml() {
1150 let mut builder = SvgBuilder::new(100, 100);
1151 builder.rect(10.0, 10.0, 80.0, 80.0, "#ff0000", 1.0);
1152 builder.circle(50.0, 50.0, 20.0, "#00ff00", "none");
1153 let svg = builder.build();
1154 assert!(svg.starts_with("<svg"));
1155 assert!(svg.ends_with("</svg>"));
1156 assert!(svg.contains("rect"));
1157 assert!(svg.contains("circle"));
1158 }
1159
1160 #[test]
1161 fn test_colormap_viridis_range() {
1162 let cm = Colormap::Viridis;
1163 let (r0, g0, b0) = cm.map(0.0);
1164 let (r1, g1, b1) = cm.map(1.0);
1165 let _ = (r0, g0, b0, r1, g1, b1);
1167 }
1168
1169 #[test]
1170 fn test_colormap_rdgn_extremes() {
1171 let cm = Colormap::RdGn;
1172 let (r_red, _, _) = cm.map(0.0);
1173 let (_, g_green, _) = cm.map(1.0);
1174 assert_eq!(r_red, 255); assert_eq!(g_green, 255); }
1177
1178 #[test]
1179 fn test_plot_constraint_region_returns_svg() {
1180 let config = PlotConfig {
1181 width: 100,
1182 height: 100,
1183 x_range: (-2.0, 2.0),
1184 y_range: (-2.0, 2.0),
1185 ..Default::default()
1186 };
1187 let plotter = ConstraintPlotter::new(config);
1188 let svg = plotter.plot_constraint_region(|x, y| x * x + y * y - 1.0, 10);
1190 assert!(svg.contains("<svg"));
1191 assert!(svg.contains("rect"));
1192 }
1193
1194 #[test]
1195 fn test_plot_feasible_region() {
1196 let config = PlotConfig::default();
1197 let plotter = ConstraintPlotter::new(config);
1198 let constraints: Vec<Box<dyn Fn(f64, f64) -> f64>> = vec![
1199 Box::new(|x, _y| x - 1.0), Box::new(|_x, y| y - 1.0), ];
1202 let svg = plotter.plot_feasible_region(&constraints, 5);
1203 assert!(!svg.is_empty());
1204 }
1205
1206 #[test]
1207 fn test_plot_trajectory() {
1208 let config = PlotConfig::default();
1209 let plotter = ConstraintPlotter::new(config);
1210 let pts = vec![(0.0_f64, 0.0), (1.0, 1.0), (2.0, 2.0)];
1211 let svg = plotter.plot_trajectory(&pts, None);
1212 assert!(svg.contains("<svg"));
1213 }
1214
1215 #[test]
1216 fn test_render_constraint_network() {
1217 let nodes = vec![
1218 ConstraintNetworkNode {
1219 id: 0,
1220 label: "x >= 0".to_string(),
1221 constraint_type: "range".to_string(),
1222 satisfied: Some(true),
1223 },
1224 ConstraintNetworkNode {
1225 id: 1,
1226 label: "x <= 1".to_string(),
1227 constraint_type: "range".to_string(),
1228 satisfied: Some(false),
1229 },
1230 ];
1231 let edges = vec![ConstraintNetworkEdge {
1232 from: 0,
1233 to: 1,
1234 label: "AND".to_string(),
1235 }];
1236 let svg = render_constraint_network(&nodes, &edges, 400, 400);
1237 assert!(svg.contains("<svg"));
1238 assert!(svg.contains("circle") || svg.contains("ellipse"));
1239 }
1240
1241 #[test]
1242 fn test_render_violation_heatmap() {
1243 let pts = vec![(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)];
1244 let violations = vec![0.0, 0.5, 1.0];
1245 let config = PlotConfig {
1246 width: 100,
1247 height: 100,
1248 ..Default::default()
1249 };
1250 let svg = render_violation_heatmap(&pts, &violations, &config);
1251 assert!(!svg.is_empty());
1252 }
1253}