🆕 Convert Peptides File to XLSX (Excel) with Monomer Sequence

Problem

You want to convert peptides into molecule sequences and export them to an xlsx Excel file.

../_images/peptides2xlsx-screenshot.png

Figure 1. Example of XLSX generated with peptides2xlsx

See also

Ingredients

Difficulty Level

🌶️ 🌶️

Download

Download code

peptides2xlsx.py

See also Usage subsection.

Source Code

peptides2xlsx
#!/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 peptide file into EXCEL XLSX file."""

import argparse
import json
import os
import pathlib
import sys
from typing import NamedTuple

import rich.console
import xlsxwriter
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 peptide file into EXCEL XLSX file."
__SCRIPT_TOOLKITS__ = ["oechem"]
__SCRIPT_KEYWORDS__ = ["HELM", "peptide", "sequence", "XLSX", "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(
        "--mol",
        metavar="MOL-FILE",
        type=str,
        required=False,
        help="input molecule file of peptides (oeb, sdf, fasta)",
    )

    # output options
    output_group = parser.add_argument_group("Output options")
    output_group.add_argument(
        "--xlsx",
        type=str,
        required=True,
        metavar="XLSX-FILE",
        help="output XLSX file (.xlsx)",
    )

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


class Sequence(NamedTuple):
    """Represents a peptide sequence."""

    title: str
    helm: str
    warning: str
    monomer_sequence: list[str]


