๐Ÿ†• Convert SMILES to HELM๏ƒ

Problem๏ƒ

You want to convert a SMILES string to a HELM string.

See also

Ingredients๏ƒ

Difficulty Level๏ƒ

๐ŸŒถ๏ธ ๐ŸŒถ๏ธ

Download๏ƒ

Download code

smiles2helm.py

See also Usage subsection.

Source Code๏ƒ

smiles2helm
#!/usr/bin/env python3
# (C) 2026 Cadence Design Systems, Inc. (Cadence)
# All rights reserved.
# TERMS FOR USE OF SAMPLE CODE The software below ("Sample Code") is
# provided to current licensees or subscribers of Cadence products or
# SaaS offerings (each a "Customer").
# Customer is hereby permitted to use, copy, and modify the Sample Code,
# subject to these terms. Cadence claims no rights to Customer's
# modifications. Modification of Sample Code is at Customer's sole and
# exclusive risk. Sample Code may require Customer to have a then
# current license or subscription to the applicable Cadence offering.
# THE SAMPLE CODE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED.  OPENEYE DISCLAIMS ALL WARRANTIES, INCLUDING, BUT
# NOT LIMITED TO, WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. In no event shall Cadence be
# liable for any damages or liability in connection with the Sample Code
# or its use.

"""Convert SMILES string to HELM."""

import argparse
import json
import os
import pathlib
import sys

import rich.console
from openeye import oechem
from rich_argparse import HelpPreviewAction, RichHelpFormatter

__SCRIPT_NAME__ = pathlib.Path(__file__).absolute().stem
__SCRIPT_DESC__ = "Convert SMILES string to HELM."
__SCRIPT_TOOLKITS__ = ["oechem"]
__SCRIPT_KEYWORDS__ = ["HELM", "SMILES", "peptide", "peptide-informatics"]
__SCRIPT_CATEGORIES__ = ["peptide-informatics"]


def parse_options() -> argparse.Namespace:
    """Set up command line options."""
    parser = argparse.ArgumentParser(
        add_help=True,
        formatter_class=RichHelpFormatter,
        description="[yellow]" + __SCRIPT_DESC__ + "[/yellow]",
    )

    input_group = parser.add_argument_group("Input options")
    input_group.add_argument(
        "--smiles",
        metavar="SMILES-STRING",
        type=str,
        required=True,
        help="input SMILES string",
    )

    monomers_group = parser.add_argument_group("Monomer set options")
    _add_monomer_collection(monomers_group)

    helm_gen_group = parser.add_argument_group("HELM generation options")
    _add_helm_generation_options(helm_gen_group)

    parser.add_argument("--help-image", action=HelpPreviewAction)
    parser.add_argument(
        "--save-console-svg",
        default=False,
        action="store_true",
        help=f"run command and capture console output in {__SCRIPT_NAME__}.svg file",
    )
    return parser.parse_args()


def main() -> int:
    """Convert SMILES string to HELM."""
    args = parse_options()
    console = rich.console.Console(record=args.save_console_svg)

    mol = oechem.OEGraphMol()
    if not oechem.OESmilesToMol(mol, args.smiles):
        console.print("[red]Warning: Invalid SMILES string![/red]")
        return os.EX_DATAERR

    monomers = _get_monomer_collection(args)
    code_set = args.code_set or monomers.GetPrimaryCodeSet()
    options = oechem.OEHelmGenerationOptions(code_set)
    options.SetAllowUnspecifiedStereo(args.allow_unspecified_stereo)
    options.SetAllowUnmatchedFragments(args.allow_unmatched_fragments)
    result = oechem.OEHelmGenerationResult()
    if not (helm := oechem.OEMolToHelm(mol, monomers, options, result)):
        console.print(f"[red]Warning: {result.GetWarning()}[/red]")
    else:
        console.print(helm, markup=False, highlight=False)

    if args.save_console_svg:
        console.save_svg(f"{__SCRIPT_NAME__}.svg", title="output")
    return os.EX_OK


class MonomerSetParameter:  # noqa: PLW1641
    """Utility class to handle both built-in and user defined monomer sets."""

    def __init__(self) -> None:  # noqa: D107
        self._monomer_sets = ["Standard", "OpenEye", "JSON-FILENAME"]

    def __repr__(self) -> str:  # noqa: D105
        return ",".join(self._monomer_sets)

    def __eq__(self, param: object) -> bool:  # noqa: D105
        if not isinstance(param, str):
            return False
        if param in ["Standard", "OpenEye"]:
            return True

        console = rich.console.Console()
        monomer_set_filepath = pathlib.Path(param)
        if (
            not monomer_set_filepath.exists()
            or monomer_set_filepath.suffix.lower() != ".json"
        ):
            console.print(f"[red]Invalid monomer set file '{param}' ![/red]")
            return False
        try:
            with monomer_set_filepath.open("r") as json_file:
                json.load(json_file)
        except json.JSONDecodeError as e:
            console.print(f"[red]Invalid monomer set file '{param}' ![/red]")
            console.print(f"[red]Error decoding JSON: {e} ![/red]")
            return False
        return True


