From slide measurements to a traceable report

Scope and measurement meaning

This workflow starts with physical-slide measurements. It does not read native whole slides, train a DNN or validate a candidate as one erythrocyte. The reusable package preserves distinctions needed for those later stages: measured red area, spot candidates, unresolved bulk events, explicit support and provenance. Software validation checks contracts and arithmetic, not biological accuracy.

The examples use deterministic synthetic data. One record is deliberately missing signal measurements and another has zero measured tissue support.

results <- bs_example()
bs_validate(results)
results$slides[, c("physical_slide_id", "organ", "tissue_area_mm2",
                   "red_area_mm2", "candidate_spots")]
#>   physical_slide_id  organ tissue_area_mm2 red_area_mm2 candidate_spots
#> 1          DEMO-001  Liver              10         0.00               0
#> 2          DEMO-002  Liver              20         0.20             110
#> 3          DEMO-003  Liver              15         0.45             240
#> 4          DEMO-004  Liver               0         0.00               0
#> 5          DEMO-005 Spleen              12         1.20            1200
#> 6          DEMO-006 Spleen              18         2.70            2700
#> 7          DEMO-007 Spleen              25         1.00             800
#> 8          DEMO-008 Spleen              16           NA              NA

Canonical tables identify physical slides with nonempty character strings, areas in square millimetres and nonnegative whole-valued counts. The operational count is candidate spots plus unresolved bulk events. Counts use numeric storage and must remain exactly representable within the documented double-precision bound. Existing QC/organ labels are metadata, never inferred from signal strength.

Pool numerators and support

A group’s red-positive fraction is 100 * sum(red_area_mm2) / sum(tissue_area_mm2). Its candidate density uses the same tissue denominator. Pooled rates are not means of slide rates: a small and a large slide contribute different support.

by_organ <- bs_summarize(results, strata = "organ")
by_organ[, c("organ", "n_slides", "tissue_area_mm2", "red_area_mm2",
             "red_percent", "spots_per_mm2")]
#>    organ n_slides tissue_area_mm2 red_area_mm2 red_percent spots_per_mm2
#> 1  Liver        4              45         0.65    1.444444      7.777778
#> 2 Spleen        4              71           NA          NA            NA
is.na(by_organ$red_percent[by_organ$organ == "Spleen"])
#> [1] TRUE

The spleen summary is missing because one contributing measurement is missing. It would be misleading to silently sum only its known values while retaining the full tissue denominator. A true zero numerator on positive support is zero; zero or unknown support gives a missing rate. Excluding incomplete or compromised slides requires a declared analysis decision and a retained membership record.

Group jointly by tissue and the supplied quality strata:

results$groups <- bs_summarize(results, strata = c("organ", "stratum"))
results$groups[, c("organ", "stratum", "n_slides", "red_percent")]
#>    organ                stratum n_slides red_percent
#> 1  Liver No substantial finding        1           0
#> 2  Liver         Structure only        1           1
#> 3  Liver            Raster only        1           3
#> 4  Liver              Uncertain        1          NA
#> 5 Spleen No substantial finding        1          10
#> 6 Spleen         Structure only        1          15
#> 7 Spleen            Raster only        1           4
#> 8 Spleen              Uncertain        1          NA

Only observed combinations appear. slide_ids retains membership. A qc_revision can replace metadata when it uniquely covers every measured slide; it cannot replace numerical measurements. Historical study-specific thresholds and exclusion decisions are not imposed by this package.

Import a preserved measurement table

The legacy importer accepts a canonical CSV or a saved analysis.json with a rows table. The p079 profile also maps tissue_mm2 and red_mm2; it does not assign controls or invent missing calibration. Source file identity is hashed.

source_file <- tempfile(fileext = ".csv")
utils::write.csv(results$slides, source_file, row.names = FALSE, na = "")
imported <- bs_import_legacy(source_file, profile = "canonical")
identical(imported$slides$physical_slide_id, results$slides$physical_slide_id)
#> [1] TRUE
bs_status(imported)
#> $kind
#> [1] "results"
#> 
#> $schema_version
#> [1] "1.0"
#> 
#> $slides
#> [1] 8
#> 
#> $scientific_status
#> [1] "exploratory"
#> 
#> $source
#> $source$path
#> [1] "/tmp/Rtmpokgo19/fileed0666c26e4.csv"
#> 
#> $source$sha256
#> [1] "55e5b25a3e459947f41aaad2c3c6cc109bb5b6fa1fc49f5a7a1c707aa3eb8123"
unlink(source_file)

The source hash is an integrity receipt, not proof of authentic acquisition or independent biological validation. Canonical result JSON exported by the package contains a schema-versioned slides table for portable exchange. The importer accepts that format as well as the historical rows format.

Compare runs on declared common support

Equal slide IDs alone do not prove equal measurement support. Here both runs explicitly reference the same synthetic support identity, so paired changes can be computed. Without matching support, support = "common" fails; the union view preserves unmatched records with unverifiable differences missing.

first <- results
second <- results
first$slides$support_id <- paste0("synthetic-support-", first$slides$physical_slide_id)
second$slides$support_id <- first$slides$support_id
second$slides$red_area_mm2[2] <- 0.25
comparison <- bs_compare(list(baseline = first, comparison = second))
comparison[, c("physical_slide_id", "support_verified", "red_area_mm2_delta")]
#>   physical_slide_id support_verified red_area_mm2_delta
#> 1          DEMO-001             TRUE               0.00
#> 2          DEMO-002             TRUE               0.05
#> 3          DEMO-003             TRUE               0.00
#> 4          DEMO-004             TRUE               0.00
#> 5          DEMO-005             TRUE               0.00
#> 6          DEMO-006             TRUE               0.00
#> 7          DEMO-007             TRUE               0.00
#> 8          DEMO-008             TRUE                 NA
results$comparison <- comparison

Preserve a report and tables

Each delivery uses a new destination directory. Existing deliveries are never overwritten; the parent directory must exist. HTML text is escaped, exports keep missing values, and a SHA-256 manifest records generated files.

delivery <- tempfile("bloodspottr-example-report-")
report <- bs_report(results, delivery, author = "RH",
                    background = "Synthetic demonstration; no biological inference.")
list.files(delivery)
#> [1] "Comparison.csv" "Groups.csv"     "Methods.csv"    "report.html"   
#> [5] "results.json"   "SHA256SUMS"     "Slides.csv"
file.exists(c(report, file.path(delivery, "SHA256SUMS")))
#> [1] TRUE TRUE

bs_export_results(results, out) writes tables without HTML. Set xlsx = TRUE for optional openxlsx2 workbook output. The initial renderer does not embed raw image masks, native detail crops, training weights or individual events; the study-specific TeX/PDF supplement is a separate artifact.

if (requireNamespace("openxlsx2", quietly = TRUE)) {
  workbook_delivery <- tempfile("bloodspottr-example-xlsx-")
  bs_export_results(results, workbook_delivery, xlsx = TRUE)
  file.exists(file.path(workbook_delivery, "results.xlsx"))
  unlink(workbook_delivery, recursive = TRUE)
}

Explore with Shiny

bs_app(results)                  # Local interactive session
app <- bs_app(launch = FALSE)    # Empty application object; explicit demo button

The application filters by tissue, recorded QC stratum and literal slide ID. Pooled cards and plots follow the selected rows; the source object is unchanged. It exposes exact measurements, supplied comparisons, provenance and filtered CSV exports. Its explicit demonstration label prevents synthetic values from being mistaken for a loaded study. Browser uploads accept CSV/JSON, not executable serialized objects. It does not launch a GPU backend or claim native annotation editing or model training controls.