Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions docs/inspection-user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,30 @@ The inspector beneath the structure input is shared across tabs. Selecting a res

On a loaded screen with no selected residue, the inspector is reduced to a single review action. After selecting a residue, navigation controls become available. Representation buttons and the ligand, DNA, RNA, spin and rock switches remain visible beneath the viewer, so users can discover them without opening a settings menu.

## Why inspect this residue?

Selecting a residue opens a compact evidence explanation in the shared
inspector. RamplotR keeps the underlying signals separate instead of combining
them into an opaque quality score.

Depending on the data available for that residue, the explanation can surface:

- Rama8000 Allowed or Outlier status;
- the native RamplotR `Not allowed` region or proximity to a native contour;
- low or very low pLDDT;
- the combination of very high pLDDT with a Rama8000 outlier;
- cis or twisted peptide geometry;
- an attached official wwPDB Ramachandran or rotamer outlier;
- official local clashes or covalent-geometry outliers.

Each item identifies its source and explains why it may be worth examining.
If none of the available evidence is unusual, the panel says so explicitly
rather than inventing a warning.

This is an **inspection aid**, not a residue-quality score. A Rama8000
classification, prediction confidence, local geometry and an official wwPDB
annotation remain scientifically distinct observations.

## Compact multi-chain sequence map

The overview directly under the Ramachandran plot shows every selected
Expand Down
115 changes: 115 additions & 0 deletions shinyRam/R/inspection.R
Original file line number Diff line number Diff line change
@@ -1,6 +1,121 @@
# Pure residue inspection and pairwise comparison helpers.
# Nothing in this file changes torsion extraction or scientific classification.

ram_residue_evidence <- function(row, boundary_margin = 2) {
if (!is.data.frame(row) || nrow(row) != 1L)
stop("Residue evidence expects exactly one residue row.")
evidence <- list()
add <- function(level, title, detail, source) {
evidence[[length(evidence)+1L]] <<- data.frame(
level=level,title=title,detail=detail,source=source,
stringsAsFactors=FALSE
)
}
value <- function(name, default=NA) {
if (!name %in% names(row) || !length(row[[name]])) return(default)
row[[name]][[1L]]
}
phi <- suppressWarnings(as.numeric(value("phi",NA_real_)))
psi <- suppressWarnings(as.numeric(value("psi",NA_real_)))
native <- as.character(value("region",NA_character_))
standard <- as.character(value("rama8000_region",NA_character_))
plddt <- suppressWarnings(as.numeric(value("plddt",NA_real_)))

if (!is.finite(phi) || !is.finite(psi)) {
add("warning","Backbone angles unavailable",
"Phi/psi cannot be evaluated for this residue, commonly because it is terminal or required backbone atoms are missing.",
"Coordinates")
} else {
if (!is.na(standard) && standard == "Outlier") {
add("high","Rama8000 backbone outlier",
"The current six-class standard validation places this residue outside the allowed Rama8000 region.",
"Rama8000")
} else if (!is.na(standard) && standard == "Allowed") {
add("info","Rama8000 allowed region",
"The residue is outside the favored Rama8000 region but remains within the allowed region.",
"Rama8000")
}
if (!is.na(native) && native == "Not allowed") {
add("warning","Unusual RamplotR density position",
"The selected RamplotR reference distribution labels this position Not allowed. This is separate from Rama8000 outlier status.",
"RamplotR density")
}
density <- suppressWarnings(as.numeric(value("density",NA_real_)))
if (is.finite(density) &&
any(abs(density-c(85,98,99.95)) <= boundary_margin)) {
add("info","Near a RamplotR contour boundary",
sprintf("Density percentile %.1f lies within %.1f points of a display-classification contour.",
density,boundary_margin),
"RamplotR density")
}
}

if (is.finite(plddt)) {
if (plddt < 50) {
add("warning","Very low prediction confidence",
sprintf("pLDDT %.1f indicates very low local model confidence; local geometry should be interpreted cautiously.",plddt),
"Prediction confidence")
} else if (plddt < 70) {
add("info","Low prediction confidence",
sprintf("pLDDT %.1f indicates low local model confidence.",plddt),
"Prediction confidence")
}
if (plddt >= 90 && !is.na(standard) && standard == "Outlier") {
add("high","High-confidence prediction with unusual backbone geometry",
sprintf("pLDDT %.1f is very high while Rama8000 classifies the backbone as an outlier; inspect the local structural context.",plddt),
"Combined evidence")
}
}

omega_status <- as.character(value("omega_status",NA_character_))
omega <- suppressWarnings(as.numeric(value("omega",NA_real_)))
if (!is.na(omega_status) && omega_status == "Twisted") {
add("high","Twisted peptide bond",
if (is.finite(omega)) sprintf("Peptide omega is %.1f degrees.",omega)
else "The preceding peptide bond is classified as twisted.",
"Local geometry")
} else if (!is.na(omega_status) && omega_status == "Cis") {
add("info","Cis peptide bond",
if (is.finite(omega)) sprintf("Peptide omega is %.1f degrees.",omega)
else "The preceding peptide bond is cis.",
"Local geometry")
}

official_rama <- tolower(as.character(value("wwpdb_rama",NA_character_)))
if (!is.na(official_rama) && official_rama == "outlier")
add("high","Official wwPDB Ramachandran outlier",
"The attached official validation report identifies this residue as a Ramachandran outlier.",
"wwPDB")
rotamer <- tolower(as.character(value("wwpdb_rotamer",NA_character_)))
if (!is.na(rotamer) && rotamer %in% c("outlier","outliers"))
add("warning","Official wwPDB rotamer outlier",
"The attached official validation report identifies the side-chain rotamer as an outlier.",
"wwPDB")
clashes <- suppressWarnings(as.numeric(value("wwpdb_clashes",NA_real_)))
if (is.finite(clashes) && clashes > 0)
add("warning","Official wwPDB local clash",
sprintf("The attached report records %d local clash%s for this residue.",
as.integer(clashes),if (clashes==1) "" else "es"),
"wwPDB")
bond <- suppressWarnings(as.numeric(value("wwpdb_bond_outliers",NA_real_)))
angle <- suppressWarnings(as.numeric(value("wwpdb_angle_outliers",NA_real_)))
if ((is.finite(bond) && bond > 0) || (is.finite(angle) && angle > 0))
add("warning","Official covalent-geometry outlier",
sprintf("The attached report records %d bond-length and %d bond-angle outlier%s.",
ifelse(is.finite(bond),as.integer(bond),0L),
ifelse(is.finite(angle),as.integer(angle),0L),
ifelse((ifelse(is.finite(bond),bond,0)+ifelse(is.finite(angle),angle,0))==1,
"","s")),
"wwPDB")

if (!length(evidence))
return(data.frame(level=character(),title=character(),
detail=character(),source=character(),stringsAsFactors=FALSE))
out <- do.call(rbind,evidence)
priority <- match(out$level,c("high","warning","info"))
out[order(priority,out$source,out$title),,drop=FALSE]
}