def _add_monomer_collection(arg_group: argparse._ArgumentGroup) -> None:
    arg_group.add_argument(
        "-m",
        "--monomers",
        type=str,
        default="Standard",
        choices=[MonomerSetParameter()],
        help="built-in monomer-set type or json file of monomers",
    )
    arg_group.add_argument(
        "--code-set",
        type=str,
        metavar="CODE-SET",
        required=False,
        default=None,
        help="code-set, if not specified primary code-set is used",
    )


def _get_monomer_collection(args: argparse.Namespace) -> oechem.OEMonomerSet:
    monomers = oechem.OEMonomerSet()
    match args.monomers:
        case "Standard":
            oechem.OELoadStandardMonomerSet(monomers)
        case "OpenEye":
            oechem.OELoadOpenEyeMonomerSet(monomers)
        case _:
            oechem.OEReadMonomerSet(monomers, args.monomers)
    return monomers


def _add_helm_generation_options(arg_group: argparse._ArgumentGroup) -> None:
    arg_group.add_argument(
        "--allow-unspecified-stereo",
        default=False,
        action="store_true",
        help="allow unspecified stereo in input molecule (default: %(default)s)",
    )
    arg_group.add_argument(
        "--allow-unmatched-fragments",
        default=False,
        action="store_true",
        help="allow unmatched fragments in input molecule -- embedded SMILES in HELM (default: %(default)s)",
    )


setattr(main, "__SCRIPT_NAME__", __SCRIPT_NAME__)
setattr(main, "__SCRIPT_DESC__", __SCRIPT_DESC__)
setattr(main, "__SCRIPT_TOOLKITS__", __SCRIPT_TOOLKITS__)
setattr(main, "__SCRIPT_KEYWORDS__", __SCRIPT_KEYWORDS__)
setattr(main, "__SCRIPT_CATEGORIES__", __SCRIPT_CATEGORIES__)

if __name__ == "__main__":
    sys.exit(main())

Solution๏ƒ

The OEHelmToMol function can take a HELM string and a monomer set (OEMonomerSet) and convert it into a molecule representation.

    mol = oechem.OEGraphMol()
    if not oechem.OESmilesToMol(mol, args.smiles):
        console.print("[red]Warning: Invalid SMILES string![/red]")
        return os.EX_DATAERR

    monomers = _get_monomer_collection(args)
    code_set = args.code_set or monomers.GetPrimaryCodeSet()
    options = oechem.OEHelmGenerationOptions(code_set)
    options.SetAllowUnspecifiedStereo(args.allow_unspecified_stereo)
    options.SetAllowUnmatchedFragments(args.allow_unmatched_fragments)
    result = oechem.OEHelmGenerationResult()
    if not (helm := oechem.OEMolToHelm(mol, monomers, options, result)):
        console.print(f"[red]Warning: {result.GetWarning()}[/red]")
    else:
        console.print(helm, markup=False, highlight=False)

Usage๏ƒ

See Download section to download the script.

> smiles2helm --help
../_images/smiles2helm-help.svg

By default, the smiles2helm script uses OEChem TKโ€™s built-in Standard monomer set for the conversion.

> smiles2helm --smiles 'CC[C@H](C)[C@@H](C(=O)N[C@@H](CC(=O)O)C(=O)N[C@@H](CCC(=O)O)C(=O)O)NC(=O)[C@H]([C@@H](C)O)NC(=O)[C@@H]1CCCN1C(=O)[C@H](CCC(=O)O)NC(=O)[C@@H]2CCCN2'
../_images/smiles2helm-01-stdout.svg

If the sequence generation followed by HELM encoding fails, the smiles2helm will print a warning message.

> smiles2helm --smiles 'CC[C@H](C)[C@@H](C(=O)N[C@@H](CC(=O)O)C(=O)N[C@H](CCC(=O)O)C(=O)O)NC(=O)[C@H]([C@@H](C)O)NC(=O)[C@@H]1CCCN1C(=O)[C@H](CCC(=O)O)NC(=O)[C@@H]2CCCN2'
../_images/smiles2helm-02-stdout.svg

Monomer Set Options๏ƒ

--monomers OpenEye๏ƒ
--code-set CODE-SET๏ƒ

OEChem TKโ€™s built-in OpenEye monomer set can be used with the --monomers OpenEye parameter.

> smiles2helm --monomers OpenEye --smiles 'CC[C@H](C)[C@@H](C(=O)N[C@@H](CC(=O)O)C(=O)N[C@H](CCC(=O)O)C(=O)O)NC(=O)[C@H]([C@@H](C)O)NC(=O)[C@@H]1CCCN1C(=O)[C@H](CCC(=O)O)NC(=O)[C@@H]2CCCN2'
../_images/smiles2helm-03-stdout.svg

