# Dose-volume histograms Cumulative and differential DVHs of any structures against any loaded dose objects, with the metrics table, protocol constraint checking and CSV export. ## Opening it *Tools ▶ 📊 Dose-volume histograms…*, or tick structures in the data tree, right-click and choose **📊 Plot … on a DVH**: the window opens with them already picked and the viewport's dose object already selected. Like every tool window it goes through [the detach mechanism](architecture.md#tool-windows): ⧉ puts it on its own top-level window - for a DVH the normal way to work, curves on one screen, images on the other. ## What it computes For every (structure, dose object) pair: * the **cumulative** histogram - volume receiving at least each dose, the curve every constraint is read off; * the **differential** histogram - volume per dose bin, where a cold spot inside a target shows up as a second hump; * the statistics: minimum, mean and maximum; * whatever metrics the table is asked for. Structures may come from any open workspace and either kind - RT structure or segmentation - and any number of dose objects may be overlaid. Structures keep their own colour and the dose object picks the line style, so two plans over the same organs read as one colour in two dashes. ## Four things it is careful about **Where it samples.** The structure's own lattice, not the dose grid: a CT mask is 1 mm and a dose grid 2-3 mm, so walking the mask and interpolating the dose gives a curve at the structure's resolution. The walk is affine, the dose-grid coordinates stepped rather than recomputed per voxel - three adds instead of three dot products. **What falls outside the dose grid.** Counted, kept, and said out loud: those voxels enter the histogram at zero dose - the honest reading of "not irradiated by *this* dose object" - and, since a DVH silently computed over 60 % of a structure looks cold rather than truncated, a warning line names every structure that extends outside the grid and by how much. **Curves older than the geometry.** A structure edited after the curves were computed makes every number in the window describe something that is no longer on the screen. The window says so, in a line above the table, and leaves the numbers alone: recomputing behind the user's back would replace a comparison they were in the middle of making. **Statistics from the samples, not the bins.** Minimum, mean and maximum are accumulated during the walk; reading them off a binned histogram costs half a bin width of accuracy for nothing. **Interpolation inside a bin - except the lowest.** D95 % is almost never exactly at a bin edge, so the cumulative curve is interpolated linearly between edges. The lowest bin holds exact zeros, so a reading inside it returns 0 rather than a few hundredths of a Gy that would look like a real dose. The histogram uses 2000 bins over the dose maximum - 3 cGy on a 60 Gy plan, finer than any constraint is quoted to. ## The axes Both are switchable, independently: | | | |---|---| | **Dose** | Gy, or per cent of a reference dose | | **Volume** | per cent of each structure, or cm³ | The reference defaults to the prescription of the first plan that declares one, and the window says which plan that was; ↺ restores it after you have typed something else. Dose-valued table columns follow the same switch, so `Dmean` reads in per cent when the axis does. ## The metrics table One row per curve. It starts with volume, minimum, mean, maximum, D95 % and D2 %, and takes any column you type. The volume is voxels-based - the structure's voxels on the image lattice, those outside the dose grid included ([volumes.md](volumes.md#5-other-volumes)): | You type | You get | |---|---| | `Dmean`, `Dmax`, `Dmin` | the statistics | | `D95%`, `D2%` | dose to at least that percentage of the structure | | `D2cc`, `D0.1cc` | dose to at least that absolute volume | | `V20`, `V20Gy` | percentage of the structure at or above that dose | | `V20cc` | the same as an absolute volume | ## The Dose estimation module The same numbers without the plot: *Modules ▶ Dose estimation* (right panel, F10) is one table, one row per **ticked** structure of the active set of its workspace, against one dose. Volume, Dmean, Dmin and Dmax are there to start with, then D95 % and D2 %; the box and its `+` add any column of the table above, every column has a `✖` to go, and *Reset columns* brings the six back. A name that does not read as a metric, or asks for more than 100 % of the volume, is refused under the box rather than computed. The column headings carry the units (`Volume [cm³]`, `Dmean [Gy]`). *Export CSV* writes the table as it stands, with the dose-grid coverage of every structure as a last column. The `✖` at the end of a structure's name leaves that structure out of the table while it stays ticked in the views; an *Excluded* line under the table lists them and brings any back. **Dynamic** (the switch in the section's title line) turns the table into a log of the Structure editor's moves: switching it on records the table as it is (step 0), and every finished Move / Rotate / Scale / Move to crosshair / hand drag of a structure adds the rows as they then stand - three moves of four ticked structures give sixteen rows. Each row carries the step, which structure was moved last, what the moves were relative to (*drawn axis*, *image axes*, *hand*, *crosshair*) and their sum since the structure's origin: shift in millimetres (`axis +10.0`, `x +2.0 y +0.0 z +1.0`), rotation in degrees and scale in per cent. *Back* takes the last move out of the sum, *Reset* clears it. *Clear log* starts again from the current table; *Export CSV* writes the whole log with those columns in front. The log is not only a table of numbers: each step also records where the structures stood when those numbers were computed, so a **Step** transport above the table can show any of them again. ⏮ and ⏭ walk the log, the scrubber jumps to a step, and **▶** plays it - the structures move in the views and in the 3D window as they were moved, and the step being shown is marked down the left of the table, so what one reads matches what one sees. *Now* returns to the last step, where the structures actually stand; the speed and what happens at the end of the log (loop, bounce, once) are in the [Playback](viewer.md#playing-through-slices-and-phases) module. The geometry is stored sparsely - the first step carries every structure, each later one only what moved - and the step counter's tooltip says how much the log is holding; *Clear log* gives it back. Playing the log replays the *logged* steps, so an edit that never produced an entry (a brush stroke, say) is not part of it. The table follows what it depends on by itself: tick or untick a structure, move or redraw one in the Structure editor, pick another dose or another column, and it is recomputed in the background and replaced when ready. When the workspace carries both physical and effective (RBE-weighted, Dose Type `EFFECTIVE`) doses, a *Physical / Effective* switch above the dose picker chooses which kind the picker lists; with only one kind there is nothing to switch and it is not shown. Points of interest have no volume and are left out. ## Constraint checking A protocol is a plain text file, one constraint per line - human-editable on purpose - that is how a department keeps them: ``` # head and neck, 30 fractions PTV* D95% >= 57 PTV* Dmax <= 63 Cord Dmax <= 45 "Parotid L" Dmean <= 26 Lung* V20Gy <= 30 ``` The structure name is matched case-insensitively; a leading or trailing `*` matches loosely, so `PTV*` catches `PTV_5400`; names with spaces are quoted. The header line of the collapsing section says how many constraints are met, and each row shows ✔ or ✖ against the value. A constraint that matches **no** structure is reported with a dash and does **not** pass - a line that quietly evaluates to "fine" because the structure was never contoured is the worst failure mode a checker can have. ## Export **Export curves…** writes the cumulative curves as CSV: one dose column, then one volume column per structure, following the volume axis currently shown. Curves against different dose objects may differ in bin width, so they are resampled onto one dose axis at the finest of them rather than assumed to share one. **Export table…** writes the metrics table as it stands. ## Verification `src/dvh.rs`'s own tests check the arithmetic on grids built in the test: a uniform dose gives a step and exact statistics; a linear ramp gives a DVH linear to within 2 %, read in both directions; voxels outside the dose grid are counted, reported, and drag D60 % to zero without disturbing the statistics of what *was* irradiated; metric names round-trip through `label()` and `parse()`; a protocol survives a write and re-read, quoted name included; and the CSV puts curves with different bin widths on one axis. `tests/dvh.rs` goes through the whole path: the synthetic RT study is written as DICOM, read back through the loader, its contours rasterized, and the histogram taken against the RTDOSE as parsed. The phantom's dose is an analytic Gaussian centred on a spherical target, so the target's DVH is known in closed form - the volume above dose `D` is the ball of radius `σ·√(2·ln(peak/D))` - and the test compares against that formula, not a previous run, at six doses and three volume levels, to within 5 % of volume and 2 Gy of dose. It also checks that a cumulative curve never rises, that a structure contained in another is nowhere hotter in absolute volume, and that a protocol reads the phantom the way a physicist would. As with everything in this viewer: research and QA use - not a medical device, not for clinical decision-making.