ram_review_queue <- function(data, boundary_margin = 2) {
if (!nrow(data)) return(data)
missing <- is.na(data$phi) | is.na(data$psi) | is.na(data$region)
Expand Down
30 changes: 28 additions & 2 deletions shinyRam/app.R
Original file line number Diff line number Diff line change
Expand Up @@ -2758,6 +2758,16 @@ server <- function(input, output, session) {
if (is.null(row)) return(tags$span(class = "ram-inspector-empty",
"Select a residue in the plot, table, sequence or 3D model. Review controls navigate to the next issue."))
angle <- function(value) if (is.finite(value)) sprintf("%.1f°", value) else "Unavailable"
evidence <- ram_residue_evidence(row)
evidence_item <- function(item) {
level <- as.character(item$level[[1L]])
tags$div(class=paste("ram-evidence-item",paste0("ram-evidence-",level)),
tags$div(class="ram-evidence-item-head",
tags$strong(as.character(item$title[[1L]])),
tags$span(as.character(item$source[[1L]]))),
tags$p(as.character(item$detail[[1L]]))
)
}
tags$div(class = "ram-inspector-data",
tags$div(tags$strong(sprintf("%s %d%s · %s",
if (nzchar(row$chain[[1L]])) paste("Chain", row$chain[[1L]]) else "Chain",
Expand Down Expand Up @@ -2797,9 +2807,25 @@ server <- function(input, output, session) {
row$confidence_category[[1L]])),
if ("plddt" %in% names(row) && is.finite(row$plddt[[1L]]) &&
row$plddt[[1L]] >= 90 &&
identical(as.character(row$region[[1L]]), "Not allowed"))
identical(as.character(row$rama8000_region[[1L]]), "Outlier"))
tags$span(class = "ram-inspector-warning",
"High model confidence, unusual backbone geometry; inspect locally.")
"High model confidence with a Rama8000 outlier; inspect locally.")
),
tags$details(class="ram-evidence-panel",
open=if(nrow(evidence)>0L) "open" else NULL,
tags$summary(
if(nrow(evidence))
sprintf("Why inspect this residue? · %d signal%s",
nrow(evidence),if(nrow(evidence)==1L) "" else "s")
else "Why inspect this residue? · no obvious issue"
),
if(nrow(evidence))
tags$div(class="ram-evidence-list",
lapply(seq_len(nrow(evidence)),function(i)
evidence_item(evidence[i,,drop=FALSE])))
else
tags$p(class="ram-evidence-none",
"No unusual signal is present in the currently available backbone, prediction-confidence or attached official-validation evidence.")
)
)
})
Expand Down
28 changes: 28 additions & 0 deletions shinyRam/www/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -1293,3 +1293,31 @@ body > .container-fluid { max-width: none; padding: 0; }
.ram-counterpart-id { flex-direction:row; gap:7px; align-items:baseline; }
.ram-counterpart-actions { width:100%; flex-wrap:wrap; }
}

