One of the recurring themes of our work is how to make results clear to the rest of the world. This is the I in the FAIR principles: interoperable. Generally speaking, this challenge boils down to using a common language or an ontology to describe an object or a relation. In metabolomics, however, we regularly have to deal with another level of information: confidence, or lack of confidence. While MS and MS2 data are immensely powerful to reveal compositional or structural aspects of a molecule, it is rarely enough to fully elucidate the chemical structure. So how do we clearly communicate what we know and what we don’t?
NOTE: A revised version was published in https://metabolomics.blog/2026/08/fair-lipid-representation-v2/
TL;DR

- Lipid shorthand is very good at saying what we know and what we don’t, but most cheminformatics tools want a molecular structure. Translating one into the other without inventing certainty is surprisingly difficult.
- CXSMILES can be tweaked to produce the desired outome:
ctu:,f:,Sg:andm:can encode several common types of structural ambiguity, including unknown geometry, regiochemistry, double-bond positions, and modification positions. - In practice, the pieces don’t always compose, and CDK and RDKit interpret or depict them differently. We therefore use a few pragmatic hacks for visualization while keeping the less pretty representation in the data.
- The biggest unresolved problem is confidence. We can say that one position is supported much more strongly than another, but there is currently no satisfactory way to carry those weights into a molecular structure.
The problem
The lipidomics community has a well-developed and useful vocabulary for partial knowledge: the shorthand nomenclature. Most lipids have a modular structure that can be described at hierarchical levels that add details (Liebisch et al. (2020)). They can be organized hierarchically, as reflected by the SwissLipids database.
| level | examples | analytical techniques |
|---|---|---|
| species | PC 34:1 | MS1 |
| composition of acyl chains | PC 16:0_18:1 | MS2 |
| sn-position of acyl chains | PC 16:0/18:1 | MS2 |
| position of modifications in acyl chains (C=C, -OH, epoxy) | PC 16:0/18:1(9) | PB + CID, OxID, UVPD, OAD, EAD, … |
| full stereochemistry of modifications | PC 16:0/18:1(9Z) | LC for simple cases, ion mobility, EAD? |
Shorthand notation is precise and well standardized (see Goslin 2.0). The problems, however, start when we want to use it to describe the results of MS experiments. Three examples:
- Communicating (lack of ) confidence: deeper levels of structural characterization require more experimental data, special techniques for activation/dissociation, and a good portion of luck: in practice, it’s common to obtain only noisy MS2 spectra for features with low abundance, or to struggle with the overlap of MS2 fragments originating from multiple acyl chains. In many cases, the result is not certain. One common approach is to move to a level with lower complexity. How do we express that we might be sure about the positions of some, but not all, C=C positions in an acyl chain? (think of alpha vs gamma linolenic acid).
- Communicating with machines: the vast majority of cheminformatics, computational chemistry, machine and deep learning, language models, etc., too, can’t do much with shorthand names. They build on structural descriptors like SMILES, SELFIES, and InChi. If we want to produce interoperable data, there is no way around engaging with one of these standards.
- Communicating with humans: this is perhaps a minor point, but a key aspect of human communication is the ability to visualize objects, i.e. molecular structures. For example, we are frequently asked to show what part of a molecule is represented by an MS2 fragment. For example: How to we depict PC 34:1 and indicate what atoms correspond to fragment 184.07?
We hit these walls repeatedly while recoding LipidOracle, our engine for annotating lipids from a variety of LC-MS techniques. After some time spent reading, testing, failing, and hacking workarounds, we eventually made some progress and tentatively addressed the three problems automatically. This post provides an overview of what seems to work and what doesn’t. The hope is to trigger a discussion and gather ideas on how to remove outstanding barriers.
1. Representing lipids in CXSMILES
It was easy to choose a language to communicate in. SMILES represents a lingua franca of cheminformatics, perfectly suited to represent 2D and 3D molecular structures with strings. For example, oleic acid and its trans- isomer elaidic acid translate to:
FA 18:1(9Z) → OC(=O)CCCCCCC/C=C\CCCCCCCC
FA 18:1(9E) → OC(=O)CCCCCCC/C=C/CCCCCCCC
SMILES are sufficient to describe any fully defined lipid. To express uncertainty, we need to resort to CXSMILES (ChemAxon Extended SMILES). It’s an extension that appends a |...| block carrying extra semantics after the SMILES string. It was designed primarily for Markush structures, polymers, and reaction schemes and offers concepts we could borrow. Most of the ideas came from CDK CXSMILES by Egon Willighagen, Adriano Rutz, and Zhixu Ni. Four features must be mentioned. We start with the simplest.
1.1 Expressing unknown geometry with ctu:
ctu stands for cis/trans unspecified, and it is followed by the indices of the bonds it relates to. Bonds are numbered in the order they appear as the SMILES string is read left to right. A fatty acid with a known double bond at Δ9 but unknown geometry becomes:
FA 18:1(9) → OC(=O)CCCCCCCC=CCCCCCCCC |ctu:10|
Why 10? Because bond numbering starts at 0 and counts every bond, including the carbonyl C=O and every C–C inside branches. So it diverges from carbon numbering immediately and never recovers.
Here is what a more interesting case looks like: every double bond localized, every hydroxyl in place, and only the cis/trans geometry left unspecified:
FA 20:4(5,8,11,14);OH(3) ↓ OC(=O)CC(O)CC=CCC=CCC=CCC=CCCCCC |ctu:7,10,13,16|
If we use CDK to draw this CxSMILES, we obtain:

