🆕 Convert Molecule File to HELM File

Problem

You want to convert a molecule file into a HELM file.

See also

Ingredients

Difficulty Level

🌶️ 🌶️

Download

Download code

mols2helms.py

See also Usage subsection.

Source Code

mols2helms
#!/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 a molecule file into a HELM file."""

import argparse
import collections
import json
import os
import pathlib
import sys

import rich.console
from openeye import oechem
from rich.progress import BarColumn, Progress, TextColumn
from rich_argparse import HelpPreviewAction, RichHelpFormatter

__SCRIPT_NAME__ = pathlib.Path(__file__).absolute().stem
__SCRIPT_DESC__ = "Convert molecule file into HELM file."
__SCRIPT_TOOLKITS__ = ["oechem"]
__SCRIPT_KEYWORDS__ = ["HELM", "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]",
    )

    io_group = parser.add_argument_group("Input/Output options")
    io_group.add_argument(
        "--mol",
        "--mol-file",
        metavar="MOL-FILE",
        type=str,
        required=True,
        help="input molecule file",
    )
    io_group.add_argument(
        "--helm",
        "--helm-file",
        metavar="HELM-FILE",
        type=str,
        required=True,
        help="outout file of HELM string",
    )
    io_group.add_argument(
        "--failures",
        metavar="MOL-FILE",
        type=str,
        required=False,
        help="output file for failed HELM generation (required: %(required)s)",
    )

    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)

    verbose_group = parser.add_argument_group("Verbose options")
    verbose_group.add_argument(
        "--show-summary",
        default=False,
        action="store_true",
        help="show summary of HELM generation attempts and unmapped fragments (default: %(default)s)",
    )

    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 molecule file to HELM file."""
    args = parse_options()
    console = rich.console.Console(record=args.save_console_svg)

    monomers = _get_monomer_collection(args)
    code_set = args.code_set or monomers.GetPrimaryCodeSet()

    helm_filepath = pathlib.Path(args.helm)
    if helm_filepath.suffix.lower() != ".helm":
        oechem.OEThrow.Fatal("Invalid file extension expected .helm!")

    failures_ofs: oechem.oemolostream | None = None
    if args.failures:
        failures_ofs = oechem.oemolostream()
        if not failures_ofs.open(args.failures):
            console.print(
                f"[red]Error: Unable to open failures file '{args.failures}'![/red]"
            )
            return os.EX_DATAERR

    options = oechem.OEHelmGenerationOptions(code_set)
    options.SetAllowUnspecifiedStereo(args.allow_unspecified_stereo)
    options.SetAllowUnmatchedFragments(args.allow_unmatched_fragments)

    mol_database = oechem.OEMolDatabase()
    if not mol_database.Open(args.mol):
        console.print(f"[red]Error: Unable to open molecule file '{args.mol}'![/red]")
        return os.EX_DATAERR

    unmapped_fragments: list[str] = []
    return_codes: list[int] = []
    num_failures = 0
    num_molecules = mol_database.NumMols()

    with (
        Progress(
            TextColumn("{task.description}"),
            BarColumn(bar_width=60),
            TextColumn("{task.percentage:3.1f}%"),
            transient=True,
            disable=num_molecules < 10,  # noqa: PLR2004
            console=console,
        ) as progress,
        helm_filepath.open("w") as helm_file,
    ):
        conversion = progress.add_task("[blue]HELM generation", total=num_molecules)
        for idx in range(num_molecules):
            mol = oechem.OEGraphMol()
            if not mol_database.GetMolecule(mol, idx):
                console.print(
                    f"[red]Error: Unable to get molecule at index {idx}![/red]"
                )
                continue
            result = oechem.OEHelmGenerationResult()
            if helm := oechem.OEMolToHelm(mol, monomers, options, result):
                helm_file.write(helm + "\n")
            else:
                num_failures += 1
                unmapped_fragments.extend(list(result.GetUnmappedFragments()))
                if failures_ofs is not None:
                    oechem.OEWriteMolecule(failures_ofs, mol)

            return_codes.append(result.GetReturnCode())

            progress.update(conversion, advance=1)

    num_successes = mol_database.NumMols() - num_failures
    num_monomers = oechem.OECount(monomers, oechem.OEIsInMonomerCodeSet(code_set))
    console.print(
        f"[yellow]HELM generation completed with {num_failures} failures and {num_successes} successes "
        f"using {num_monomers} monomers from `{code_set}` code-set.[/yellow]"
    )
    if args.show_summary:
        _show_summary(return_codes, unmapped_fragments, console)

    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)",
    )


def _show_summary(
    return_codes: list[int],
    unmapped_fragments: list[str],
    console: rich.console.Console,
) -> None:
    return_code_map = {
        oechem.OEHelmGenerationReturnCode_Success: "[green]success[/green]",
        oechem.OEHelmGenerationReturnCode_UnspecifiedAtomStereo: "[yellow]unspecified atom stereo[/yellow]",
        oechem.OEHelmGenerationReturnCode_UnspecifiedBondStereo: "[yellow]unspecified bond stereo[/yellow]",
        oechem.OEHelmGenerationReturnCode_UnmappedFragments: "[yellow]unmapped fragments[/yellow]",
        oechem.OEHelmGenerationReturnCode_NoSequence: "[red]no sequence[/red]",
    }
    for code, message in return_code_map.items():
        count = return_codes.count(code)
        if count != 0:
            console.print(
                f"{count:5d} ({count / len(return_codes) * 100:5.2f}%) {message}"
            )

    # summarize unmapped fragments
    if not unmapped_fragments:
        return

    fragment_counts = collections.Counter(unmapped_fragments)
    total = sum(fragment_counts.values())

    console.print("\n[yellow]Unmapped fragments summary:[/yellow]")
    for frag, count in fragment_counts.most_common():
        console.print(f"{count:5d} ({count / total * 100:5.2f}%) {frag}")


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())

Usage

See Download section to download the script.

> mols2helms --help
../_images/mols2helms-help.svg

By default, the mols2helms script uses OEChem TK’s built-in Standard monomer set for the conversions (peptides.ism).

> mols2helms --mol peptides.ism --helm peptides.helm

The script will display the number of successful and failed conversions (see also: --show-summary option for displaying more details).

../_images/mols2helms-01-stdout.svg

The command generates a peptides.helm file containing the HELM strings for the three peptides whose sequences can be fully described using only the 20 standard amino acids. Conversion attempts for all other peptides are reported as failures when using the default monomer set and options.

PEPTIDE1{P.E.P.T.I.D.E}$$$$
PEPTIDE1{M.E.D.I.C.I.N.E}$$$$
PEPTIDE1{E.M.E.D.I.C.I.N}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$
--failures MOL-FILE

The --failures option can be used to write out the molecules to a file for which no valid HELM string could be generated.

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 (peptides.ism).

> mols2helms --mol peptides.ism --helm peptides.helm --monomers OpenEye
../_images/mols2helms-02-stdout.svg

converts (peptides.ism) to (peptides.helm)

PEPTIDE1{P.E.P.T.I.D.E}$$$$
PEPTIDE1{[dPro].[dGlu].[dPro].[dThr].[dIle].[dAsp].[dGlu]}$$$$
PEPTIDE1{[Cha].I.R}$$$$
PEPTIDE1{M.E.D.I.C.I.N.E}$$$$
PEPTIDE1{E.M.E.D.I.C.I.N}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$
PEPTIDE1{[4FPhe].E.M.E.D.I.C.I}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$
PEPTIDE1{[App].L.E}$$$$
PEPTIDE1{Y.[dLeu].[dLeu].L.[dLeu].P}$PEPTIDE1,PEPTIDE1,1:R1-6:R2$$$

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.

> mols2helms --mol peptides.ism --helm peptides.helm --monomers OpenEye --code-set ChEMBL
../_images/mols2helms-03-stdout.svg

converts (peptides.ism) to (peptides.helm)

PEPTIDE1{P.E.P.T.I.D.E}$$$$
PEPTIDE1{[dP].[dE].[dP].[dT].[dI].[dD].[dE]}$$$$
PEPTIDE1{[Cha].I.R}$$$$
PEPTIDE1{M.E.D.I.C.I.N.E}$$$$
PEPTIDE1{E.M.E.D.I.C.I.N}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$
PEPTIDE1{[X3].E.M.E.D.I.C.I}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$
PEPTIDE1{Y.[dL].[dL].L.[dL].P}$PEPTIDE1,PEPTIDE1,1:R1-6:R2$$$
--monomers JSON-MONOMER-FILE

The following example shows how to convert a HELM file that uses custom monomers defined in a json file (custom-monomers.json)

> mols2helms --mol peptides.ism --helm peptides.helm --monomers custom-monomers.json
../_images/mols2helms-04-stdout.svg

converts (peptides.ism) to (peptides.helm)

PEPTIDE1{[Pro].[Glu].[Pro].[Thr].[Ile].[Asp].[Glu]}$$$$
PEPTIDE1{[Met].[Glu].[Asp].[Ile].[Cys].[Ile].[Asn].[Glu]}$$$$
PEPTIDE1{[Glu].[Met].[Glu].[Asp].[Ile].[Cys].[Ile].[Asn]}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$
PEPTIDE1{[Phe(4-F)].[Glu].[Met].[Glu].[Asp].[Ile].[Cys].[Ile]}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$

Verbose Options

--show-summary
> mols2helms --mol peptides.ism --helm peptides.helm --monomers OpenEye --show-summary
../_images/mols2helms-05-stdout.svg

Helm Generation Options

--allow-unspecified-stereo
--allow-unmatched-fragments

In the previous example, three molecules could not be converted into HELM representations due to either having unspecified stereo or unmatched fragments. The --allow-unspecified-stereo and --allow-unmatched-fragments options can be used to relax these constraints and generate HELM representations for these molecules with embedded SMILES encoding unmatched fragments. See also Helm Generation Options section of smiles2helm for more explanation and examples.

> mols2helms --mol peptides.ism --helm peptides.helm --monomers OpenEye --allow-unspecified-stereo --allow-unmatched-fragments --show-summary
../_images/mols2helms-06-stdout.svg

converts (peptides.ism) to (peptides.helm)

 1PEPTIDE1{P.E.P.T.I.D.E}$$$$
 2PEPTIDE1{[dPro].[dGlu].[dPro].[dThr].[dIle].[dAsp].[dGlu]}$$$$
 3PEPTIDE1{[Cha].I.R}$$$$
 4PEPTIDE1{M.E.D.I.C.I.N.E}$$$$
 5PEPTIDE1{E.M.E.D.I.C.I.N}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$
 6PEPTIDE1{[4FPhe].E.M.E.D.I.C.I}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$
 7PEPTIDE1{N.E.M.E.D.I.C.[[R1]N[C@@H]([C@@H](C)C(C)C)C(=O)[R2]]}$PEPTIDE1,PEPTIDE1,1:R1-8:R2$$$
 8PEPTIDE1{[App].L.E}$$$$
 9PEPTIDE1{Y.[dLeu].[dLeu].L.[dLeu].P}$PEPTIDE1,PEPTIDE1,1:R1-6:R2$$$
10PEPTIDE1{[dLeu].[dLeu].P.Y.[dLeu].[[R1]NC(CC(C)C)C(=O)[R2]]}$PEPTIDE1,PEPTIDE1,1:R1-6:R2$$$
11PEPTIDE1{[dPro].[dLeu].[Tyr(Me)].P.L.[[R1]N[C@@H](C[C@@H](C)CCN)C(=O)[R2]]}$PEPTIDE1,PEPTIDE1,1:R1-6:R2$$$

Structure 07

Structure 10

Structure 11

../_images/peptide-07.svg ../_images/peptide-10.svg ../_images/peptide-11.svg

unmatched fragment

unspecified stereo; unmatched fragment

unmatched fragment

See also in OEChem TK manual

API