---
title: "Finding your way around linf"
output:
rmarkdown::html_vignette:
toc: true
math_method: mathml
vignette: >
%\VignetteIndexEntry{Finding your way around linf}
%\VignetteEngine{knitr::rmarkdown}
%\VignetteEncoding{UTF-8}
---
```{r setup, include=FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
library(linf)
# Keep table rendering separate from the runnable analysis code.
knitr::opts_chunk$set(render = function(x, options) {
rendered <- knitr::knit_print(x, options = options)
if (inherits(x, "knitr_kable")) {
knitr::asis_output(paste0(
'
\n\n',
rendered, '\n\n
\n'
))
} else rendered
})
```
Start with the result you need: a dominant feature for each sample, a supported
grouping of samples, a deeper hierarchy, or assignments to a reference you have
already fitted. These are different stopping points. You do not need to run
the full pipeline to use L-infinity normalization.
The [example datasets guide](example-datasets.html) catalogs the data separately.
For longer analyses, see the [normalization and gut tutorial](linf-intro.html)
and [vaginal microbiome tutorial](linf-vaginal.html).
## Choose a task
Tables scroll horizontally on narrow screens. Focus a table and use the arrow
keys, or swipe within it, to read every column without breaking function or dataset names.
| Your task | Entry point | When to stop |
|---|---|---|
| [Remove low-depth samples and low-prevalence features from counts](#filtering-scaling-and-grouping-answer-different-questions) | `filter.asv()` | You need filtered counts or unit-sum relative abundances. |
| [Express abundance relative to each sample's largest feature](#a-small-workflow-with-stopping-points) | `normalize.linf()` | You need scaled profiles, without grouping samples. |
| [Name the largest feature in each sample](#a-small-workflow-with-stopping-points) | `linf.dominant.features()` | You need individual assignments, without a support threshold. |
| [Retain supported dominance groups](#what-a-dcst-hierarchy-stores) | `linf.csts()` | A depth-1 dCST partition answers your question. |
| [Subdivide selected groups by remaining features](#refinement-choices) | `refine.linf.csts()` | The chosen depth is sufficient; deeper is not automatically better. |
| [Inspect stored low-support policies or select representatives](#landmarks-are-selected-observations) | `dcst.view()`, `summary()`, `linf.landmarks()` | You need a descriptive summary of the fitted samples. |
| [Assign new samples to an existing hierarchy](#transfer-to-a-frozen-reference) | `transfer.dcsts()` | You need frozen-reference assignments, without learning new states. |
| [Prepare coordinate charts or readable names](#embeddings-and-display-labels) | `linf.hypercube.embedding()`, `linf.feature.labels()` | You need a representation for display or further analysis. |
## A small workflow, with stopping points
These six samples have stable IDs that differ from the display names. Two
depth-1 groups have support at least two. One parent can be split into two
supported children.
```{r first-workflow}
X <- rbind(
s1 = c(10, 8, 1), s2 = c(9, 7, 2),
s3 = c(10, 1, 8), s4 = c(9, 2, 7),
s5 = c(1, 10, 2), s6 = c(2, 9, 1)
)
colnames(X) <- c("asv_1", "asv_2", "asv_3")
labels <- c("Taxon A", "Taxon B", "Taxon C")
M <- normalize.linf(X)
dominant <- linf.dominant.features(M, feature.labels = labels)
dominant$id
fit1 <- linf.csts(M, feature.labels = labels, n0 = 2,
low.freq.policy = "pure")
fit2 <- refine.linf.csts(M, fit1, n0 = 2, refinement.factor = 2,
low.freq.policy = "pure", verbose = FALSE)
accounting <- summary(fit2)
fields <- c("depth", "n.lineages", "input.samples",
"assigned.samples", "unassigned.samples", "rare.samples")
knitr::kable(accounting[, fields],
col.names = c("Depth", "Groups", "Input", "Assigned", "Unassigned", "Rare"))
```
Stop at `M` for normalized profiles or at `dominant` for sample-level maximum
features. `fit1` adds the support threshold; `fit2` adds a level of subdivision.
The two registered methods work through these objects: `print(fit2)` displays
counts at each active depth, while `summary(fit2)` returns a data frame of
lineage counts and sizes. The table accounts for all six samples at both depths: two groups become
three after refinement. Summaries include separate input, assigned, unassigned
and rare sample counts. Size statistics include rare groups and are `NA` when
no samples are assigned, without warnings. For compatibility, `total.samples`
continues to mean assigned samples; use `input.samples` for the full denominator.
## Function catalog
On narrow screens, focus a table and scroll horizontally to read all columns.
Function names remain intact for copying.
This guide describes the 0.3.1 interface: **13 explicit function exports,
two registered S3 methods, and five bundled datasets**. S3 methods are selected
by R when you call `print()` or `summary()` on a fitted object. Neither method
is also an explicit function export, so there is no overlap in these counts.
Each explicit export appears once in the catalog below. Use `help("function.name",
package = "linf")` for complete arguments and examples, for example
`help("normalize.linf", package = "linf")`.
### Prepare and assign
| Function | Purpose | Expected input | Principal result |
|---|---|---|---|
| `filter.asv()` | Filter library size and feature prevalence. | Dense samples-by-features counts; numeric data frames also accepted. | List with `counts`, unit-sum `rel`, kept row/column indices, prevalence and thresholds. |
| `normalize.linf()` | Divide rows by their maximum above `tol`. | Finite, nonnegative dense or sparse matrix. | Same-sized matrix; zero and below-tolerance rows remain unchanged. |
| `linf.dominant.features()` | Assign each sample's maximum feature. | Finite, nonnegative matrix and optional aligned IDs/labels. | Indices, IDs, labels and observed/full feature levels; optional row maxima. |
### Fit and inspect a hierarchy
| Function | Purpose | Expected input | Principal result |
|---|---|---|---|
| `linf.csts()` | Retain dominance sample sets with support at least `n0`. | Samples-by-features matrix, usually L-infinity normalized. | Class `linf.csts`, with active, pure and absorb hierarchies. |
| `linf.udcsts()` | Pool top-feature orders into unordered sets before applying support. | Nonnegative samples-by-features matrix and a level. | Pure set IDs and frequencies, assignments, and a sample mask; pure, absorb or filter policy. |
| `linf.udcst.graph()` | Construct pair-state graphs supported by observed triples. | Matrix or matching pure pair/triple fits; coverage or support selection. | Nodes, unit edges, full incidence, witness similarities, faces, sample membership and components. |
| `refine.linf.csts()` | Add one hierarchy level within selected active leaf lineages. | Fitted object and its matrix in the original row/column order. | Updated `linf.csts`; previous levels and feature metadata retained. |
| `dcst.view()` | Activate a stored policy without recomputing it. | Fitted object; `view = "pure"` or `"absorb"`. | Copy with active assignments switched and attached landmarks cleared. |
| `linf.landmarks()` | Select observed samples by the lineage's leaf-feature value. | Fitted object, aligned matrix, depth and view. | `lineages` metadata and `landmarks` selection table, with row indices/names. |
| `linf.dcst.landmark.pipeline()` | Normalize, fit depth 1, refine once and select landmarks. | Nonnegative matrix, feature metadata and stage-specific controls. | Normalized matrix, two fitted objects, two landmark objects and parameters. |
### Reuse and display
| Function | Purpose | Expected input | Principal result |
|---|---|---|---|
| `transfer.dcsts()` | Walk new samples through a frozen fitted hierarchy. | Query matrix, reference fit, feature-matching and view controls. | Label/ID assignment matrices, feature matches and stopping diagnostics; unassignable depths are `NA`. |
| `linf.hypercube.embedding()` | Construct a zero-aware chart relative to one component. | Nonnegative matrix with at least two features and a reference index, ID or label. | Dense matrix with one fewer column; attributes record the reference and scaling. |
| `linf.feature.labels()` | Format taxonomy for display while keeping separate stable IDs. | Equal-length feature-ID and taxonomy vectors; optional aliases/abbreviations. | Unique display labels. |
## Matrix and identity conventions
Rows are **samples** and columns are **features**. Give samples unique row names
and features unique, nonmissing stable IDs. Keep taxonomy or other display text
in a separate vector. `feature.ids` and `feature.labels` are positional: each
must have length `ncol(X)` and describe columns in exactly that order. Metadata
vector names do not perform a join. Defaults use column names as IDs, or
`V1`, `V2`, ... for unnamed columns, and use IDs as display labels. Fitting
disambiguates duplicates with `make.unique(..., sep = "_")`; supplying unique
IDs from the start avoids an order-dependent mapping.
For refinement and landmarks, reuse the fitted matrix's **row and column
order**. Both dimensions are checked. New fits save `input.dimnames` separately
from custom feature IDs; if an axis is named in both the fit and the supplied
matrix, its names/order must match. The error identifies the axis to align.
There is no automatic reordering. Unnamed axes and older fits without saved
input names retain the positional contract; prefer unique names because
permutations within duplicated names cannot be detected.
Align sample metadata with `match(rownames(X), metadata$sample_id)` and verify
all matches before analysis. After filtering, use `kept.sample.idx` and
`kept.feature.idx` to subset metadata. See the companion's
[alignment example](example-datasets.html#align-data-and-metadata).
Core matrix functions accept ordinary dense numeric matrices and `Matrix`
sparse matrices. `backend = "auto"` normally preserves sparse input;
refinement and landmarks inherit the fitted backend when their backend argument
is omitted. Explicit `"dense"` and `"sparse"` choose a conversion path.
Sparse output is numeric compressed-column storage. Embeddings always return
dense coordinates. `filter.asv()` is a separate dense count-filtering interface;
it does not accept a sparse `Matrix` object directly.
### Unordered dominance sets
`linf.udcsts()` groups rows by their highest-abundance feature sets, ignoring
order. At level 1, its pure membership agrees with dominant-feature grouping.
At level 2, `(A, B)` and `(B, A)` pool **before** applying `n0`. Calls at different
levels are separate partitions, not necessarily a refinement hierarchy.
```{r unordered-states}
U <- rbind(c(A = 9, B = 5, C = 1), c(A = 8, B = 4, C = 1),
c(A = 4, B = 9, C = 1), c(A = 3, B = 8, C = 1),
c(A = 1, B = 3, C = 9))
unordered <- linf.udcsts(U, level = 2, n0 = 3, low.freq.policy = "filter")
unordered$frequency.table
U[unordered$keep, , drop = FALSE]
unordered$feature.labels[unordered$feature.sets[1, ]]
```
Four rows belong to the pure set `{A, B}` and remain; the single `{B, C}` row
is filtered. `pure.id` always retains every row's original set. `ID` uses
original column indices, `freq` counts rows before filtering, and `cumm.pct`
is the percentage of **all input rows** in states with at least the current
row's frequency. Equal-size states have equal cumulative percentages.
The `"pure"` policy marks small states as rare while keeping their rows;
`"filter"` excludes them through `keep` without reassignment. Both use missing
active assignments for those rows and retain their original `pure.id`.
`"absorb"` assigns a rare row to the retained set with greatest combined
feature abundance. This set-based rule differs from ordered parent-local
dCST absorption. Numerical tuple order resolves tied candidate scores.
Absorption errors when no state survives; filtering may legitimately keep no
rows. Subset all metadata with the same mask.
This constructor always uses original-column ties, rejects all-zero rows,
and reports `positive.features` so that sets containing zero-valued ranks can
be identified. It accepts counts or proportions without normalization.
The output is not a `linf.csts` hierarchy and is not an input for its refinement,
landmark, policy-view or frozen-transfer functions. See `?linf.udcsts` for the
full result fields and sparse-input behavior.
### Filtering, scaling and grouping answer different questions
Count filtering removes samples/features using library size and prevalence.
`filter.asv()` applies the library-size threshold first, computes prevalence
among those retained samples, then drops samples left with no counts. Its `rel`
matrix sums to one across the retained features in each row.
For a nonnegative row, L-infinity normalization instead divides by its largest
entry when that maximum exceeds `tol`:
\[
M_{ij} = X_{ij}/\max_k X_{ik}\quad\text{when }\max_k X_{ik}>\mathrm{tol}.
\]
It is neither unit-sum normalization nor a log-ratio transform. No pseudocount
is added. At the default `tol = 0`, a positive row has maximum one; an all-zero
row remains zero. With positive tolerance, a row whose maximum is **at or
below** the tolerance is left unchanged, including its small nonzero entries.
Such a row still has a dominant feature. Only exact-zero rows receive `NA`
from `linf.dominant.features()`.
The core matrix functions reject missing (`NA`, `NaN`), infinite and negative
entries and empty matrices; they do not impute missing values. `filter.asv()` also validates finite nonnegative counts and threshold ranges.
Fractional count-like values are accepted. Empty filtering results keep every
documented field and indices that correspond to the returned matrices.
With `min.lib = 0`, zero-total rows count toward prevalence but have no present
features under a relative threshold; they are removed before returning `rel`.
Dominant-feature assignment and refinement depend on within-row rankings.
Positive row scaling preserves those rankings. L-infinity scaling is useful
for interpreting feature values but is not required to discover the maximum.
The retained feature set, column order for ties, support thresholds and policy
still affect the resulting grouping.
### Ties and random seeds
The default `tie.method = "first"` selects the first exactly maximal column.
`"random"` samples among exact ties using R's current random-number state;
these functions have no private seed and do not restore the caller's state.
Set `set.seed()` immediately before a reproducible random-tie call. Dense and
sparse paths use the same column ordering for dominance and positive absorption
ties. `"error"` stops at a positive tie. Zero rows do not trigger a dominance tie.
Refinement has **no `tie.method` argument**: its child fits use `"first"`, even
if the parent was fitted with random ties. The pipeline's tie argument controls
depth-1 fitting and landmark selection, not refinement. Landmark `"first"`
selects the first tied sample in matrix row order. Frozen transfer has its own
default, `"support"`, described below.
## What a dCST hierarchy stores
A **dominant feature** is one sample's largest feature. Samples sharing it form
a provisional **dominance sample set**. A set retained at support `n0` becomes
a named depth-1 dCST. A **dominance lineage** records successive feature choices
as a group is refined. **Depth** counts stored hierarchy levels; it need not
equal the number of features in every terminal lineage.
Under the **pure policy**, low-support samples enter a synthetic rare bucket
(default `RARE_DOMINANT`). Under the **absorb policy**, they are reassigned to
the retained feature with greatest abundance in that sample. If every retained
feature is zero, fitting falls back to the state with greatest reference
support, then lexical label order, regardless of tie method. Exact-zero rows
also follow that fitting fallback. With no retained states, pure assignments
are all rare and absorb assignments are all `NA`.
Both representations are stored. Unsuffixed fields are the **active** view:
`lineage.id`/`lineage.label` hold current leaves;
`lineage.ids`/`lineage.labels` hold a list of levels. Their `.pure` and `.absorb`
counterparts preserve stored policies. `dcst.view(fit2, "absorb")` changes which
stored hierarchy is active without fitting again, and clears attached landmarks
so that summaries from a different view are not reused accidentally.
`size.table` describes provisional depth-1 support, not current leaf sizes.
### Refinement choices
With `lineages.to.refine = NULL`, automatic selection uses **active** leaf
support at least `refinement.factor * n0` (defaults: `2 * 50`). An explicit
character vector selects existing active leaf **IDs**, bypassing that parent
size rule; it does not bypass the child support threshold. Unknown IDs and
the top-level synthetic rare ID are rejected. For example:
```{r explicit-refinement}
explicit <- refine.linf.csts(
M, fit1, lineages.to.refine = "asv_1", n0 = 2,
low.freq.policy = "pure", verbose = FALSE
)
stopifnot(identical(explicit$lineage.ids, fit2$lineage.ids))
```
Within each selected active group, refinement removes features already used
on its fitted path and fits the remaining columns. It appends a stored level
on every call. Parents with no retained child, no remaining feature, or no
selection keep their labels at that level. Repeated calls extend the hierarchy;
they do not guarantee an additional feature for every sample. Parent-prefixed
rare buckets at deeper levels are stored synthetic categories, not features.
Selection and child fitting use the active membership. The saved policy views
are representations of that fitted history, not two independently refitted
analyses. Choose the active policy before refining and keep `low.freq.policy`,
`rare.label` and `sep` consistent across calls. Fitted objects store explicit feature paths and node keys, including IDs
containing `__`. A real `A__B` feature and the refined path `A` then `B` are
different nodes. When their readable strings collide, returned IDs/labels
receive a `[node ...]` suffix; select the returned `lineage.id` when refining.
A real feature named `RARE_DOMINANT` is distinct from a synthetic rare node.
Synthetic or undefined parents stay terminal in their own policy view.
Unambiguous older fits are upgraded on use; ambiguous legacy fits must be
rebuilt from their matrices. Never split displayed strings to recover features.
### Landmarks are selected observations
```{r landmark-example}
landmarks <- linf.landmarks(M, fit2, view = "absorb",
landmark.types = c("endpoint.min", "mean.rep"))
knitr::kable(landmarks$landmarks[, c(
"lineage.label", "landmark.type", "point.name", "observed.value",
"target.value"
) ], digits = 3)
# Retrieve the complete observed profiles, if needed:
profiles <- M[landmarks$landmarks$point.index, , drop = FALSE]
```
Landmarks target the leaf feature of each lineage at the requested depth.
`endpoint.max` and `endpoint.min` select rows with the largest and smallest
target-feature value. `mean.rep` and `median.rep` select the **observed row
closest to the mean or median of that one feature**. None constructs a mean
profile or a centroid across features. Several methods may choose the same row.
Values are on the scale of the supplied matrix. With `M` above they measure
abundance relative to each sample's row maximum, not a fraction of total
abundance. At depth 1 under the pure policy, normalized dominant values are
usually all one, so endpoint methods can reduce to tied selections. Under
absorption the target need not be the sample's true maximum. These summaries
alone do not establish whole-community typicality or biological distinctness.
Inspect `landmarks$lineages$landmarks.computable`: rare categories have no unique
target and are skipped. Target IDs come from explicit node paths, so separators and colliding
readable paths cannot change the feature selected. Terminal lineages retain their last feature at later stored depths.
Keep stable IDs and display labels separate; neither display labels nor
concatenated lineage strings should be split to infer feature identities.
### The optional pipeline
```{r pipeline}
out <- linf.dcst.landmark.pipeline(
X, feature.labels = labels, n0.depth1 = 2, n0.depth2 = 2,
refinement.factor = 2, verbose = FALSE
)
names(out)
stopifnot(identical(out$dcst.depth2$lineage.ids, fit2$lineage.ids))
```
Defaults are `n0.depth1 = 50`, `n0.depth2 = 25`, `refinement.factor = 2`,
`low.freq.policy = "pure"`, `landmark.view = "absorb"`, `tie.method = "first"`,
`sep = "__"`, and `backend = "auto"`. All four landmark types are requested
at both depths. The wrapper normalizes with default tolerance zero; it does
not filter counts. It returns `linf.rel`, `dcst.depth1`, `dcst.depth2`,
`landmarks.depth1`, `landmarks.depth2` and `params`. Use explicit stages when
you want another tolerance, selected lineages, more depths, or fewer results.
## Transfer to a frozen reference
`transfer.dcsts()` assigns new rows to realized reference lineages, without
changing their features, support counts or structure. It defaults to the
**absorb** view, matching by **feature IDs**, and all fitted depths. Columns
may be reordered; extra features are ignored and missing reference features
are filled with zero. Pass query `feature.ids` in query-column order when column
names are not those IDs. Label matching is optional. Unnamed matrices use
synthetic `V1`, `V2`, ... IDs, which require consistent positional meanings.
Matching keys must be unique, nonmissing and nonempty. Duplicate keys are
rejected; choose an explicit biological aggregation or disambiguation upstream.
Repeated display labels remain allowed when matching by unique feature IDs.
At each depth, the query chooses among realized children using the abundance
of the newly added feature. The default `tie.method = "support"` resolves
positive ties using reference support, then lexical order. `"first"` uses
frozen child order, not query-column order; `"random"` uses the current R seed;
`"error"` stops on ties. A terminal label can recur only where it is actually
stored, scored using its last feature. With no candidate or no positive
candidate feature, this depth and subsequent depths are `NA`. There is **no
fitting-style zero-evidence fallback**, even under `view = "absorb"`.
`carry.forward.terminal.depths` is retained for call compatibility; neither
setting permits assignments outside the fitted hierarchy.
The original assignment matrices contain display labels even when matching by IDs.
Their `assignment.ids` and `all.depths.ids` counterparts contain lineage IDs for
joins using identifiers that do not encode display labels. The existing
lexical display-label tie rule still applies: relabeling and refitting can
change tied assignments even though the identifiers themselves are unchanged.
`assignment` contains requested depths and `all.depths` contains all fitted
depths. A pure rare bucket has no feature to score; it is not a learned query
class. Reference fitting and frozen transfer thus need not reproduce every
training assignment, especially for rare, zero or tied rows. The companion
includes a [separate reference/query example](example-datasets.html#reference-and-query).
### Inspect a transfer decision and saved refinement settings
```{r transfer-records}
query <- M[c("s1", "s5"), c("asv_2", "asv_1"), drop = FALSE]
transferred <- transfer.dcsts(query, fit2, view = "pure")
transferred$assignment.ids
knitr::kable(transferred$feature.match)
knitr::kable(transferred$diagnostics[, c("sample.id", "assigned.depth", "reason")])
fit2$history[[2]][c("n0", "selection.mode", "selected.lineage.ids", "tie.method")]
```
The feature-match table shows that `asv_3` is absent, even if both queries can
complete this particular walk through other features. For an unassigned query,
`missing_candidate_features` means every real candidate at the stopping depth
was absent; `no_positive_candidate_values` means some candidates were present
but none was positive. Check the present/missing candidate counts if some were
absent. `no_candidates` and `synthetic_only` describe the frozen reference.
These diagnostics describe every fitted depth even when fewer are requested.
They explain the rule applied, not confidence or predictive accuracy.
The `history` list records the support threshold, selection, source/returned
policy and tie handling for every new depth. `refined.node.ids` excludes selected
parents that stayed terminal because no child could be retained. Switching views
preserves this history. The top-level `n0` remains the depth-1 setting; use the
corresponding history entry for a later depth. Older fits can still be used;
refining one marks previously unsaved history entries `available = FALSE`.
## Embeddings and display labels
`linf.hypercube.embedding(X, reference)` needs a finite nonnegative matrix and
one unambiguous reference component. For positive reference entries it forms
ratios to that component, then maps their direction radially into the unit cube.
For a zero reference it uses the L-infinity boundary extension; an all-zero
sample maps to zero by convention. Its tolerance controls which reference
entries and norms are treated as zero, unlike the leave-unchanged tolerance
rule of `normalize.linf()`.
With *p* input features it returns a dense samples-by-(*p* − 1) matrix, plus
attributes recording reference identity, other feature IDs, `lambda` and
finite/zero-reference row counts. By default `lambda` is scaled so the radial
map reaches 0.95 at the 0.95 quantile of positive reference-ratio norms
(fallback scale one if none exceed tolerance). Logarithmic calculations include
norms outside the ordinary numeric range without overflow. For comparable charts
of reference and query data, reuse the reference choice and
`log.lambda = attr(reference.embedding, "log.lambda")`. The ordinary `lambda`
attribute is `NA` if that scale is not representable as a positive finite number;
the logarithmic value remains usable. `-expm1(-t)` preserves small positive radii.
The result is dense by implementation choice; non-reference zeros stay zero.
This is a coordinate representation, not a clustering or predictive fit.
Distances and apparent separation depend on the feature subset, reference and
scaling; coordinates alone do not demonstrate biological groups or validated
prediction. The companion shows a static chart. The optional
[interactive embedding article](https://pgajer.github.io/linf/articles/valencia-hypercube-embedding.html)
is website-only and is not an installed vignette.
## Resource planning
Transfer and landmark calculations reuse prepared candidate positions and
memberships. Embedding still returns a dense matrix even for sparse input;
its numeric output alone occupies about `8 * nrow(X) * (ncol(X) - 1)` bytes,
before temporary storage. The installed performance note gives measured
workloads, timings and memory limits of the comparison:
```{r performance-location, eval=FALSE}
cat(readLines(system.file("PERFORMANCE.md", package = "linf", mustWork = TRUE)),
sep = "\n")
```
## Finding help and migrating older code
```{r help-navigation, eval=FALSE}
help(package = "linf")
help("linf-package", package = "linf")
browseVignettes("linf")
vignette("example-datasets", package = "linf")
```
Use `lineage.id`/`lineage.label` for leaf assignments and
`lineage.ids`/`lineage.labels` for levels. The 0.2 release introduced these
explicit policy hierarchies; rebuild pre-0.2 flat objects from their matrices.
Use `linf.dominant.features()` for depth-1 assignment and `dcst.view()` for
policy selection. Since 0.3, repeat `refine.linf.csts()` for additional depths,
use the explicit filtering/normalization/fitting stages, and format summary
tables with ordinary reporting tools. Removed cell-based aliases and former
convenience functions are not part of the current API.
The organization follows the short purpose descriptions and help cross-references
in [Hmisc's documentation](https://hbiostat.org/r/hmisc/) and
[package overview](https://CRAN.R-project.org/package=Hmisc),
adapted to these few workflows. Maintainers can run `make audit-guides` to check
the catalog against `NAMESPACE`, registered methods, help aliases and bundled
data; vignette builds execute the examples independently.
## Triple-supported pair-state graphs
`linf.udcst.graph()` constructs pure level-two state graphs. Both endpoint pair
configurations must occur within their common leading triple. Counts always use
the full input sample set, including pair states excluded by the coverage rule.
```{r triple-graph}
G <- rbind(c(A=6, B=3, C=1), c(A=6, B=1, C=3), c(A=1, B=6, C=3))
graph <- linf.udcst.graph(G, coverage=60, min.support=1)
graph$nodes
graph$edges[, c("pair.a", "pair.b", "triple", "min.support", "length")]
graph$faces
```
Coverage retains tied frequency groups together, so actual coverage can exceed
the target. `adjacency="shared_feature"` provides a comparison without requiring
both observed configurations. Increasing `min.support` removes edges while
preserving isolated vertices. Each edge has unit path length; support and
Jaccard similarities are separate attributes. A filled face requires all three
pairs within a single witness triple. The function does not embed or repair the
graph; use an embedding package on within-component graph distances.