ctu: entry). A double bond with known cis- or trans- configuration is indicated in the SMILES section with either C/C=C\C or C/C=C/C. Alternatively, you can use c:7 or t:7 blocks.
NOTE that for modifications like OH we deviate from the convention of writing ;3OH and opt for ;OH(3). As we’ll see later, the latter allows us to better express confidence and multiple modifications.
1.2 Expressing unknown sn- regioisomerism with f:
A . in a SMILES string separates fragments, but from a different angle. ChemAxon defines f: as fragment-level grouping of reactant/agent/product fragments: grouped fragments are “connected groups”, not mutually exclusive alternative connections. For lipids with unknown position of acyl chains (_ in shorthand notation), we can tweak the CXSMILES f: block to specify how fragments are connected to build a single entity. For example:
PC 16:0_18:1(9)
↓
C(COP(=O)([O-])OCC[N+](C)(C)C)(O*)CO*.*C(=O)CCCCCCCCCCCCCCC.*C(=O)CCCCCCCC=CCCCCCCCC |f:0.1,0.2,ctu:44|
The backbone is the glycerophosphocholine skeleton with two * attachment stubs where chains go. Each chain is its own dot-separated component, also led by a * stub. f:0.1,0.2 declares two component groups: {0,1} and {0,2}. Component 0 (the backbone) pairs with component 1, and also with component 2. It’s important to note that we are using a construct that was designed for and as or. Hence, we go beyond the CXSMILES specs. Is this legit? We don’t know.
As we see in this example, blocks can be simply concatenated. The wavy bond on the 18:1 chain shows that ctu:44 is honored even here.
Side note on the *. It’s a stub, not a carbon. In *C(=O)CCCCCCCCCCCCCCC, the * stands in for the ester oxygen on the backbone side, not for C1 of the chain. The chain keeps all sixteen of its own carbons. If we plot this CXSMILES with CDK, we obtain a single image with the 3 fragments.

1.3 Expressing the unknown position of C=C bonds with Sg:
Sg is CXSMILES’s polymer S-group notation, designed for describing repeating units in polymers. We use it to mean “a run of methylenes whose length is a free variable”. Each marker declares a one-atom repeating unit, like a single CH₂ repeated n times. The rule is N+1: for N double bonds you need N+1 flexible runs, one before the first C=C and one after each. A fatty acid with two double bonds at unknown positions leads to 3 Sg: elements:
FA 18:2
↓
OC(=O)CC=CC=CC |Sg:n:3:a:ht,Sg:n:5:b:ht,Sg:n:8:c:ht,ctu:4,6| a+b+c=14
The literal string contains only 7 carbons. The other 11 exist only in that trailing equation a+b+c=14. Note that the final term with the sum lives outside the pipes. It’s not part of the CXSMILES standard at all. Using it to express constraints on acyl chain length was proposed by Willighagen et al, but it is not a standard. Nothing enforces it, nothing checks its consistency (like that only known letters are used), and no toolkit reads it. Software that ignores the |...| block sees a seven-carbon diene acid and reports it with total confidence, an eleven-carbon error that propagates into every mass, formula, and similarity calculation downstream. The CDK interprets it as a title, a string, which can be embedded in the illustration.