/* Evidence explanation for the selected residue. Signals stay separate rather
than being collapsed into a composite quality score. */
.ram-evidence-panel {
margin-top:8px; border-top:1px solid rgba(42,91,96,.14); padding-top:6px;
}
.ram-evidence-panel > summary {
cursor:pointer; color:#245a61; font-size:10px; font-weight:750;
list-style-position:outside;
}
.ram-evidence-list { display:grid; gap:5px; margin-top:6px; }
.ram-evidence-item {
border:1px solid #dce8e6; border-left-width:3px;
border-radius:6px; padding:6px 8px; background:#fbfdfc;
}
.ram-evidence-item-head {
display:flex; align-items:baseline; justify-content:space-between;
gap:8px; flex-wrap:wrap;
}
.ram-evidence-item-head strong { color:#294f55; font-size:10px; }
.ram-evidence-item-head span {
color:#75898d; font-size:8px; text-transform:uppercase; letter-spacing:.04em;
}
.ram-evidence-item p { margin:2px 0 0; color:#5b7379; font-size:9px; line-height:1.4; }
.ram-evidence-high { border-left-color:#b7473d; background:#fff8f6; }
.ram-evidence-warning { border-left-color:#c88a31; background:#fffaf0; }
.ram-evidence-info { border-left-color:#4f8f88; }
.ram-evidence-none { margin:6px 0 0; color:#687f84; font-size:9px; }
40 changes: 40 additions & 0 deletions tests/inspection.R
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,44 @@ assert(isTRUE(all.equal(seq_a$known_fraction,2/3)),
assert(identical(ram_chain_query_sequence(seq_fixture,"B")$sequence,"V"),
"Chain-specific counterpart sequence extraction changed.")

evidence_row <- data.frame(
chain="A",resi=10L,insertion_code="",resn="ALA",
phi=-120,psi=95,region="Not allowed",density=98.4,
rama8000_region="Outlier",rama8000_group="general",
rama8000_score=0.0001,plddt=96,confidence_category="Very high",
omega=72,omega_status="Twisted",
wwpdb_rama="outlier",wwpdb_rotamer="outlier",
wwpdb_clashes=2,wwpdb_bond_outliers=1,wwpdb_angle_outliers=0,
stringsAsFactors=FALSE
)
ev <- ram_residue_evidence(evidence_row)
assert(all(c(
"Rama8000 backbone outlier",
"Unusual RamplotR density position",
"High-confidence prediction with unusual backbone geometry",
"Twisted peptide bond",
"Official wwPDB Ramachandran outlier",
"Official wwPDB rotamer outlier",
"Official wwPDB local clash",
"Official covalent-geometry outlier"
) %in% ev$title), "Residue evidence should preserve independent warning sources.")
assert(ev$title[[1L]] %in% c(
"High-confidence prediction with unusual backbone geometry",
"Official wwPDB Ramachandran outlier","Twisted peptide bond"),
"High-severity residue evidence should sort before informational notes.")
quiet_row <- evidence_row
quiet_row$region <- "Favoured"; quiet_row$density <- 40
quiet_row$rama8000_region <- "Favored"; quiet_row$plddt <- 95
quiet_row$omega <- 179; quiet_row$omega_status <- "Trans"
quiet_row$wwpdb_rama <- "favored"; quiet_row$wwpdb_rotamer <- "favored"
quiet_row$wwpdb_clashes <- 0; quiet_row$wwpdb_bond_outliers <- 0
quiet_row$wwpdb_angle_outliers <- 0
assert(nrow(ram_residue_evidence(quiet_row))==0L,
"Unremarkable residues should not receive artificial warnings.")
missing_row <- quiet_row
missing_row$phi <- NA_real_
missing_ev <- ram_residue_evidence(missing_row)
assert("Backbone angles unavailable" %in% missing_ev$title,
"Missing torsions should be explained explicitly.")

message("Inspection, alignment and structural model unit tests passed")
7 changes: 7 additions & 0 deletions tests/ui-browser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,13 @@ const assert = require("node:assert/strict");
!document.querySelector(".ram-global-inspector").classList.contains("is-empty")
&& getComputedStyle(document.getElementById("prevReview")).display !== "none",
{timeout:10000});
const evidencePanel = await page.$eval("#selectedResidueInfo .ram-evidence-panel",
node => ({
summary: node.querySelector("summary")?.textContent?.trim() || "",
items: node.querySelectorAll(".ram-evidence-item").length
}));
assert.ok(evidencePanel.summary.startsWith("Why inspect this residue?"),
"Selected residues should explain why they merit inspection without a composite score.");
await page.screenshot({
path: "benchmarks/output/ui-preview/desktop-residue-zoom.png",
fullPage: true
Expand Down
Loading