Skip to main content

scirs2_ndimage/visualization/
reports.rs

1//! Comprehensive Report Generation System
2//!
3//! This module provides a complete report generation system for image analysis results,
4//! supporting multiple output formats (HTML, Markdown, Text) and comprehensive
5//! analysis sections including basic statistics, quality metrics, and texture analysis.
6
7use scirs2_core::ndarray::ArrayStatCompat;
8use scirs2_core::ndarray::ArrayView2;
9use scirs2_core::numeric::{Float, FromPrimitive, ToPrimitive, Zero};
10use std::fmt::{Debug, Write};
11
12use crate::analysis::{ImageQualityMetrics, TextureMetrics};
13use crate::error::{NdimageError, NdimageResult};
14use crate::visualization::types::{ReportConfig, ReportFormat};
15use statrs::statistics::Statistics;
16
17/// Generate a comprehensive analysis report
18///
19/// This function creates a detailed report containing image information, statistics,
20/// quality metrics, and texture analysis based on the provided configuration.
21///
22/// # Arguments
23///
24/// * `image` - The image data as a 2D array view
25/// * `qualitymetrics` - Optional quality metrics for the image
26/// * `texturemetrics` - Optional texture analysis metrics
27/// * `config` - Configuration specifying report format and sections to include
28///
29/// # Returns
30///
31/// A formatted report string in the specified format (HTML, Markdown, or Text)
32///
33/// # Examples
34///
35/// ```rust,ignore
36/// use scirs2_core::ndarray::Array2;
37/// use scirs2_ndimage::visualization::{ReportConfig, ReportFormat, generate_report};
38///
39/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
40/// let image = Array2::zeros((100, 100));
41/// let config = ReportConfig::new()
42///     .with_format(ReportFormat::Markdown)
43///     .with_header("Analysis Report", "SciRS2");
44///
45/// let report = generate_report(&image.view(), None, None, &config)?;
46/// # Ok(())
47/// # }
48/// ```
49#[allow(dead_code)]
50pub fn generate_report<T>(
51    image: &ArrayView2<T>,
52    qualitymetrics: Option<&ImageQualityMetrics<T>>,
53    texturemetrics: Option<&TextureMetrics<T>>,
54    config: &ReportConfig,
55) -> NdimageResult<String>
56where
57    T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
58{
59    let mut report = String::new();
60
61    match config.format {
62        ReportFormat::Html => {
63            writeln!(&mut report, "<!DOCTYPE html>")?;
64            writeln!(&mut report, "<html><head><title>{}</title>", config.title)?;
65            writeln!(&mut report, "<style>")?;
66            writeln!(
67                &mut report,
68                "body {{ font-family: Arial, sans-serif; margin: 20px; }}"
69            )?;
70            writeln!(
71                &mut report,
72                "table {{ border-collapse: collapse; width: 100%; }}"
73            )?;
74            writeln!(
75                &mut report,
76                "th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}"
77            )?;
78            writeln!(&mut report, "th {{ background-color: #f2f2f2; }}")?;
79            writeln!(
80                &mut report,
81                ".metric-value {{ font-weight: bold; color: #2E86AB; }}"
82            )?;
83            writeln!(&mut report, "</style></head><body>")?;
84            writeln!(&mut report, "<h1>{}</h1>", config.title)?;
85            writeln!(
86                &mut report,
87                "<p><em>Generated by {}</em></p>",
88                config.author
89            )?;
90        }
91        ReportFormat::Markdown => {
92            writeln!(&mut report, "# {}", config.title)?;
93            writeln!(&mut report)?;
94            writeln!(&mut report, "*Generated by {}*", config.author)?;
95            writeln!(&mut report)?;
96        }
97        ReportFormat::Text => {
98            writeln!(&mut report, "{}", config.title)?;
99            writeln!(&mut report, "{}", "=".repeat(config.title.len()))?;
100            writeln!(&mut report)?;
101            writeln!(&mut report, "Generated by: {}", config.author)?;
102            writeln!(&mut report)?;
103        }
104    }
105
106    // Basic image information
107    let (height, width) = image.dim();
108    add_image_info(&mut report, width, height, config.format)?;
109
110    // Basic statistics
111    if config.include_statistics {
112        add_basic_statistics(&mut report, image, config.format)?;
113    }
114
115    // Quality metrics
116    if config.include_qualitymetrics {
117        if let Some(metrics) = qualitymetrics {
118            add_quality_metrics(&mut report, metrics, config.format)?;
119        }
120    }
121
122    // Texture analysis
123    if config.includetexture_analysis {
124        if let Some(metrics) = texturemetrics {
125            add_texture_metrics(&mut report, metrics, config.format)?;
126        }
127    }
128
129    // Close HTML if needed
130    if config.format == ReportFormat::Html {
131        writeln!(&mut report, "</body></html>")?;
132    }
133
134    Ok(report)
135}
136
137/// Add basic image information to the report
138///
139/// Includes dimensions, total pixels, and aspect ratio information.
140#[allow(dead_code)]
141pub fn add_image_info(
142    report: &mut String,
143    width: usize,
144    height: usize,
145    format: ReportFormat,
146) -> Result<(), std::fmt::Error> {
147    match format {
148        ReportFormat::Html => {
149            writeln!(report, "<h2>Image Information</h2>")?;
150            writeln!(report, "<table>")?;
151            writeln!(report, "<tr><th>Property</th><th>Value</th></tr>")?;
152            writeln!(
153                report,
154                "<tr><td>Width</td><td class='metric-value'>{}</td></tr>",
155                width
156            )?;
157            writeln!(
158                report,
159                "<tr><td>Height</td><td class='metric-value'>{}</td></tr>",
160                height
161            )?;
162            writeln!(
163                report,
164                "<tr><td>Total Pixels</td><td class='metric-value'>{}</td></tr>",
165                width * height
166            )?;
167            writeln!(
168                report,
169                "<tr><td>Aspect Ratio</td><td class='metric-value'>{:.3}</td></tr>",
170                width as f64 / height as f64
171            )?;
172            writeln!(report, "</table>")?;
173        }
174        ReportFormat::Markdown => {
175            writeln!(report, "## Image Information")?;
176            writeln!(report)?;
177            writeln!(report, "| Property | Value |")?;
178            writeln!(report, "|----------|-------|")?;
179            writeln!(report, "| Width | {} |", width)?;
180            writeln!(report, "| Height | {} |", height)?;
181            writeln!(report, "| Total Pixels | {} |", width * height)?;
182            writeln!(
183                report,
184                "| Aspect Ratio | {:.3} |",
185                width as f64 / height as f64
186            )?;
187            writeln!(report)?;
188        }
189        ReportFormat::Text => {
190            writeln!(report, "Image Information")?;
191            writeln!(report, "-----------------")?;
192            writeln!(report, "Width:        {}", width)?;
193            writeln!(report, "Height:       {}", height)?;
194            writeln!(report, "Total Pixels: {}", width * height)?;
195            writeln!(report, "Aspect Ratio: {:.3}", width as f64 / height as f64)?;
196            writeln!(report)?;
197        }
198    }
199    Ok(())
200}
201
202/// Add basic statistical information to the report
203///
204/// Computes and includes mean, standard deviation, variance, minimum, maximum, and range.
205#[allow(dead_code)]
206pub fn add_basic_statistics<T>(
207    report: &mut String,
208    image: &ArrayView2<T>,
209    format: ReportFormat,
210) -> Result<(), std::fmt::Error>
211where
212    T: Float + FromPrimitive + ToPrimitive + Debug + Clone,
213{
214    let mean = image.mean_or(T::zero());
215    let min_val = image.iter().cloned().fold(T::infinity(), T::min);
216    let max_val = image.iter().cloned().fold(T::neg_infinity(), T::max);
217
218    let variance = image
219        .mapv(|x| (x - mean) * (x - mean))
220        .mean()
221        .unwrap_or(T::zero());
222    let std_dev = variance.sqrt();
223
224    match format {
225        ReportFormat::Html => {
226            writeln!(report, "<h2>Basic Statistics</h2>")?;
227            writeln!(report, "<table>")?;
228            writeln!(report, "<tr><th>Statistic</th><th>Value</th></tr>")?;
229            writeln!(
230                report,
231                "<tr><td>Mean</td><td class='metric-value'>{:.6}</td></tr>",
232                mean.to_f64().unwrap_or(0.0)
233            )?;
234            writeln!(
235                report,
236                "<tr><td>Standard Deviation</td><td class='metric-value'>{:.6}</td></tr>",
237                std_dev.to_f64().unwrap_or(0.0)
238            )?;
239            writeln!(
240                report,
241                "<tr><td>Variance</td><td class='metric-value'>{:.6}</td></tr>",
242                variance.to_f64().unwrap_or(0.0)
243            )?;
244            writeln!(
245                report,
246                "<tr><td>Minimum</td><td class='metric-value'>{:.6}</td></tr>",
247                min_val.to_f64().unwrap_or(0.0)
248            )?;
249            writeln!(
250                report,
251                "<tr><td>Maximum</td><td class='metric-value'>{:.6}</td></tr>",
252                max_val.to_f64().unwrap_or(0.0)
253            )?;
254            writeln!(
255                report,
256                "<tr><td>Range</td><td class='metric-value'>{:.6}</td></tr>",
257                (max_val - min_val).to_f64().unwrap_or(0.0)
258            )?;
259            writeln!(report, "</table>")?;
260        }
261        ReportFormat::Markdown => {
262            writeln!(report, "## Basic Statistics")?;
263            writeln!(report)?;
264            writeln!(report, "| Statistic | Value |")?;
265            writeln!(report, "|-----------|-------|")?;
266            writeln!(report, "| Mean | {:.6} |", mean.to_f64().unwrap_or(0.0))?;
267            writeln!(
268                report,
269                "| Standard Deviation | {:.6} |",
270                std_dev.to_f64().unwrap_or(0.0)
271            )?;
272            writeln!(
273                report,
274                "| Variance | {:.6} |",
275                variance.to_f64().unwrap_or(0.0)
276            )?;
277            writeln!(
278                report,
279                "| Minimum | {:.6} |",
280                min_val.to_f64().unwrap_or(0.0)
281            )?;
282            writeln!(
283                report,
284                "| Maximum | {:.6} |",
285                max_val.to_f64().unwrap_or(0.0)
286            )?;
287            writeln!(
288                report,
289                "| Range | {:.6} |",
290                (max_val - min_val).to_f64().unwrap_or(0.0)
291            )?;
292            writeln!(report)?;
293        }
294        ReportFormat::Text => {
295            writeln!(report, "Basic Statistics")?;
296            writeln!(report, "----------------")?;
297            writeln!(
298                report,
299                "Mean:              {:.6}",
300                mean.to_f64().unwrap_or(0.0)
301            )?;
302            writeln!(
303                report,
304                "Standard Deviation:{:.6}",
305                std_dev.to_f64().unwrap_or(0.0)
306            )?;
307            writeln!(
308                report,
309                "Variance:          {:.6}",
310                variance.to_f64().unwrap_or(0.0)
311            )?;
312            writeln!(
313                report,
314                "Minimum:           {:.6}",
315                min_val.to_f64().unwrap_or(0.0)
316            )?;
317            writeln!(
318                report,
319                "Maximum:           {:.6}",
320                max_val.to_f64().unwrap_or(0.0)
321            )?;
322            writeln!(
323                report,
324                "Range:             {:.6}",
325                (max_val - min_val).to_f64().unwrap_or(0.0)
326            )?;
327            writeln!(report)?;
328        }
329    }
330    Ok(())
331}
332
333/// Add image quality metrics to the report
334///
335/// Includes PSNR, SSIM, MSE, SNR, entropy, and sharpness measurements.
336#[allow(dead_code)]
337pub fn add_quality_metrics<T>(
338    report: &mut String,
339    metrics: &ImageQualityMetrics<T>,
340    format: ReportFormat,
341) -> Result<(), std::fmt::Error>
342where
343    T: Float + ToPrimitive,
344{
345    match format {
346        ReportFormat::Html => {
347            writeln!(report, "<h2>Quality Metrics</h2>")?;
348            writeln!(report, "<table>")?;
349            writeln!(report, "<tr><th>Metric</th><th>Value</th></tr>")?;
350            writeln!(
351                report,
352                "<tr><td>PSNR</td><td class='metric-value'>{:.3} dB</td></tr>",
353                metrics.psnr.to_f64().unwrap_or(0.0)
354            )?;
355            writeln!(
356                report,
357                "<tr><td>SSIM</td><td class='metric-value'>{:.6}</td></tr>",
358                metrics.ssim.to_f64().unwrap_or(0.0)
359            )?;
360            writeln!(
361                report,
362                "<tr><td>MSE</td><td class='metric-value'>{:.6}</td></tr>",
363                metrics.mse.to_f64().unwrap_or(0.0)
364            )?;
365            writeln!(
366                report,
367                "<tr><td>SNR</td><td class='metric-value'>{:.3} dB</td></tr>",
368                metrics.snr.to_f64().unwrap_or(0.0)
369            )?;
370            writeln!(
371                report,
372                "<tr><td>Entropy</td><td class='metric-value'>{:.3} bits</td></tr>",
373                metrics.entropy.to_f64().unwrap_or(0.0)
374            )?;
375            writeln!(
376                report,
377                "<tr><td>Sharpness</td><td class='metric-value'>{:.6}</td></tr>",
378                metrics.sharpness.to_f64().unwrap_or(0.0)
379            )?;
380            writeln!(report, "</table>")?;
381        }
382        ReportFormat::Markdown => {
383            writeln!(report, "## Quality Metrics")?;
384            writeln!(report)?;
385            writeln!(report, "| Metric | Value |")?;
386            writeln!(report, "|--------|-------|")?;
387            writeln!(
388                report,
389                "| PSNR | {:.3} dB |",
390                metrics.psnr.to_f64().unwrap_or(0.0)
391            )?;
392            writeln!(
393                report,
394                "| SSIM | {:.6} |",
395                metrics.ssim.to_f64().unwrap_or(0.0)
396            )?;
397            writeln!(
398                report,
399                "| MSE | {:.6} |",
400                metrics.mse.to_f64().unwrap_or(0.0)
401            )?;
402            writeln!(
403                report,
404                "| SNR | {:.3} dB |",
405                metrics.snr.to_f64().unwrap_or(0.0)
406            )?;
407            writeln!(
408                report,
409                "| Entropy | {:.3} bits |",
410                metrics.entropy.to_f64().unwrap_or(0.0)
411            )?;
412            writeln!(
413                report,
414                "| Sharpness | {:.6} |",
415                metrics.sharpness.to_f64().unwrap_or(0.0)
416            )?;
417            writeln!(report)?;
418        }
419        ReportFormat::Text => {
420            writeln!(report, "Quality Metrics")?;
421            writeln!(report, "---------------")?;
422            writeln!(
423                report,
424                "PSNR:      {:.3} dB",
425                metrics.psnr.to_f64().unwrap_or(0.0)
426            )?;
427            writeln!(
428                report,
429                "SSIM:      {:.6}",
430                metrics.ssim.to_f64().unwrap_or(0.0)
431            )?;
432            writeln!(
433                report,
434                "MSE:       {:.6}",
435                metrics.mse.to_f64().unwrap_or(0.0)
436            )?;
437            writeln!(
438                report,
439                "SNR:       {:.3} dB",
440                metrics.snr.to_f64().unwrap_or(0.0)
441            )?;
442            writeln!(
443                report,
444                "Entropy:   {:.3} bits",
445                metrics.entropy.to_f64().unwrap_or(0.0)
446            )?;
447            writeln!(
448                report,
449                "Sharpness: {:.6}",
450                metrics.sharpness.to_f64().unwrap_or(0.0)
451            )?;
452            writeln!(report)?;
453        }
454    }
455    Ok(())
456}
457
458/// Add texture analysis metrics to the report
459///
460/// Includes GLCM-based metrics, LBP uniformity, Gabor filter responses, and fractal dimension.
461#[allow(dead_code)]
462pub fn add_texture_metrics<T>(
463    report: &mut String,
464    metrics: &TextureMetrics<T>,
465    format: ReportFormat,
466) -> Result<(), std::fmt::Error>
467where
468    T: Float + ToPrimitive,
469{
470    match format {
471        ReportFormat::Html => {
472            writeln!(report, "<h2>Texture Analysis</h2>")?;
473            writeln!(report, "<table>")?;
474            writeln!(report, "<tr><th>Metric</th><th>Value</th></tr>")?;
475            writeln!(
476                report,
477                "<tr><td>GLCM Contrast</td><td class='metric-value'>{:.6}</td></tr>",
478                metrics.glcm_contrast.to_f64().unwrap_or(0.0)
479            )?;
480            writeln!(
481                report,
482                "<tr><td>GLCM Homogeneity</td><td class='metric-value'>{:.6}</td></tr>",
483                metrics.glcm_homogeneity.to_f64().unwrap_or(0.0)
484            )?;
485            writeln!(
486                report,
487                "<tr><td>GLCM Energy</td><td class='metric-value'>{:.6}</td></tr>",
488                metrics.glcm_energy.to_f64().unwrap_or(0.0)
489            )?;
490            writeln!(
491                report,
492                "<tr><td>LBP Uniformity</td><td class='metric-value'>{:.6}</td></tr>",
493                metrics.lbp_uniformity.to_f64().unwrap_or(0.0)
494            )?;
495            writeln!(
496                report,
497                "<tr><td>Gabor Mean</td><td class='metric-value'>{:.6}</td></tr>",
498                metrics.gabor_mean.to_f64().unwrap_or(0.0)
499            )?;
500            writeln!(
501                report,
502                "<tr><td>Gabor Std</td><td class='metric-value'>{:.6}</td></tr>",
503                metrics.gabor_std.to_f64().unwrap_or(0.0)
504            )?;
505            writeln!(
506                report,
507                "<tr><td>Fractal Dimension</td><td class='metric-value'>{:.3}</td></tr>",
508                metrics.fractal_dimension.to_f64().unwrap_or(0.0)
509            )?;
510            writeln!(report, "</table>")?;
511        }
512        ReportFormat::Markdown => {
513            writeln!(report, "## Texture Analysis")?;
514            writeln!(report)?;
515            writeln!(report, "| Metric | Value |")?;
516            writeln!(report, "|--------|-------|")?;
517            writeln!(
518                report,
519                "| GLCM Contrast | {:.6} |",
520                metrics.glcm_contrast.to_f64().unwrap_or(0.0)
521            )?;
522            writeln!(
523                report,
524                "| GLCM Homogeneity | {:.6} |",
525                metrics.glcm_homogeneity.to_f64().unwrap_or(0.0)
526            )?;
527            writeln!(
528                report,
529                "| GLCM Energy | {:.6} |",
530                metrics.glcm_energy.to_f64().unwrap_or(0.0)
531            )?;
532            writeln!(
533                report,
534                "| LBP Uniformity | {:.6} |",
535                metrics.lbp_uniformity.to_f64().unwrap_or(0.0)
536            )?;
537            writeln!(
538                report,
539                "| Gabor Mean | {:.6} |",
540                metrics.gabor_mean.to_f64().unwrap_or(0.0)
541            )?;
542            writeln!(
543                report,
544                "| Gabor Std | {:.6} |",
545                metrics.gabor_std.to_f64().unwrap_or(0.0)
546            )?;
547            writeln!(
548                report,
549                "| Fractal Dimension | {:.3} |",
550                metrics.fractal_dimension.to_f64().unwrap_or(0.0)
551            )?;
552            writeln!(report)?;
553        }
554        ReportFormat::Text => {
555            writeln!(report, "Texture Analysis")?;
556            writeln!(report, "----------------")?;
557            writeln!(
558                report,
559                "GLCM Contrast:     {:.6}",
560                metrics.glcm_contrast.to_f64().unwrap_or(0.0)
561            )?;
562            writeln!(
563                report,
564                "GLCM Homogeneity:  {:.6}",
565                metrics.glcm_homogeneity.to_f64().unwrap_or(0.0)
566            )?;
567            writeln!(
568                report,
569                "GLCM Energy:       {:.6}",
570                metrics.glcm_energy.to_f64().unwrap_or(0.0)
571            )?;
572            writeln!(
573                report,
574                "LBP Uniformity:    {:.6}",
575                metrics.lbp_uniformity.to_f64().unwrap_or(0.0)
576            )?;
577            writeln!(
578                report,
579                "Gabor Mean:        {:.6}",
580                metrics.gabor_mean.to_f64().unwrap_or(0.0)
581            )?;
582            writeln!(
583                report,
584                "Gabor Std:         {:.6}",
585                metrics.gabor_std.to_f64().unwrap_or(0.0)
586            )?;
587            writeln!(
588                report,
589                "Fractal Dimension: {:.3}",
590                metrics.fractal_dimension.to_f64().unwrap_or(0.0)
591            )?;
592            writeln!(report)?;
593        }
594    }
595    Ok(())
596}
597
598#[cfg(test)]
599mod tests {
600    use super::*;
601    use crate::visualization::types::{ReportConfig, ReportFormat};
602    use scirs2_core::ndarray::Array2;
603
604    #[test]
605    fn test_generate_basic_report() {
606        let image = Array2::<f64>::ones((10, 10));
607        let config = ReportConfig::new()
608            .with_format(ReportFormat::Text)
609            .with_header("Test Report", "Test Suite")
610            .minimal(); // Only basic info, no advanced metrics
611
612        let result = generate_report(&image.view(), None, None, &config);
613        assert!(result.is_ok());
614
615        let report = result.expect("Operation failed");
616        assert!(report.contains("Test Report"));
617        assert!(report.contains("Test Suite"));
618        assert!(report.contains("Image Information"));
619        assert!(report.contains("Width:        10"));
620        assert!(report.contains("Height:       10"));
621    }
622
623    #[test]
624    fn test_generate_html_report() {
625        let image = Array2::<f64>::zeros((5, 5));
626        let config = ReportConfig::new()
627            .with_format(ReportFormat::Html)
628            .with_header("HTML Test", "HTML Suite")
629            .with_sections(true, false, false, false, false); // Only statistics
630
631        let result = generate_report(&image.view(), None, None, &config);
632        assert!(result.is_ok());
633
634        let report = result.expect("Operation failed");
635        assert!(report.contains("<!DOCTYPE html>"));
636        assert!(report.contains("<h1>HTML Test</h1>"));
637        assert!(report.contains("Basic Statistics"));
638        assert!(report.contains("</body></html>"));
639    }
640
641    #[test]
642    fn test_generate_markdown_report() {
643        let image = Array2::from_elem((3, 3), 2.5f64);
644        let config = ReportConfig::new()
645            .with_format(ReportFormat::Markdown)
646            .with_header("Markdown Test", "MD Suite")
647            .with_sections(true, false, false, false, false);
648
649        let result = generate_report(&image.view(), None, None, &config);
650        assert!(result.is_ok());
651
652        let report = result.expect("Operation failed");
653        assert!(report.contains("# Markdown Test"));
654        assert!(report.contains("*Generated by MD Suite*"));
655        assert!(report.contains("## Image Information"));
656        assert!(report.contains("## Basic Statistics"));
657        assert!(report.contains("| Mean | 2.500000 |"));
658    }
659
660    #[test]
661    fn test_add_image_info_all_formats() {
662        let mut html_report = String::new();
663        let mut md_report = String::new();
664        let mut text_report = String::new();
665
666        // Test HTML format
667        assert!(add_image_info(&mut html_report, 100, 200, ReportFormat::Html).is_ok());
668        assert!(html_report.contains("<h2>Image Information</h2>"));
669        assert!(html_report.contains("100"));
670        assert!(html_report.contains("200"));
671        assert!(html_report.contains("20000")); // Total pixels
672
673        // Test Markdown format
674        assert!(add_image_info(&mut md_report, 100, 200, ReportFormat::Markdown).is_ok());
675        assert!(md_report.contains("## Image Information"));
676        assert!(md_report.contains("| Width | 100 |"));
677
678        // Test Text format
679        assert!(add_image_info(&mut text_report, 100, 200, ReportFormat::Text).is_ok());
680        assert!(text_report.contains("Width:        100"));
681        assert!(text_report.contains("Height:       200"));
682        assert!(text_report.contains("Aspect Ratio: 0.500"));
683    }
684
685    #[test]
686    fn test_add_basic_statistics() {
687        let image =
688            Array2::from_shape_vec((2, 2), vec![1.0, 2.0, 3.0, 4.0]).expect("Operation failed");
689        let mut report = String::new();
690
691        let result = add_basic_statistics(&mut report, &image.view(), ReportFormat::Text);
692        assert!(result.is_ok());
693
694        assert!(report.contains("Basic Statistics"));
695        assert!(report.contains("Mean:              2.500000")); // Mean of [1,2,3,4]
696        assert!(report.contains("Minimum:           1.000000"));
697        assert!(report.contains("Maximum:           4.000000"));
698        assert!(report.contains("Range:             3.000000"));
699    }
700}