The same mechanism scales to any degree of unsaturation. Here is arachidonic acid with all four double bonds unlocalized: five markers, one constraint, the same N+1 rule:
FA 20:4 ↓ OC(=O)CC=CC=CC=CC=CC |Sg:n:3:a:ht,Sg:n:5:b:ht,Sg:n:7:c:ht,Sg:n:9:d:ht,Sg:n:12:e:ht,ctu:4,6,8,10| a+b+c+d+e=14

Partial localization falls out of the same mechanism for free. FA 18:2(9) declares two double bonds but gives one position, so Δ9 is written literally and only the tail becomes flexible: Sg:n:12:a:ht,Sg:n:15:b:ht| a+b=6.
And on a full glycerophospholipid where sn-positions are known but one chain is unresolved:
PC 16:0/18:2
↓
C(COP(=O)([O-])OCC[N+](C)(C)C)(OC(=O)CC=CC=CC)COC(=O)CCCCCCCCCCCCCCC |Sg:n:16:a:ht,Sg:n:18:b:ht,Sg:n:21:c:ht,ctu:17,19| a+b+c=14
With CDK we obtain:

1.4 Expressing modifications with unspecified position with m:
In CXSMILES, m: is formally a multicenter S-group, more commonly called a position-variation or Markush bond. When a modification’s presence is established but its position isn’t, the modified group is written as its own disconnected fragment and m: declares exactly one bond from it to one of the candidate atoms on the chain:
FA 18:0;COOH
↓
OC(=O)CCCCCCCCCCCCCCCCC.[C](=O)O |m:20:3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19|
m:20:3.4....19 says atom 20 (the floating carboxyl carbon, the 21st from left) bonds to exactly one of atoms 3 through 19 — not all of them, one, unspecified which. This is the only one of the four blocks whose semantics are a genuinely exact fit for what we mean. The other three are approximations.
Note the brackets: the carboxyl is written [C](=O)O, not C(=O)O, because a bare carbon already carries implicit hydrogens with nothing left to bond. Bracket notation declares the hydrogen count explicitly and leaves a valence open.
A side note on the candidate list: For an acyl chain, C1 is the ester carbonyl — not a substitutable methylene — so it never appears. The list starts at C2, which is why the example above holds 17 entries for an 18-carbon chain. And when Sg: is also present, the candidate list shrinks further: it can only enumerate atoms that physically exist in the string, so every carbon hidden inside a flexible run is omitted. m:36:32.33.34.35 on an 18-carbon chain lists four candidates where the truth is closer to seventeen. The two blocks were designed independently, for unrelated purposes, and simply don’t compose.
1.5 Examples with multiple blocks
The four blocks don’t live in isolation, and this is where the approach either proves itself or doesn’t. Real lipids from real experiments routinely need several at once, and the way they compose reveals both the power and the limits.
Start with a relatively clean case, which immediately highlights a problem: sn-positions known, both double bonds unlocalized, but a hydroxyl at a known position on the same chain:
DG 18:0/18:2;OH(5) ↓ C(CO)(OC(=O)CCCC(O)CC=CC=CC)COC(=O)CCCCCCCCCCCCCCCCC |Sg:n:11:a:ht,Sg:n:13:b:ht,Sg:n:16:c:ht,ctu:12,14| a+b+c=10
The slash separator leads to one connected molecule without f: blocks. The saturated sn-1 chain is written out completely. The sn-2 chain has its hydroxyl drawn in place at C5 (C(O)), but its two double bonds are unlocalized, so three Sg: markers carry them with a constraint of a+b+c=10 (five carbons are already written literally). The hydroxyl is exact; the double bonds are honest about their ambiguity. The problem is that we don’t know whether the 3 C=C all remain on the hydrocarbon side of C5. Hypothetically, one double bond could be located between C1 and C5. Luckily, this is not a problem we encounter with MS2: assigning the position of OH to C5 implies that we observed fragments for C4-C5 and C5-C6, differing by the mass of CH2O. As the m/z of these fragments depends on the composition of the acyl chain until the head group, they will also reveal the degrees of saturation between C1 and C5. Hence, it will be possible to determine whether the OH was preceded by a carbon double bond. This doesn’t solve the problem of translating DG 18:0/18:2;OH(5) into a fair CXSMILES, but it’s irrelevant for our case.