Since the OpenEye monomer set contains multiple code-sets (OpenEye - default, Standard, PDB, ChEMBL), the --code-set parameter can be used to generate different versions of the HELM string.

> smiles2helm --monomers OpenEye --code-set ChEMBL --smiles 'CC[C@H](C)[C@@H](C(=O)N[C@@H](CC(=O)O)C(=O)N[C@H](CCC(=O)O)C(=O)O)NC(=O)[C@H]([C@@H](C)O)NC(=O)[C@@H]1CCCN1C(=O)[C@H](CCC(=O)O)NC(=O)[C@@H]2CCCN2'
../_images/smiles2helm-04-stdout.svg
> smiles2helm --monomers OpenEye --code-set PDB --smiles 'CC[C@H](C)[C@@H](C(=O)N[C@@H](CC(=O)O)C(=O)N[C@H](CCC(=O)O)C(=O)O)NC(=O)[C@H]([C@@H](C)O)NC(=O)[C@@H]1CCCN1C(=O)[C@H](CCC(=O)O)NC(=O)[C@@H]2CCCN2'
../_images/smiles2helm-05-stdout.svg
--monomers JSON-MONOMER-FILE๏ƒ

The following example shows how to generate a HELM string with a custom monomer set defined in a json file (custom-monomers.json)

> smiles2helm --smiles 'C[C@H]1C(=O)N[C@@H](CSSC[C@@H](C(=O)N2CCC[C@H]2C(=O)N[C@H](C(=O)N1)Cc3ccc(cc3)F)N)C(=O)O' --monomers custom-monomers.json
../_images/smiles2helm-06-stdout.svg

Helm Generation Options๏ƒ

--allow-unspecified-stereo๏ƒ
--allow-unmatched-fragments๏ƒ

The success of the sequence generation and HELM encoding process relies heavily on the choice of monomer set and the specific options used during the conversion.

Structure A in Table 1 and Table 2 is an example where PEPTIDE1{S.P.C}$$$$ can be generated successfully with multiple monomer sets which contain definition of the 20 standard amino acids.

The sequence of Structure B contains a D-amino acid. The HELM generation will fail when using only the standard monomers for sequence generation (see Table 1). In this case HELM can only be generated when the --allow-unmatched-fragments option is turned on that allows to encode unmapped fragment as an embedded SMILES in the generated HELM. The OpenEye monomer set can handle D-amino acids and therefore can be used to generate a HELM for this structure by default (see Table 2).

Generating a HELM for Structure C with unspecified stereo can be achieved by the --allow-unspecified-stereo option. Since OEChem TK currently does not allow the use of monomers with unspecified atom or bond stereo, input molecules should have a fully defined stereo configuration in order for monomers to be successfully mapped to them in the sequence generation phase. By default, any structure with unspecified stereo automatically fails. However, stereo check can be turned off with the --allow-unspecified-stereo option. Combining this option with the --allow-unmatched-fragments will allow to generate a HELM where embedded SMILES define the unmapped fragment with unspecified stereo.

Table 1. HELM Generation Options with Standard monomer set๏ƒ

Structure A

Structure B (D-amino acid)

Structure C (unspecified stereo)

../_images/helm-generation-Standard-A.svg ../_images/helm-generation-Standard-B.svg ../_images/helm-generation-Standard-C.svg

default options

PEPTIDE1{S.P.C}$$$$

No match for [R1]N1CCC[C@@H]1C(=O)[R2]!

Unspecified atom stereo in molecule

handle stereo [1]

PEPTIDE1{S.P.C}$$$$

No match for [R1]N1CCC[C@@H]1C(=O)[R2]!

No match for [R1]N1CCCC1C(=O)[R2]!

handle stereo and
unmatched [2]

PEPTIDE1{S.P.C}$$$$

PEPTIDE1{S.[[R1]N1CCC[C@@H]1C(=O)[R2]].C}$$$$

PEPTIDE1{S.[[R1]N1CCCC1C(=O)[R2]].C}$$$$

Table 2. HELM Generation Options with OpenEye monomer set๏ƒ

Structure A

Structure B (D-amino acid)

Structure C (unspecified stereo)

../_images/helm-generation-OpenEye-A.svg ../_images/helm-generation-OpenEye-B.svg ../_images/helm-generation-OpenEye-C.svg

default options [3]

PEPTIDE1{S.P.C}$$$$

PEPTIDE1{S.[dPro].C}$$$$

Unspecified atom stereo in molecule

handle stereo [4]

PEPTIDE1{S.P.C}$$$$

PEPTIDE1{S.[dPro].C}$$$$

No match for [R1]N1CCCC1C(=O)[R2]!

handle stereo and
unmatched [5]

PEPTIDE1{S.P.C}$$$$

PEPTIDE1{S.[dPro].C}$$$$

PEPTIDE1{S.[[R1]N1CCCC1C(=O)[R2]].C}$$$$

See also in OEChem TK manual๏ƒ

API