--- title: "Glyco-regex syntax, step by step" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Glyco-regex syntax, step by step} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") ``` ## What is a pattern? A **pattern** is a short description of the feature you want to find in a glycan. For example, `Gal-GlcNAc` asks for Gal directly attached to GlcNAc. A **regular expression**, often shortened to **regex**, adds symbols that express choices, repetition, and conditions. You do not need prior regex knowledge: this guide introduces those symbols one at a time. Glyco-regex searches the connected residues of a glycan, rather than letters in its written name. `Gal` means one galactose residue; it does not match the letters “Gal” within `GalNAc`. Different ways of writing the same branch arrangement do not turn it into a different structural feature. The [Get Started guide](glyrex.html) introduces the biological motivation and basic workflow. This guide explains the syntax in more detail, including optional advanced features. The examples use small illustrative glycans so that you can check each answer by eye. ```{r setup} library(glyrex) glycans <- c( lacnac = "Gal(b1-4)GlcNAc(b1-", sialylated = "Neu5Ac(a2-3)Gal(b1-4)GlcNAc(b1-", fucosylated = "Gal(b1-4)[Fuc(a1-3)]GlcNAc(b1-", both = "Neu5Ac(a2-3)Gal(b1-4)[Fuc(a1-3)]GlcNAc(b1-", glcnac = "GlcNAc(b1-" ) rex_detect(glycans, "Gal-GlcNAc") ``` The first argument is the collection of glycans; the second is the pattern in quotation marks. `TRUE` means a match was found, and `FALSE` means none was found. Each answer corresponds to the input with the same name. ## Keep structure notation and pattern notation separate The inputs above use **IUPAC-condensed notation** to describe structures. The pattern uses **glyco-regex notation** to describe a search: | Feature | Input structure notation | Pattern notation | |:--|:--|:--| | Gal attached by β1–4 to GlcNAc | `Gal(b1-4)GlcNAc` | `Galb4-GlcNAc` | | Fuc branch on the same GlcNAc | `Gal(b1-4)[Fuc(a1-3)]GlcNAc` | `Galb4-(Fuca3-)GlcNAc` | | Reducing-end α-GlcNAc | `GlcNAc(a1-` | `GlcNAca$` | The ending `(b1-` in our inputs records the reducing-end anomer and donor position; it does not add another residue. Do not copy IUPAC linkage parentheses into a pattern: `Gal(b1-4)GlcNAc` is not a valid search pattern. Patterns are case-sensitive. Write recognized residue names such as `Gal` and `GlcNAc`. Spaces in patterns are ignored, although the examples omit them. ## One residue, several choices, or any residue Start with a residue name, a family, or a choice: | Pattern | Meaning | |:--|:--| | `Gal` | One Gal residue | | `Hex` | One residue in the Hex family, including Gal, Glc, and Man | | `HexNAc` | One residue in the HexNAc family, including GlcNAc and GalNAc | | `Sia` | One Neu5Ac, Neu5Gc, or Kdn residue | | `.` | Any **one** residue | | [Gal|Man] | Either Gal or Man | | `[^Fuc]` | One residue other than Fuc | | [^Fuc|Xyl] | One residue that is neither Fuc nor Xyl | The vertical bar `|` means “or.” Square brackets collect the alternatives. Each negated alternative (inside `[^...]`) must describe one residue, not a multi-residue path. The dot does not mean “any number of residues.” ```{r residue-choices} residues <- c("Gal", "GalNAc", "Man", "Fuc") data.frame( residue = residues, gal = rex_detect(residues, "Gal"), hex = rex_detect(residues, "Hex"), gal_or_man = rex_detect(residues, "[Gal|Man]"), not_fuc = rex_detect(residues, "[^Fuc]") ) ``` Residue modifications matter. For example, `Gal6S` asks for sulfated Gal, whereas plain `Gal` does not match that modified residue: ```{r modifications} rex_detect("Gal6S(b1-4)GlcNAc", c("Gal6S", "Gal", ".")) ``` Use residue names supported by glyrepr. A slash can also express residue choices, as in `Gal/Man`; `[Gal|Man]` makes the alternatives more explicit. ## Build a connected path Read from left to right, **towards the reducing end**. A dash joins adjacent residues: - `Gal-GlcNAc`: Gal attaches directly to GlcNAc. - `Sia-Gal-GlcNAc`: a three-residue path in that order. - `Gal-.-GlcNAc`: exactly one intervening residue between Gal and GlcNAc. A path may occur inside a larger glycan. Extra residues and branches are allowed unless you add a condition that excludes them. Finding `Gal-GlcNAc` therefore does not establish that Gal is terminal or that GlcNAc has no other branches. Alternatives can describe whole paths too: ```{r path-choices} rex_detect( c("Gal(b1-4)GlcNAc", "Gal(b1-3)GalNAc", "Man(a1-3)Man"), "[Gal-GlcNAc|Gal-GalNAc]" ) ``` The choice belongs inside the square brackets. A bare `Gal|Man` is not supported. ## Specify a linkage only as precisely as you need A linkage suffix belongs to the residue **before** the dash. In `Galb4-GlcNAc`, `b` means beta and `4` is the acceptor position on GlcNAc. The donor position is inferred as 1, or 2 for Neu5Ac, Neu5Gc, Kdn, and `Sia`. Thus `Neu5Aca3-Gal` describes an α2–3 linkage. | Pattern | Linkage requirement | |:--|:--| | `Gal-GlcNAc` | No linkage constraint | | `Galb-GlcNAc` | Beta anomer; no position constraint | | `Galb4-GlcNAc` | β1–4 | | `Galb3/4-GlcNAc` | Beta, with acceptor position 3 or 4 | | `Gal?4-GlcNAc` | Either anomer, with donor 1 and acceptor 4 | | `Galb?-GlcNAc` | Beta, with donor 1 and any acceptor position | ```{r linkage-choices} linkages <- c( beta4 = "Gal(b1-4)GlcNAc", beta3 = "Gal(b1-3)GlcNAc", alpha4 = "Gal(a1-4)GlcNAc", unknown_position = "Gal(b1-?)GlcNAc" ) rex_detect(linkages, "Galb4-GlcNAc") rex_detect(linkages, "Galb3/4-GlcNAc") ``` **The glycan must be at least as specific as the pattern.** Unknown fields in the pattern act as wildcards; unknown input fields cannot satisfy a specified value. The unknown-position example therefore does not match `Galb4-GlcNAc`. Every possible input position must be allowed by the pattern: input `4` matches pattern `3/4`, but input `3/4` does not match pattern `4`. Equal sets of possible positions match. A linkage suffix is checked even on the final residue of a pattern. For example, `GlcNAcb3` requires that GlcNAc attach onwards by β1–3, although that next residue is not included in the match. At the reducing end there is no outgoing glycosidic linkage, so an acceptor position cannot match there. Use `GlcNAca$`, rather than `GlcNAca4$`, for reducing-end α-GlcNAc. ## Restrict where the match occurs These symbols test position without adding residues to the result: | Pattern | Position required | |:--|:--| | `^Gal` | Gal is at a non-reducing terminus | | `GlcNAc$` | GlcNAc is the reducing-end residue | | `Gal%` | Gal is internal: neither terminal nor reducing-end | ```{r positions} rex_detect(glycans, "^Galb4-GlcNAc") rex_detect(glycans, "Gal%") rex_detect(glycans, "GlcNAcb$") ``` Only the uncapped Gal residues satisfy `^Gal`. In the sialylated examples, Gal is internal and satisfies `Gal%` instead. The symbol `^` means something different **inside** a negated choice: `[^Fuc]` excludes a residue identity; it does not ask for a terminus. Even `^Gal-GlcNAc$` need not describe the whole glycan: extra branches on GlcNAc remain allowed. The two symbols constrain the ends of the selected path, not the total number of residues in the structure. ## Describe branches on a shared residue Ordinary parentheses mean a **branch**. In `Galb4-(Fuca3-)GlcNAc`, Gal and Fuc both attach to GlcNAc: ```text Gal --β1–4--> GlcNAc Fuc --α1–3--> ``` Read `(Fuca3-)` as “also require a Fucα1–3 branch on the following residue.” Keep its attachment dash inside the parentheses and place `GlcNAc` immediately after the closing parenthesis. ```{r branches} rex_detect(glycans, "Galb4-(Fuca3-)GlcNAc") rex_extract(glycans["fucosylated"], "Galb4-(Fuca3-)GlcNAc") ``` A required branch is included in the extracted match. A branch can contain a path, such as `(Galb4-GlcNAcb6-)Man`, and branches can be nested: ```{r nested-branches} nested <- "Gal(b1-4)[Gal(b1-4)[Fuc(a1-3)]GlcNAc(b1-6)]GlcNAc" rex_detect(nested, "Galb4-(Galb4-(Fuca3-)GlcNAcb6-)GlcNAc") ``` Work from the inside out: the inner Fuc attaches to the branch GlcNAc; that GlcNAc attaches by β1–6 to the final GlcNAc. The outer Gal attaches by β1–4 to that final GlcNAc. Extra branches are still allowed. ### Require a branch to be absent Add `!` immediately inside the parentheses: ```{r branch-absence} rex_detect(glycans, "Galb4-(!Fuc-)GlcNAc") ``` This requires the Gal–GlcNAc path with no remaining Fuc branch on its GlcNAc. `(!Fuca3-)` excludes only a compatible α1–3 Fuc branch; `(!Fuc-)` excludes Fuc with any linkage. An absence condition can describe a longer branch too, for example `Gal-(!Fuc-Man-)GlcNAc`. The word **remaining** matters: the check examines branches not already used by the preceding pattern. `Fuc-(!Fuc-)GlcNAc` can match a single Fuc attached to GlcNAc; it asks for no additional Fuc branch there. ### Three different meanings of “not” | Question | How to express it | |:--|:--| | Is a non-Fuc residue attached to GlcNAc? | `[^Fuc]-GlcNAc` | | Does this GlcNAc lack a Fuc branch? | `(!Fuc-)GlcNAc` | | Does the entire glycan lack Fuc? | `rex_detect(glycans, "Fuc", negate = TRUE)` | ```{r compare-negation} data.frame( glycan = names(glycans), non_fuc_neighbor = unname(rex_detect(glycans, "[^Fuc]-GlcNAc")), no_fuc_branch = unname(rex_detect(glycans, "(!Fuc-)GlcNAc")), no_fuc_anywhere = unname(rex_detect(glycans, "Fuc", negate = TRUE)) ) ``` The fucosylated LacNAc has a non-Fuc neighbour (Gal), so it passes the first column. GlcNAc alone has no neighbour, so it fails that column but passes the absence checks. A local branch condition says nothing about fucose elsewhere in a larger structure. ## Optional residues and repeated units Put the unit in square brackets, then say how many copies are allowed. The symbol or braces after the group apply to the **whole group**. | Form | Number of consecutive copies | |:--|:--| | `[Gal]?` | Zero or one | | `[Gal]*` | Zero or more | | `[Gal]+` | One or more | | `[Gal]{2}` | Exactly two | | `[Gal]{2,4}` | Two to four | | `[Gal]{2,}` | At least two | | `[Gal]{,4}` | Zero to four | For example, `[Sia]?-Galb4-GlcNAc` allows an optional Sia before LacNAc: ```{r optional-residue} rex_detect(glycans, "[Sia]?-Galb4-GlcNAc") ``` Optional does not mean forbidden: both sialylated and unsialylated LacNAc can match. Because a match may start within a glycan, even the zero-copy choice can find a capped Gal. Add terminal or neighbouring-path conditions if you need to distinguish those cases. A repeated group can contain several residues: ```{r repeated-unit} repeat_glycan <- "Gal(b1-4)GlcNAc(b1-3)Gal(b1-4)GlcNAc(b1-3)Gal(b1-" rex_extract(repeat_glycan, "[Galb4-GlcNAcb3]{2}") ``` This repeats the Gal–GlcNAc unit twice. The `b3` on GlcNAc constrains its attachment to the next residue, including the final attachment beyond the selected two units. It is a connected path, not a request for two copies anywhere in the glycan. Use `rex_count()` for counting occurrences. Use `[Gal]+`, not `Gal+`. The alternative grouping syntax `(?:Galb4-GlcNAcb3){2}` has the same purpose as square brackets; plain parentheses would mean a branch. A branch can also be optional, as in `Galb4-(Fuca3-)?GlcNAc`. It allows the Fuc branch to participate in the match but does not exclude other branches or other Fuc linkages when that optional branch is skipped. ### Prefer longer or shorter repeats By default, repetition tries longer runs first. This is called **greedy** matching. Add `?` after the repetition symbol to try shorter runs first, called **lazy** matching. The rest of the pattern must succeed either way. ```{r repeat-preference} chain <- "Gal(b1-4)GlcNAc(b1-4)GlcNAc" rex_extract(chain, "Hex-[HexNAc]+") rex_extract(chain, "Hex-[HexNAc]+?") ``` The first includes both GlcNAc residues; the second includes just one. In `[Gal]?`, the question mark means optional; in `[Gal]+?`, it changes which repetition length is tried first. In `Gal?4`, it instead belongs to the linkage suffix. Use the bracketed form `[Gal]?` to make optionality clear. Even when zero copies are allowed, glyrex does not return an empty match: `[Gal]*` on a glycan without Gal does not produce a successful zero-residue result. Unbounded repetition is limited by the residues in the input glycan. ## Check neighbours without including them (advanced) Sometimes you want to extract Gal only when it has particular neighbours. A **lookaround assertion** checks a neighbouring path without adding those neighbours to the match. “Ahead” means towards the reducing end; “behind” means towards the non-reducing side. | Pattern | Condition; only Gal is included | |:--|:--| | `Gal-(?=GlcNAc)` | The next residue towards the reducing end is GlcNAc | | `Gal-(?!GlcNAc)` | That next residue is not GlcNAc, or there is none | | `(?<=Sia-)Gal` | A Sia is directly attached on the non-reducing side | | `(?...)`, replacing `name` with a useful label: ```{r named-capture} rex_match( glycans, "(?Sia)-(?Galb4-GlcNAc)" ) ``` The `match` column contains the full match; `cap` and `unit` contain the labelled parts. Inputs without a match have missing values. Captures include residues in the full match, whereas lookarounds only test their presence. Ordinary branch parentheses do not create a named column. Names must start with a letter and contain only letters, digits, or underscores. Use each name once, and do not use the reserved name `match`. Use `rex_match_all()` to retrieve all retained matches and their captures. ## Build and check your own pattern Start with the required residues, then add one condition at a time: | Step | Pattern | Added requirement | |:--|:--|:--| | 1 | `Gal-GlcNAc` | Adjacent Gal and GlcNAc | | 2 | `Galb4-GlcNAc` | β1–4 linkage | | 3 | `Sia-Galb4-GlcNAc` | Sialic acid on Gal | | 4 | `^Sia-Galb4-GlcNAc` | That sialic acid is terminal | | 5 | `^Sia-Galb4-(!Fuc-)GlcNAc` | No remaining Fuc branch on that GlcNAc | ```{r finished-pattern} pattern <- rex_compile("^Sia-Galb4-(!Fuc-)GlcNAc") rex_subset(glycans, pattern) ``` `rex_compile()` checks the syntax and prepares a reusable pattern. It does not establish that the pattern asks the biological question you intended. Try known positive and negative structures, and inspect extracted matches. | Common mistake | Use instead | |:--|:--| | IUPAC linkage in a pattern: `Gal(b1-4)GlcNAc` | `Galb4-GlcNAc` | | Attachment dash outside branch: `Gal-(Fuc)-GlcNAc` | `Gal-(Fuc-)GlcNAc` | | Ordinary parentheses for a repeated path: `(Gal)+` | `[Gal]+` | | Bare alternatives: Gal|Man | [Gal|Man] | | Bare negation: `!Fuc` or `[!Fuc]` | `[^Fuc]` for a non-Fuc residue; `(!Fuc-)` for branch absence | | Using `.` to skip an arbitrary path | `[.]*` for zero or more connected residues | ## Interpret the results in structural terms - `rex_detect()` asks whether a match exists; `rex_subset()` keeps matching inputs. Missing inputs give `NA` in detection, and are omitted by subsetting. - `rex_extract()` returns one retained match; `rex_extract_all()` returns all retained matches. Extracted strings use standardized IUPAC notation. - `rex_count()` counts retained structural matches, not glycan abundance. glyrex keeps the first successful match per starting residue, then removes matches wholly contained in larger matches. Remaining matches may overlap; it does not enumerate every possible combination of alternatives or branches. - Matches are ordered by decreasing residue count, then starting residue ID. The first result is not necessarily the leftmost text in the input string. - `rex_locate()` and `rex_locate_all()` report graph residue and edge IDs, not character positions in a string. The input must describe a supported connected structure. Composition alone cannot answer connectivity questions, and floating parts or unspecified branch placement are not supported. Invalid structures produce errors rather than `FALSE`. Unspecified pattern fields accept unknown input information, but specified constraints require input information at least as specific. See `?rex_compile` for compiling reusable patterns and `?rex_detect` for query functions and result conventions.