Let’s flip it: double bonds are localized, but the hydroxyl floats:
DG 18:0/18:2(9,12);OH ↓ C(CO)(OC(=O)CCCCCCCC=CCC=CCCCCC.[OH])COC(=O)CCCCCCCCCCCCCCCCC |m:23:6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22,ctu:13,16|
Both double bonds are written at their known positions. The hydroxyl has no position, so it can’t be a literal C(O). Instead, it’s appended as a disconnected [OH] fragment. The m:23:6.7.…22 block enumerates every substitutable carbon on that chain (C2 through C18) as a candidate. Note where the .[OH] sits: nested inside the sn-2 branch, immediately after the chain it belongs to. A dot inside a branch is legal SMILES. It starts a new disconnected component without disturbing the parentheses. This is a layout hint smuggled through token order: when the m: block is inevitably ignored by the renderer, at least the floating fragment lands near its own chain.
As we will see later, drawing this CXSMILES hides a problem. With a little hack, we can obtain a visual representation of the OH “floating” along the sn-2 chain.

Now the full monty: a name that needs all four blocks at once. This is the normal case, not an edge case:
DG 18:1(9);OH(5)_18:1;OH ↓ C(CO)(O*)CO*.*C(=O)CCCC(O)CCCC=CCCCCCCCC.*C(=O)CC=CC.[OH] |f:0.1,0.2,Sg:n:32:a:ht,Sg:n:35:b:ht,m:36:32.33.34.35,ctu:18,31| a+b=15
Chain 1 (18:1(9);OH(5)) is fully localized so it contributes only a ctu: entry for its unspecified geometry. Chain 2 (18:1;OH) knows neither the double bond position nor the hydroxyl position, and contributes an Sg: pair, an m: block, and a ctu: entry. The two chains sit side by side in one string at completely different rungs of the ladder from the introduction. And they share an f: block because the underscore means we also don’t know which sn-slot each occupies.
| what’s unknown | which block |
|---|---|
| which chain is sn-1 vs sn-2 | f:0.1,0.2 |
| chain 2’s double-bond position | Sg:n:32:a:ht,Sg:n:35:b:ht |
| chain 2’s hydroxyl position | m:36:32.33.34.35 |
| both chains’ double-bond geometry | ctu:18,31 |
This results in the following image:

Finally, a very common case of a triglyceride with three chains at three different levels of determination: TG 18:0_18:1_18:2. This is a case that can hardly be solved by enumerating all isomers with SMILES. The saturated 18:0 chain is fully written and contributes to neither constraint equation. The 18:1 chain needs two Sg: markers. The 18:2 chain needs three. Each chain gets its own constraint, comma-joined, and matching is positional — the first constraint consumes the first two markers, the second consumes the next three:
TG 18:0_18:1_18:2 ↓ C(CO*)(O*)CO*.*C(=O)CCCCCCCCCCCCCCCCC.*C(=O)CC=CC.*C(=O)CC=CC=CC |f:0.1,0.2,0.3,Sg:n:32:a:ht,Sg:n:35:b:ht,Sg:n:39:c:ht,Sg:n:41:d:ht,Sg:n:44:e:ht,ctu:31,37,39| a+b=15,c+d+e=14

