immunogenetr since the paper: what changed from 1.0.1 to 1.5.0

Five releases in under a year brought clearer errors, a large performance rewrite, better handling of serologic typing, and GL String searches that understand expression suffixes.
immunogenetr
R
HLA
Author

Nicholas K. Brown

Published

September 10, 2026

The immunogenetr manuscript appeared in Human Immunology in December 2025 (Coskun and Brown, 2026), and it described the package as it stood at version 1.0.1. Since then there have been five releases, 1.1.0 through 1.5.0, with the current version published on CRAN on August 19, 2026. Anyone who learned the package from the paper will find that the workflows described there still run without modification. However, a good deal has happened underneath, and some of it changes what the functions accept and what they return, so the same code may now give different answers. This post summarizes those changes by theme rather than by version, so that readers can find the parts that matter to their own work. The version-by-version detail lives in the changelog on immunogenetr.org.

Clearer errors and a place to start

The first release after the manuscript, 1.1.0, was mostly about making the package easier to use correctly. Every exported function now validates its inputs before doing any work, so passing a data frame where a character vector was expected produces a message naming the problem instead of a cryptic failure several layers down. All error messages were standardized on the cli package, which is why they now look the same across the package. Moreover, the package gained a Getting Started vignette that walks through the main workflows (tabular data to GL Strings, splitting GL Strings by locus, mismatch and match calculation, and reading HML files), and a package-level help page, ?immunogenetr, that groups the exported functions by category. If you have only ever read the paper, the vignette is the natural next stop.

The same release fixed a real bug in read_HML, which had assumed a hard-coded XML namespace prefix. It now discovers the namespace from the file, so HML exports from different vendors read correctly.

Genotype-level match summaries

Version 1.2.0 added a scope argument to HLA_match_summary_HCT. The default, "locus", keeps the original behavior: for a bidirectional summary, the function takes the worse of the two directions at each locus and then sums. The new "genotype" scope instead computes the graft-versus-host and host-versus-graft totals separately and reports the better of the two. The two conventions differ only when one side is homozygous at a locus where the other is not, but in that situation they disagree, and different programs have different expectations.

library(immunogenetr)
library(dplyr)
library(stringr)

recipient <- "HLA-A*01:01+HLA-A*02:01^HLA-B*07:02+HLA-B*08:01^HLA-C*07:01+HLA-C*07:02^HLA-DRB1*03:01+HLA-DRB1*15:01"
donor     <- "HLA-A*01:01+HLA-A*01:01^HLA-B*07:02+HLA-B*08:01^HLA-C*07:01+HLA-C*07:02^HLA-DRB1*03:01+HLA-DRB1*15:01"

HLA_match_summary_HCT(recipient, donor, match_grade = "Xof8", direction = "bidirectional")
[1] 7
HLA_match_summary_HCT(recipient, donor, match_grade = "Xof8", direction = "bidirectional", scope = "genotype")
[1] 8

A performance rewrite

Version 1.3.0 was the largest release since the manuscript, and almost none of it is visible from the outside. The package was written on top of tidyr and dplyr pipelines, which are clear to read but expensive when called once per cell of a typing table. Profiling showed that HLA_prefix_remove, which many other functions call internally, expanded every GL String into an ambiguity table, edited it, and reassembled it. Replacing that round trip with a few direct regular expression passes made the function roughly 100 times faster, and every caller inherited the speedup. Most of the other heavy functions were reworked along the same lines.

Representative timings from the 1.3.0 release notes.
Function Workload Before After
HLA_columns_to_GLstring 10,000 rows ~272 s ~1.3 s
HLA_prefix_remove 100,000 inputs ~100× faster
HLA_mismatch_base 10,000 pairs ~11× faster, ~1000× less memory
GLstring_expand_longer varies 20 to 43× faster
HLA_truncate 1,000 alleles ~5× faster

In practical terms, converting a 10,000-row typing table to GL Strings went from a coffee break to about a second. Importantly, the rewrite was checked against a new round-trip test suite, which asserts that expanding a GL String to an ambiguity table and collapsing it again reproduces the original for a representative pool of strings. The same release trimmed the dependency list, dropping glue and magrittr and adding stringi as a direct import.

Serologic and molecular typing in the same column

Real typing tables are messy. A column labeled Cw often holds a mix of serologic values (7) and molecular ones (*07:01), depending on when and how the sample was typed. Version 1.4.0 fixed HLA_columns_to_GLstring producing the invalid locus name HLA-Cw*07:01 for molecular values in serologic-named columns, and restored the rule that a bare leading * marks a serologic value (*17 becomes HLA-Cw17). All spellings of a locus are now grouped together, so a C column and a Cw column never end up split across a ^ delimiter.

typing <- tibble(
  patient = c("P1", "P2"),
  A1  = c("2",      "24"),
  A2  = c("3",      "*01:01"),
  Cw1 = c("*07:01", "7"),
  Cw2 = c("*17:01", "*17")
)

HLA_columns_to_GLstring(typing, HLA_typing_columns = c("A1", "A2", "Cw1", "Cw2"))
[1] "HLA-A2+HLA-A3^HLA-C*07:01+HLA-C*17:01" "HLA-A24+HLA-A*01:01^HLA-Cw7+HLA-Cw17" 

The same release added a nomenclature argument for callers who already know whether a column is molecular or serologic and would rather say so than rely on auto-detection. Of note, the relabeling is structural only: no allele-to-antigen translation is attempted, since that is a job for a different tool.