def main() -> int:
    """Convert peptide file into EXCEL XLSX 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()

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

    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

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

    sequences: list[Sequence] = _generate_sequences(
        mol_database, monomers, options, console
    )
    num_warnings = sum(1 for s in sequences if s.warning)
    console.print(
        f"{len(sequences)} molecules have been read from {pathlib.Path(args.mol).name} with {num_warnings} HELM generation issues!"
    )

    write_xlsx_file(args.xlsx, sequences, monomers, code_set)
    if args.save_console_svg:
        console.save_svg(f"{__SCRIPT_NAME__}.svg", title="output")

    return os.EX_OK


def _get_monomer_analog_color(monomer: oechem.OEMonomer) -> str:  # noqa: PLR0911
    if monomer.GetPolymerType() != oechem.OEPolymerType_Peptide:
        return "#000000"
    analog = oechem.OEGetStandardAnalog(monomer.GetCanonicalSmiles())
    if analog == oechem.OEResidueIndex_UNK:
        return "#AAAAAA"
    match analog:
        case oechem.OEResidueIndex_CYS | oechem.OEResidueIndex_MET:
            return "#e4e488"
        case (
            oechem.OEResidueIndex_ALA
            | oechem.OEResidueIndex_GLY
            | oechem.OEResidueIndex_ILE
            | oechem.OEResidueIndex_LEU
            | oechem.OEResidueIndex_PRO
            | oechem.OEResidueIndex_VAL
        ):
            return "#c09071"
        case (
            oechem.OEResidueIndex_PHE
            | oechem.OEResidueIndex_TRP
            | oechem.OEResidueIndex_TYR
        ):
            return "#5faf5f"
        case oechem.OEResidueIndex_ASP | oechem.OEResidueIndex_GLU:
            return "#e0ac70"
        case (
            oechem.OEResidueIndex_ARG
            | oechem.OEResidueIndex_HIS
            | oechem.OEResidueIndex_LYS
        ):
            return "#85b4e6"
        case oechem.OEResidueIndex_SER | oechem.OEResidueIndex_THR:
            return "#ff8787"
        case oechem.OEResidueIndex_ASN | oechem.OEResidueIndex_GLN:
            return "#5493EA"
    return "#AAAAAA"


def write_xlsx_file(
    output_filename: str,
    sequences: list[Sequence],
    monomers: oechem.OEMonomerSet,
    code_set: str,
) -> None:
    """Write peptide sequences to an XLSX file."""
    workbook = xlsxwriter.Workbook(output_filename)
    worksheet = workbook.add_worksheet()

    head_format = _add_head_format(workbook)
    monomer_formats: dict[str, xlsxwriter.format] = _add_monomer_formats(
        workbook, sequences, monomers, code_set
    )

    # generate header
    row, col = 0, 0
    worksheet.set_row(row, None, head_format)
    worksheet.set_column(col, col, 30)
    worksheet.write(row, col, "Molecule Titles")
    max_sequence_length = max(len(s.monomer_sequence) for s in sequences)
    for pos in range(1, max_sequence_length + 1):
        worksheet.write(row, pos, f"P{pos}")

    has_warnings = any(seq.warning for seq in sequences)
    if has_warnings:
        cell_format = workbook.add_format(
            {
                "color": "red",
                "bold": True,
                "align": "left",
                "valign": "vcenter",
                "size": 18,
            }
        )
        worksheet.write(row, max_sequence_length + 1, "Warnings", cell_format)
        worksheet.set_column(max_sequence_length + 1, max_sequence_length + 2, 100)

    for seq in sequences:
        row += 1
        worksheet.write(row, col, seq.title)
        for pos, monomer_code in enumerate(seq.monomer_sequence, start=1):
            monomer_format = monomer_formats.get(monomer_code)
            worksheet.write(row, pos, monomer_code, monomer_format)
        if seq.warning:
            worksheet.write(row, max_sequence_length + 1, seq.warning)

    workbook.close()


def _add_head_format(
    workbook: xlsxwriter.Workbook,
) -> xlsxwriter.format:
    """Add header format to the workbook."""
    head_format = workbook.add_format(
        {"bold": True, "align": "center", "valign": "vcenter", "size": 18}
    )
    head_format.set_bg_color("#F4F4F4")
    head_format.set_border_color("#DDDDDD")
    head_format.set_border()

    return head_format


def _add_monomer_formats(
    workbook: xlsxwriter.Workbook,
    sequences: list[Sequence],
    monomers: oechem.OEMonomerSet,
    code_set: str,
) -> dict[str, xlsxwriter.format]:
    """Add monomer formats to the workbook."""
    unique_monomer_codes: set[str] = {c for s in sequences for c in s.monomer_sequence}

    monomer_colors: dict[str, xlsxwriter.format] = {}
    for code in unique_monomer_codes:
        if (monomer := monomers.GetMonomer(code_set, code)) is not None:
            color = _get_monomer_analog_color(monomer)

            data_format = workbook.add_format(
                {"bold": False, "align": "center", "valign": "vcenter", "size": 12}
            )
            data_format.set_bg_color(color)
            data_format.set_shrink()
            monomer_colors[code] = data_format

    return monomer_colors


def _generate_sequences(
    mol_database: oechem.OEMolDatabase,
    monomers: oechem.OEMonomerSet,
    options: oechem.OEHelmGenerationOptions,
    console: rich.console.Console,
) -> list[Sequence]:
    sequences: list[Sequence] = []
    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,
    ):
        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):
                monomer_sequence = oechem.OEGetHelmMonomerCodes(helm, "PEPTIDE1")
                sequences.append(Sequence(mol.GetTitle(), helm, "", monomer_sequence))
            else:
                sequences.append(Sequence(mol.GetTitle(), "", result.GetWarning(), []))
            progress.update(conversion, advance=1)
    return sequences


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

Usage

See Download section to download the script.

> peptides2xlsx --help
../_images/peptides2xlsx-help.svg

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

> peptides2xlsx --mol peptides.ism --xlsx peptides.xlsx

The script will display the number of successful and failed conversions.

../_images/peptides2xlsx-01-stdout.svg

The command generates a peptides.xlsx 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.

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

> peptides2xlsx --mol peptides.ism --xlsx peptides.xlsx --monomers OpenEye
../_images/peptides2xlsx-02-stdout.svg

converts (peptides.ism) to (peptides.xlsx)

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.

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

converts (peptides.ism) to (peptides.xlsx)

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

> peptides2xlsx --mol peptides.ism --xlsx peptides.xlsx --monomers custom-monomers.json
../_images/peptides2xlsx-04-stdout.svg

converts (peptides.ism) to (peptides.xlsx)

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.

> peptides2xlsx --mol peptides.ism --xlsx peptides.xlsx --monomers OpenEye --allow-unspecified-stereo --allow-unmatched-fragments
../_images/peptides2xlsx-05-stdout.svg

converts (peptides.ism) to (peptides.xlsx)

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