Overall, these examples demonstrate how the CXSMILES extension with Sg, m, f, and ctu blocks can serve our purpose in translating shorthand notation into faithful structural, machine-readable descriptions. All examples shown here were automatically generated in LipidOracle.
2. Drawing CXSMILES
Generating a correct CXSMILES string is the easy half. Getting a picture out of it is where things get difficult. There are several depiction tools. We tested with CDK Depict and RDKit, both of which claim CXSMILES support. It turns out that neither implements all of it.
| block | CDK Depict | RDKit |
|---|---|---|
ctu: | ✅ wavy bond | ✅ crossed double bond |
f: | ⚠️ parsed, fragments still drawn apart | ⚠️ parsed, fragments still drawn apart |
Sg: + constraint | ✅ labeled repeat brackets, constraint as title | ❌ silently ignored |
m: floating [OH] | ⚠️ draws detached radical | ❌ hard parse failure |
m: dummy-stub *O | ⚠️ collapses to one position | ✅ highlights all candidates for ONE modification |
Let’s show some examples:
f: is honored by neither, for drawing purposes. Both toolkits parse the block and both still draw the components as separate fragments side by side. Neither toolkit assembles them into a molecule with a “this chain OR that chain” annotation. This is a 2D drawing convention for that doesn’t yet exist. In RDKit, f:0.1,0.2, f:0.1.2, and omitting the block entirely all yield the same three disconnected fragments.
Sg: in RDKit is silently ignored. Not warned about. Not errored on. RDKit parses the base SMILES, discards the block, and hands you a perfectly valid Mol object but for the wrong molecule:
>>> m = Chem.MolFromSmiles("OC(=O)CC=CC=CC |Sg:n:3:a:ht,...| a+b+c=14")
>>> rdMolDescriptors.CalcMolFormula(m)
'C7H10O2' # should be C18H32O2
That is an eleven-carbon error, and nothing in the returned object flags it. If you pipe LipidOracle CXSMILES into an RDKit workflow without expanding first, this is the failure mode, and you will never know it happened. Always expand before handing a stored string to RDKit or use the SMARTS output, which has no CXSMILES blocks at all.
m: in our stored form is a hard parse failure in RDKit. Our floating [OH] doesn’t match RDKit’s requirement that the variable end be a dummy atom with exactly one bond, so RDKit rejects the whole string. At least this one fails loudly. But the dummy-stub form, *O, parses in both toolkits and in RDKit it renders the position variation properly, highlighting every candidate carbon in grey with the -OH drawn once. That is, by a wide margin, the best depiction of “a hydroxyl somewhere on this chain” in existence. On a multi-chain lipid, it also answers “which chain” in the same stroke. The multi-chain problem simply doesn’t arise, because every highlighted atom lies on one chain.
We haven’t switched because the * persists as an atom in computed formulas (C18H37*O3), CDK collapses the variation to one arbitrary position, and it changes stored output so archives need migrating. But if the primary consumer of the SMILES column is RDKit-based tooling, the current choice may not be optimal for the most common use case. This is worth revisiting, and we’d welcome input on it.
m: in CDK draws a floating modification, which in the case of lipids with multiple acyl chains is ambiguous. For the aforementioned example of
DG 18:0/18:2(9,12);OH ↓ C(CO)(OC(=O)CCCCCCCC=CCC=CCCCCC.[OH])COC(=O)CCCCCCCCCCCCCCCCC |m:23:6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22,ctu:13,16|
CDK depicts the following image, in which it is no longer clear that the OH group sits on the 18:2 chain:

2.1 Workarounds for making pictures work in practice
Many issues might be fixable, but we don’t know how. In LipidOracle, we implemented some hacks to produce images that, based on our own subjective judgment, are better representations. In the dashboard, lipid images are created on the fly using external web services that translate a CXSMILES into a PNG or SVG. Under the cover, LipidOracle keeps two forms of every structure. The stored form is the rigorous CXSMILES written into the database with every uncertainty explicitly marked. A second, formally incorrect form is produced on demand to obtain the desired image.
One not-so-elegant approach that we embedded to overcome most limitations is the Sg: expansion. It consists of sampling a,b,c… numbers to obtain one example. Each constraint equation is solved by picking one valid even split across its variables:
stored: OC(=O)CC=CC=CC |Sg:n:3:a:ht,Sg:n:5:b:ht,Sg:n:8:c:ht,ctu:4,6| a+b+c=14
↓ a=4, b=4, c=6 (one sample out of many)
depicted: OC(=O)CCCCC=CCCCC=CCCCCCC
The expanded string is now C18 and draws correctly everywhere. But the picture now shows double bonds at Δ6 and Δ11. It’s one example out of many, and we code the expansion to always pick different sets. The figure is complemented with a note that the molecule is just one representative example out of XY. This is useful when matching MS2 fragments to the structure.
The same hack applies to _-joined names: we build the molecule as if it were /-joined, arbitrarily assigning chains to sn-positions. Otherwise you get the loose-fragment picture, which reads as a bug rather than a statement. The honesty has been moved out of the image and into a caption..
And then there is the floating hydroxyl problem, illustrated just above for DG 18:0/18:2(9,12);OH. [OH] is the chemically exact representation of the unlocalized hydroxyl group. But when a renderer ignores the m: block, it draws a lone •HO radical next to your lipid, which a biologist reads as contamination or software failure, not as “hydroxyl, position unknown.” So the depiction expander applies a table of pragmatic substitutions.
We think that the most intuitive way of depicting DG 18:0/18:2(9,12);OH is