GL String searches that understand expression suffixes

The most consequential change for existing users is in GLstring_regex, and it arrived in 1.5.0. The function builds a regular expression that finds an allele in a GL String at any resolution, and it is the workhorse behind allele searches across a cohort. Before 1.5.0, the pattern could not match an allele carrying a WHO expression suffix (N, Q, L, S, C, or A) or a G or P group letter. A search for HLA-A*01:01 silently missed HLA-A*01:01:03N, and worse, a name truncated with HLA_truncate, which keeps the suffix, could not find the allele it came from.

The rules are now as follows. An allele supplied without a suffix matches regardless of expression status and also matches G and P group names. An allele supplied with a suffix matches only alleles carrying that same letter, at the same or higher resolution. Consequently, HLA-A*01:01N matches HLA-A*01:01:03N but never HLA-A*01:01:03 or HLA-A*01:01:03Q. The HLA- prefix is also optional now, on both sides of the search, because wild-caught GL Strings very often omit it.

gl <- "HLA-A*01:01:03N+HLA-A*24:02:01:01^HLA-B*08:01:01+HLA-B*44:02:01:01"

str_detect(gl, GLstring_regex("HLA-A*01:01N"))
[1] TRUE
str_detect(gl, GLstring_regex("HLA-A*01:01"))
[1] TRUE
str_detect(gl, GLstring_regex("A*24:02"))
[1] TRUE

A second change in the same function will be welcome to anyone who has used it with str_extract or str_replace. The text matched by the pattern is now the full allele name as it appears in the GL String, not the search string. Previously a lower-resolution pattern replaced only the leading fields, leaving dangling fields behind and corrupting the GL String.

str_extract(gl, GLstring_regex("HLA-B*44:02"))
[1] "HLA-B*44:02:01:01"
str_replace(gl, GLstring_regex("HLA-B*44:02"), "HLA-B*44:03")
[1] "HLA-A*01:01:03N+HLA-A*24:02:01:01^HLA-B*08:01:01+HLA-B*44:03"

Notably, code that relied on suffixed alleles being invisible to unsuffixed searches will see new matches after upgrading. For the cases where that older behavior was actually wanted, 1.5.0 also added GLstring_drop_non_expressed, which removes non-expressed alleles from a GL String before searching. The default drops N, S, and C alleles, which produce no cell-surface protein, and keeps L, Q, and A alleles, which do. Removal is structural: an ambiguity list narrows, a gene copy with nothing expressed collapses, an empty locus disappears along with its delimiter, and a GL String with nothing left becomes NA.

GLstring_drop_non_expressed(gl)
[1] "HLA-A*24:02:01:01^HLA-B*08:01:01+HLA-B*44:02:01:01"
GLstring_drop_non_expressed("HLA-A*01:01N+HLA-A*02:01Q")
[1] "HLA-A*02:01Q"

Missing typing and malformed GL Strings

Finally, the matching and mismatching family (HLA_mismatch_number, HLA_mismatch_logical, HLA_match_number, HLA_match_summary_HCT, and their relatives) became more tolerant of the kind of input a real cohort produces. A pair with missing typing on either side used to abort the entire calculation with a misleading “missing these loci” error. It now returns NA for that pair and processes the rest of the cohort normally.

pairs <- tibble(
  recipient = c("HLA-A*01:01+HLA-A*02:01", NA, "HLA-A*03:01+HLA-A*11:01"),
  donor     = c("HLA-A*01:01+HLA-A*03:01", "HLA-A*01:01+HLA-A*02:01", "HLA-A*03:01+HLA-A*11:01")
)

pairs |>
  mutate(mismatches = HLA_mismatch_number(recipient, donor, loci = "HLA-A", direction = "bidirectional"))
# A tibble: 3 × 3
  recipient               donor                   mismatches
  <chr>                   <chr>                        <int>
1 HLA-A*01:01+HLA-A*02:01 HLA-A*01:01+HLA-A*03:01          1
2 <NA>                    HLA-A*01:01+HLA-A*02:01         NA
3 HLA-A*03:01+HLA-A*11:01 HLA-A*03:01+HLA-A*11:01          0

In the other direction, the same functions now reject GL Strings containing the ~ (haplotype) and ? (possible gene location) delimiters, which were previously accepted and silently mis-tokenized. A ?-joined pair of DRB3/4/5 alleles, for example, was being read as a single allele and produced wrong match counts. Ambiguous GL Strings containing | or / were already rejected; the error now names all four delimiters. Two smaller additions round out the release: a take_first_allele argument for HLA_validate and HLA_columns_to_GLstring, for callers who would rather have a GL String landing in a single-allele column raise an error than be quietly truncated, and ambiguity_table_to_GLstring returning character(0) for an empty table so that expand, filter, and reassemble pipelines compose without special cases.

Take-homes

  1. Upgrade with install.packages("immunogenetr"). The code in the paper still runs, and large tables now convert in seconds rather than minutes. Items 2 and 3 below are the places where results can change.
  2. If you search GL Strings for alleles, re-check any logic that assumed null alleles would not match. Use GLstring_drop_non_expressed first if that assumption was deliberate.
  3. If you use str_extract or str_replace with GLstring_regex, the match is now the whole allele, which is almost certainly what you wanted.
  4. Cohorts with missing typing no longer need to be filtered before matching; the NA values propagate.

The full changelog is on immunogenetr.org, and each release has notes on GitHub. Bug reports and feature requests are welcome in the issue tracker; most of the changes above started as one.