This is obtained with the (i) formally wrong substitution of .[OH] with .CO and assigning the m: to a specific bond in the sn-2 chain, here 6.7. Note that the indices must be adjusted. These are the two CXSMILES compared: the first for the machines, the second for the human eye.
Correct:
C(CO)(OC(=O)CCCCCCCC=CCC=CCCCCC.[OH])COC(=O)CCCCCCCCCCCCCCCCC |m:23:6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22,ctu:13,16|
Wrong (for depiction):
C(CO)(OC(=O)CCCCCCCC=CCC=CCCCCC.CO)COC(=O)CCCCCCCCCCCCCCCCC |m:24:6.7,ctu:13,16|
Is there a better way?
3. Open questions and known limitations
No representation scheme is perfect, and ours has gaps we’re well aware of. Each of the following is an area where we’d particularly welcome input from the cheminformatics and lipidomics communities. Below, we highlight some of the problems we encountered but could bypass withcreative hacks. There are more challenging cases we’d like to bring up, hoping that someone might have suggestions.
3.1 Confidence scores have nowhere to live
This is the big one, and it deserves more space than the others because it’s the single most valuable thing the pipeline produces and the single thing we cannot put into a structure.
For example, LipidOracle’s EAD engine localizes double bonds by matching observed fragment ions against predicted ones for every candidate isomer. Usually no single isomer wins outright. Several score similarly, because the diagnostic fragments that would separate them are weak, absent, or shared. Enumerating all is an option with obvious limits given the combinatorial explosion of lipid isomers. Discarding the runners-up or reporting a false certainty is also undesired.
We now aggregate all best hits to identify whether they agree on some information. For example, in PUFAs, or lipids with polyunsaturated chains, it is quite common to identify the position of the central double bonds, but lack information to assign the last, most distal position. This corresponds to having 100% confidence about some, and 40-60% for the last. How do we represent this?
Now we simply report the numbers next to each enriched position that is significant. The number is computed as a softmax-weighted vote across the kept candidates. The number is rounded and added after the position in () brackets separated by ~. To be clear: this is a totally arbitrary decision that is not supported
FA 18:2(9~92%,12~88%);OH(11~64%)
Read: 92% of the score-weighted surviving candidates put a double bond at Δ9, 88% put one at Δ12, and 64% put the hydroxyl at C11. That is an honest, precise, information-dense statement. It distinguishes a position the data nails (92%) from one it merely leans toward (64%). The consensus notation generalizes to any pattern of evidence: 18:1(9~100%) when all candidates agree, 18:1(9~80%,11~20%) when one position is preferred but another is plausible, 20:4(5~100%,8~100%,11~100%,14~59%,15~41%) when three positions are nailed and the last splits across neighbors.
It cannot be turned into a structure. ~ is our invention. There is no ~ in SMILES, and more fundamentally there is no CXSMILES construct for weighted alternatives. m: says “one of these positions” with no way to add “and this one is far more likely.” Sg: says “somewhere in this run,” uniformly. Ambiguity in both formats is uniformly distributed by construction; our evidence is not. That mismatch isn’t a gap in our implementation, but in the formats.
What we think of doing instead: rewrite the consensus name using a hard >50% majority threshold. A position above 50% becomes known and localized. Below 50% is dropped from the position list but the declared double-bond count stays, so the leftover bond re-enters through the “N declared, K given” path and comes out as an Sg: run. A modification without a majority position degrades to a bare ;OH, keeping the formula right even though the position is abandoned.
The threshold is a cliff. Consider a chain where the evidence splits 9~55% / 11~45% versus one that splits 9~45% / 11~40% / 13~15%. The first crosses 50% and is drawn as a hard Δ9. The second has no majority, so the bond stays unlocalized. Two nearly identical evidence states, two categorically different structures. A ten-percentage-point shift flips the output between “certain” and “unknown” with nothing in between: the one thing a continuous confidence measure should never produce.
Every trace of the uncertainty is gone. The 64%-supported hydroxyl at C11 is drawn with exactly the same confidence as the 92%-supported double bond at Δ9. A reader of the SMILES column cannot distinguish a 51% call from a 100% call — the representation has flattened a probability distribution into a point estimate and discarded the error bars. It’s an approximation, and it’s flagged us such in the caption.

FA 18:2(9~92%,12~88%);OH(11~64%). Nothing in the picture hints at those percentages.The full annotation CSV preserves the complete ~percentage notation in what we call idlevel 4, and lists every individual candidate as idlevel 3 rows. Nothing is lost from the pipeline. It’s lost from the structure representation.
What we’d want: some way to attach a weight to a position-variation candidate — conceptually m:20:3;0.92.4;0.08 — so a depicter could shade candidates by confidence and a parser could carry the distribution through. As far as we can tell, no such construct exists, no toolkit would render it, and inventing a private extension would move the interoperability problem rather than solve it. Ideas very welcome.
3.2 Too simple to draw: the species-level problem
Ironically, the simplest shorthand example are the most challenging.
Take PC 38:4 as a concrete example. We know the total carbon count across both chains (38) and the total number of double bonds (4). What we don’t know is how those 38 carbons and 4 double bonds are partitioned between the two chains. The chains could be 16:0 and 22:4, or 18:1 and 20:3, or 18:2 and 20:2, or any other split that sums to the right totals. The problem has two layers.
The first layer is the carbon counts, and is uncomfortable but arguably tractable. We could write a backbone with two * stubs and two chain fragments, each with enough Sg: markers to cover any plausible chain length. A single global constraint like a+b+c+d+…=36 (38 total carbons minus the two ester carbonyls) would keep the sum correct, and the f: block would handle the regiochemistry as usual. This is a natural extension of what we already do for per-chain constraints, just lifted to span across chains.
The second layer is the carbon double bonds. This is where the tools genuinely run out. We have four double bonds to distribute across two chains, and we don’t know the split. The difficulty is not that there are many possibilities; it’s that none of our four CXSMILES constructs can express “a C=C bond may or may not be present at this position.” Sg: varies the length of a methylene run. It says “this stretch is somewhere between m and n carbons long“. It cannot make a double bond appear or disappear. m: connects a floating fragment to one of several candidate atoms. It varies where a group attaches. It cannot turn a single bond into a double bond, because a double bond is not a fragment you attach: it’s a change in the bond order between two carbons that are already connected, and m: has no concept of bond order at all.
To put it more formally: m: creates a new bond between a floating atom and one candidate atom on the scaffold. It controls connectivity. A double bond is not a new connection. It’s a change to the order of an existing connection. There is no CXSMILES construct for “the bond between these two atoms is either single or double“, because the format was never designed to express unsaturated- vs. saturated-bond ambiguity.
A similarly “simple example is PC 36:3;O, with O standing for one extra oxygen sitting somewhere on the molecule. This adds a subtler dimension to the same problem. The ;O notation says there is an extra oxygen somewhere, but it doesn’t say in what form. A hydroxyl (;OH) adds one oxygen while leaving the chain’s unsaturation count unchanged: the three double bonds are all genuine C=C bonds in the chains. A ketone (;oxo) also adds one oxygen, but a carbonyl replaces a CH₂, which means one of the three “double-bond equivalents” declared in the name is actually the C=O, and only two are chain C=C bonds. The same name, PC 36:3;O, describes two chemically distinct situations: three chain double bonds plus a hydroxyl, or two chain double bonds plus a ketone. And you cannot tell which from the name alone, because the shorthand deliberately doesn’t distinguish them.
Representing this in CXSMILES is strictly harder than the PC 38:4 case. Even if we solved the chain-length and double-bond distribution problems, we would need the m: block to do something it was never designed for: simultaneously vary both where the oxygen attaches and what the bond order is at that position. For a hydroxyl, m: would create a single bond from the floating [OH] to a chain carbon. For a ketone, m: would need to create a double bond from the floating [O] to a chain carbon — but m: has no concept of bond order, and the chain carbon at that position would also need to lose two hydrogens. The two modifications are structurally incommensurable: one is an addition, the other is a substitution. A single CXSMILES string cannot encode “either of these two different chemical situations“, because the format’s ambiguity constructs all assume the scaffold is fixed and only the attachment points vary.
This is, in a way, a backhanded compliment to the shorthand notation itself. Two characters ;O pack an extraordinary amount of structured ambiguity into the name. The shorthand is powerful precisely because it doesn’t commit to details the evidence doesn’t support.
3.3 Lack of canonicalization
As SMILES, CXSMILES can be written in many different ways to represent the same molecules. We build CXSMILES in a deterministic way, but it doesn’t reflect the form of a canonical SMILES. Rearranging the indices of atoms and bonds has a major effect on the CXSMILES extension. Is there a good tool to canonicalize CXSMILES in all their complexity?
3.4 Round trip validation
We still lack an inverse tool to convert our pseudo-CXSMILES to shorthand annotation strings. It’s not difficult. We just didn’t have the time yet. This will be critical to assess round-trip validation errors and the semantic equivalence of the two representations.
4. Where this leaves us
Building on what others suggested before, we can tweak CXSMILES into a molecular representation of what is encoded in a shorthand string.
We have a system that works for most cases. It produces molecular representations from shorthand lipid names, in both string and image formats.
It’s not CXSMILES standard. For example, we tweak f: interpretation from and to or, and we make use of the string following the pipes to set constraints on chain lengths. These arbitrary extensions are not understood by downstream analysis tools. Depending on the complexity of the lipid and on the specifics of the downstream analysis, it might be preferable to apply Sg: and f: expansion to ensure correct downstream results.
The gaps remain, and they cluster around the same theme. The formats we have were designed for certainty, and lipidomics/metabolomics are keen to produce partial knowledge. CXSMILES can be hacked into the desired direction, but the final composition is imperfect. For instance, m: candidate lists can’t see inside Sg: runs, variable names collide after ten, rings vanish, and there is no round trip. They render inconsistently across the two major toolkits. And they cannot carry the one thing our pipeline most needs to convey: how confident we are in each assignment.
We’d like to switch the m: encoding from floating [OH] to dummy-stub *O. We’d like to support species-level shorthand. We’d like a round-trip parser. And we’d really, really like a way to put a local score on a position. If you have ideas, please get in touch.
Note: the results were obtained with LipidOracle 1.0.163, RDKit 2024.09.6, CDK Depict (accessed 2026-08-08).
A nice read but you have a lot of misunderstanding with the features of CXSMILES. Really for lipids you need new sets of “operators” to represent the structural variations you need (e.g. double bond position) rather than trying to shoehorn in to the existing notation. The biggest challenge is the CXSMILES parts are mainly intended for well established Markush structures from patents which have nice ways to draw them. A lot of what you’re are trying to do I really don’t think there is a better way to draw it.
ctu:
In SMILES a double bond can either be one of three state cis, trans or unspecified. If you do not specify the configuration it is presumed unspecified by default (meaning it is either E/Z). The ctu: is intended for queries when you want to match any configuration, this is somewhat specific to ChemAxon because most tool kits would do that anyways since SMARTS can handle this: “C/C=C/?\?C”.
f:
This section is completely wrong, the fragment grouping is ONLY intended for reactions when you have molecules which are composed of multiple parts you want to keep together (e.g. Na+Cl- you want to keep together). It is mainly useful for displaying reactions nicer. What I think you want here is R groups:
C(COP(=O)([O-])OCCN+(C)C)(O)CO |$;;;;;;;;;;;;;;R1;;;R1$,RG:_R1={*C(=O)CCCCCCCCCCCCCCC |$_AP1$|},{*C(=O)CCCCCCCC=CCCCCCCCC |$_AP1$|}|
In the RG definitions you can omit the _AP1 if it is the first atom.
C(COP(=O)([O-])OCCN+(C)C)(O)CO |$;;;;;;;;;;;;;;R1;;;R1$,RG:_R1={C(=O)CCCCCCCCCCCCCCC},{C(=O)CCCCCCCC=CCCCCCCCC}|
m:
As it stands currently, the multiple centre needs a ‘*’ atom and anything else is ignored. There is potentially scope to allow this atom to overwrite the exiting atom e.g. c1ccccc1.nCl |m:6:0.1.2.3.4.5| but this would be an extension and would have a hard time convincing others this is reasonable. I also think it is possibly reasonable to allow no bonds e.g. c1ccccc1.n |m:6:0.1.2.3.4.5| but again no sure how you would display this and you can technically again already do with with an RGroup – c1ccccc1C****C |$;;;;;;R1;R1;R1;R1;R1$,RG:_R1={C},{N}|. I’m not sure I fully understand what the OH(5) means – if it is just one one atom (C5?) why not put it there fixed?
This is great feedback! Thank you. I’ll implement the corrections and reflect on the pathological cases. Thx!
Not sure if my other comment got swallowed but you have some misunderstandings of CXSMILES, particularly with the “f:” fragment grouping is completely wrong.