Skip to content

PyMOL cartoons and surfaces

quiltwright.pymol

Molecular cartoons and surfaces, via headless PyMOL

The representation :mod:pypdb2pov cannot write. pdb2pov has emitted atoms and bonds since 1993 -- spheres at van der Waals, covalent or CPK radii, optionally with bond cylinders -- and nothing else. There is no ribbon in it and never was.

Yet pov-scenes/porin/3porin.inc is a Richardson cartoon, because it was never pdb2pov output: its header says converted to POVRay V2.2 by WCVT2POV V2.7, so the ribbons came from some 1993-94 mesh exporter whose output survives in no repository. The most striking image in the archive is therefore the one representation the pipeline could not regenerate. This module regenerates it, from a PDB ID and a command line.

The contract is pdb2pov -o's, deliberately. What comes out is an object-only .inc: geometry centred on the origin, a #declare <name>_enclosing_radius beside it, no camera and no lights. That is the same shape :mod:pypdb2pov writes, which is what makes a cartoon drop into an existing scene unchanged::

#include "ompf_cartoon.inc"
Vitrine_Mount(ompf_cartoon, ompf_cartoon_enclosing_radius)

Vitrine_Mount asks nothing about how the geometry was produced, and neither :func:~quiltwright.povray.render_pov_quilt nor :class:~quiltwright.povray.PovCamera needs any change to render it.

Why the export needs post-processing. cmd.get_povray() emits one mesh2 object per triangle, each carrying its own three-entry vertex, normal and texture list. Nothing is shared. A GFP cartoon arrives as 17,140 single-face meshes and 9.3 MB; an OmpF porin trimer as 75,792 and 41.3 MB; an alpha-hemolysin heptamer as 152,596 and 83.1 MB -- and a quilt re-parses that 48 times. :func:~quiltwright.povgen.coalesce_mesh2 merges them into one mesh with shared lists, which is most of what this module does with what PyMOL hands it.

PyMOL is optional, and stays optional. Open-source PyMOL is not OSI-licensed -- Homebrew declares it LicenseRef-Homebrew-cannot-represent -- while quiltwright is BSD-3, so it can never be a hard dependency. It is also awkward to reach: PyPI has only ever published alphas of pymol-open-source, and the Homebrew build bundles its own interpreter that no project virtualenv can import from. So this module drives PyMOL in-process when it can import it and by subprocess when it cannot, chooses automatically, and reports which through :func:available.

Typical use::

from quiltwright.pymol import cartoon_inc

result = cartoon_inc("2omf.cif.gz", "ompf_cartoon.inc")
print(result.enclosing_radius, result.faces)

CartoonMeshResult(path, enclosing_radius, vertices, faces, backend) dataclass

What :func:cartoon_obj wrote.

Parameters:

Name Type Description Default
path Path

The .obj written.

required
enclosing_radius float

Radius of the sphere about the origin that contains the geometry, in angstroms -- identical to what :func:cartoon_inc would report for the same PyMOL export, since both are measured from the same mesh before either is recentred.

required
vertices int

Vertices in the emitted mesh, after deduplication.

required
faces int

Triangles in the emitted mesh.

required
backend str

"module" or "subprocess".

required

CartoonResult(path, identifier, enclosing_radius, centre, vertices, faces, rep, backend) dataclass

What :func:cartoon_inc wrote.

Parameters:

Name Type Description Default
path Path

The .inc written.

required
identifier str

The POV-Ray identifier declared in it.

required
enclosing_radius float

Radius of the sphere about the origin that contains the geometry, in angstroms. Also declared in the file as <identifier>_enclosing_radius, which is what a host scene should read rather than this.

required
centre tuple[float, float, float]

The model-space point moved to the origin, in angstroms.

required
vertices int

Vertices in the emitted mesh, after deduplication.

required
faces int

Triangles in the emitted mesh.

required
rep str

The representation exported.

required
backend str

"module" or "subprocess".

required

PyMolNotAvailable

Bases: RuntimeError

Raised when neither an importable PyMOL nor a pymol binary exists.

available()

How PyMOL can be reached from here, if at all.

Returns:

Type Description
str | None

"module" if import pymol works in this interpreter, "subprocess" if only a pymol binary is on PATH, else None. The distinction matters because the common install -- Homebrew -- bundles its own interpreter, so the binary exists while the import does not.

Source code in src/quiltwright/pymol.py
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
def available() -> str | None:
    """How PyMOL can be reached from here, if at all.

    :return: ``"module"`` if ``import pymol`` works in this interpreter,
        ``"subprocess"`` if only a ``pymol`` binary is on ``PATH``, else
        ``None``.  The distinction matters because the common install --
        Homebrew -- bundles its own interpreter, so the binary exists while
        the import does not.
    """
    try:
        import pymol  # noqa: F401  # ty: ignore[unresolved-import]
    except ImportError:
        pass
    else:
        return "module"
    return "subprocess" if shutil.which("pymol") else None

cartoon_inc(source, out, *, rep='cartoon', color='spectrum', finish='normal', selection='polymer', assembly='1', transparency=0.0, surface_quality=None, coalesce=True, name=None)

Export a structure as a POV-Ray object-only include.

Parameters:

Name Type Description Default
source str | Path

Anything PyMOL can load -- .pdb, .cif, .cif.gz.

required
out str | Path

Path of the .inc to write.

required
rep str

One of :data:REPRESENTATIONS.

'cartoon'
color str | None

"spectrum" for a rainbow ramp along the chain, "ss" for :data:SS_COLORS by secondary structure (cmd.dss() then one flat colour each for helix, strand and loop -- the conventional three-colour cartoon), any PyMOL colour name for a flat colour, or None to keep PyMOL's default colouring.

'spectrum'
finish str

"normal" for whatever finish PyMOL's own baked pigments get by default (POV-Ray's plain default -- flat, no reflection), or "metallic" for the vitrine's own brass recipe kept but tinted with each baked colour instead of brass yellow. Applied per baked colour, so color="ss" comes out as three metals, not one shared material.

'normal'
selection str

PyMOL selection to show; "polymer" drops waters and ligands, which is almost always what a cartoon wants.

'polymer'
assembly str

Biological assembly to load. "1" is the biological unit -- ferritin arrives as a 24-mer rather than as a 24th of itself. Pass "" for the asymmetric unit.

'1'
transparency float

0 to 1. Exports as POV-Ray transmit, which is flat see-through rather than refractive.

0.0
surface_quality int | None

PyMOL's surface_quality; lower is coarser. Worth setting negative for a large rep="surface", which otherwise runs to millions of triangles.

None
coalesce bool

Merge the per-triangle meshes. Leave it on unless you are comparing against the raw export.

True
name str | None

POV-Ray identifier to declare. Defaults to out's stem, put through :func:pov_identifier.

None

Returns:

Type Description
CartoonResult

A :class:CartoonResult.

Raises:

Type Description
PyMolNotAvailable

If PyMOL cannot be reached at all.

ValueError

If rep or finish is not a recognised value.

Source code in src/quiltwright/pymol.py
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
def cartoon_inc(
    source: str | Path,
    out: str | Path,
    *,
    rep: str = "cartoon",
    color: str | None = "spectrum",
    finish: str = "normal",
    selection: str = "polymer",
    assembly: str = "1",
    transparency: float = 0.0,
    surface_quality: int | None = None,
    coalesce: bool = True,
    name: str | None = None,
) -> CartoonResult:
    """Export a structure as a POV-Ray object-only include.

    :param source: Anything PyMOL can load -- ``.pdb``, ``.cif``, ``.cif.gz``.
    :param out: Path of the ``.inc`` to write.
    :param rep: One of :data:`REPRESENTATIONS`.
    :param color: ``"spectrum"`` for a rainbow ramp along the chain,
        ``"ss"`` for :data:`SS_COLORS` by secondary structure (``cmd.dss()``
        then one flat colour each for helix, strand and loop -- the
        conventional three-colour cartoon), any PyMOL colour name for a flat
        colour, or ``None`` to keep PyMOL's default colouring.
    :param finish: ``"normal"`` for whatever finish PyMOL's own baked
        pigments get by default (POV-Ray's plain default -- flat, no
        reflection), or ``"metallic"`` for the vitrine's own brass recipe
        kept but tinted with each baked colour instead of brass yellow.
        Applied per baked colour, so ``color="ss"`` comes out as three
        metals, not one shared material.
    :param selection: PyMOL selection to show; ``"polymer"`` drops waters and
        ligands, which is almost always what a cartoon wants.
    :param assembly: Biological assembly to load.  ``"1"`` is the biological
        unit -- ferritin arrives as a 24-mer rather than as a 24th of itself.
        Pass ``""`` for the asymmetric unit.
    :param transparency: 0 to 1.  Exports as POV-Ray ``transmit``, which is
        flat see-through rather than refractive.
    :param surface_quality: PyMOL's ``surface_quality``; lower is coarser.
        Worth setting negative for a large ``rep="surface"``, which otherwise
        runs to millions of triangles.
    :param coalesce: Merge the per-triangle meshes.  Leave it on unless you
        are comparing against the raw export.
    :param name: POV-Ray identifier to declare.  Defaults to *out*'s stem, put
        through :func:`pov_identifier`.
    :return: A :class:`CartoonResult`.
    :raises PyMolNotAvailable: If PyMOL cannot be reached at all.
    :raises ValueError: If *rep* or *finish* is not a recognised value.
    """
    if rep not in REPRESENTATIONS:
        raise ValueError(f"rep must be one of {REPRESENTATIONS}, got {rep!r}")
    if finish not in ("normal", "metallic"):
        raise ValueError(f"finish must be 'normal' or 'metallic', got {finish!r}")

    out = Path(out)
    identifier = pov_identifier(name if name is not None else out.stem)

    with tempfile.TemporaryDirectory() as work:
        body_out = str(Path(work) / "body.pov")
        meta_out = str(Path(work) / "meta.json")
        script = (
            _literals(
                SOURCE=str(Path(source).resolve()),
                REP=rep,
                COLOR=color,
                SS_COLORS=SS_COLORS,
                SELECTION=selection,
                ASSEMBLY=assembly,
                TRANSPARENCY=float(transparency),
                SURFACE_QUALITY=surface_quality,
                PULL_BACK=_PULL_BACK,
                BODY_OUT=body_out,
                META_OUT=meta_out,
            )
            + _EXPORT_SCRIPT
        )
        _run_export(script, body_out, meta_out)
        body = Path(body_out).read_text()
        meta = json.loads(Path(meta_out).read_text())

    backend = available() or "subprocess"
    mesh = coalesce_mesh2(body) if coalesce else body

    if "mesh2" in mesh:
        centre, radius, vertices = _measure(mesh, meta["pull_back"])
        faces = _count_faces(mesh)
        if finish != "normal":
            mesh = _retexture(mesh, finish)
    else:
        # sticks and spheres export as native primitives, which carry no
        # vertex list to measure.  Fall back to the atom extents, which are
        # exact for those since a sphere's extent is its centre plus radius.
        lo, hi = meta["extent"]
        centre = tuple((a + b) / 2.0 for a, b in zip(lo, hi, strict=True))
        radius = max(math.dist(corner, centre) for corner in (lo, hi))
        vertices = faces = 0

    out.parent.mkdir(parents=True, exist_ok=True)
    out.write_text(
        _wrap(
            mesh,
            identifier=identifier,
            centre=centre,
            radius=radius,
            z_shift=meta["pull_back"],
            meta=meta,
            rep=rep,
            source=Path(source).name,
            faces=faces,
        )
    )
    return CartoonResult(
        path=out,
        identifier=identifier,
        enclosing_radius=radius,
        centre=centre,
        vertices=vertices,
        faces=faces,
        rep=rep,
        backend=backend,
    )

cartoon_obj(source, out, *, rep='cartoon', color=None, roughness=0.3, selection='polymer', assembly='1', surface_quality=None)

Export a structure as a Wavefront OBJ -- the mesh twin of :func:cartoon_inc.

Runs the identical PyMOL export (same representation, selection and assembly) and coalesces it the same way, but writes the triangles as a plain .obj instead of a POV-Ray include, for feeding the same subject to :mod:quiltwright.cycles instead of :mod:quiltwright.povray. This is what makes a "which backend wins on a mesh this size" comparison honest: both start from the exact same PyMOL triangulation, not two independently-modelled scenes.

Geometry is centred on the origin exactly as :func:cartoon_inc centres it (undoing PyMOL's camera pull-back, then the same bounding-box recentring), so the enclosing radius here matches what that function would report for the same export.

No coordinate flip. With the identity view the export script sets, cmd.get_povray() emits the model's own right-handed coordinates, offset only by the view centre and the camera pull-back -- PyMOL's POV header compensates with a right-handed camera rather than by reflecting the geometry. :mod:quiltwright.cycles is right-handed too, so the OBJ is the model, recentred, with PyMOL's winding kept. (It is :func:cartoon_inc, bound for left-handed POV-Ray, that reflects z.) An earlier version negated z here on the belief that the export was already left-handed, and so rendered every structure as its enantiomer; tests/test_pymol.py now checks the mesh against the structure's own CA atoms. Also see quiltwright.cycles's obj importer, which must be told the file is already in this package's +z-up convention, or Blender's default Y-up-to-Z-up remap for Wavefront files turns this mesh 90 degrees against a camera framed for it.

Colour. Plain OBJ carries none, so by default (color=None) this writes geometry only, exactly as before. Passing a colour bakes it into a companion .mtl instead: color="ss" runs cmd.dss() and splits the mesh into the three flat colours of :data:SS_COLORS (helix, strand, loop -- the conventional three-colour cartoon), and any other PyMOL colour name or "#rrggbb" paints the whole thing one flat colour. Either way the colour PyMOL baked into the raw mesh is read back out of its own texture list rather than recomputed, so it is exactly what PyMOL applied. color="spectrum" is rejected: a rainbow ramp is one colour per residue, which would mean one OBJ material per residue -- unworkable, and exactly the case :func:cartoon_inc and the POV-Ray backend exist for.

Parameters:

Name Type Description Default
source str | Path

Anything PyMOL can load -- .pdb, .cif, .cif.gz.

required
out str | Path

Path of the .obj to write. A companion .mtl of the same stem is written alongside it when color is given.

required
rep str

One of :data:REPRESENTATIONS. Only representations that export as mesh triangles are usable here; sticks and spheres raise, since they carry no vertex list to convert (use :func:cartoon_inc and quiltwright's POV-Ray backend for those, or author them directly as :class:~quiltwright.povgen.Sphere / :class:~quiltwright.povgen.Cylinder primitives for either backend).

'cartoon'
color str | None

None for a colourless mesh (default), "ss" for :data:SS_COLORS by secondary structure, or any flat PyMOL colour name / "#rrggbb". "spectrum" raises -- see above.

None
roughness float

Blender Pr (roughness) written into the .mtl for every material, when color is given. Lower is glossier; Blender's own default is 0.5, which reads as flat and plasticky next to POV-Ray's finish -- this is what "more interesting material properties" means in practice for a Cycles render.

0.3
selection str

PyMOL selection to show; "polymer" drops waters and ligands.

'polymer'
assembly str

Biological assembly to load; "1" is the biological unit.

'1'
surface_quality int | None

PyMOL's surface_quality, for rep="surface".

None

Returns:

Type Description
CartoonMeshResult

A :class:CartoonMeshResult.

Raises:

Type Description
PyMolNotAvailable

If PyMOL cannot be reached at all.

ValueError

If rep is not a known representation, color is "spectrum", or the export produces native POV-Ray primitives rather than mesh triangles.

Source code in src/quiltwright/pymol.py
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
def cartoon_obj(
    source: str | Path,
    out: str | Path,
    *,
    rep: str = "cartoon",
    color: str | None = None,
    roughness: float = 0.3,
    selection: str = "polymer",
    assembly: str = "1",
    surface_quality: int | None = None,
) -> CartoonMeshResult:
    """Export a structure as a Wavefront OBJ -- the mesh twin of :func:`cartoon_inc`.

    Runs the identical PyMOL export (same representation, selection and
    assembly) and coalesces it the same way, but writes the triangles as a
    plain ``.obj`` instead of a POV-Ray include, for feeding the same
    subject to :mod:`quiltwright.cycles` instead of :mod:`quiltwright.povray`.
    This is what makes a "which backend wins on a mesh this size" comparison
    honest: both start from the exact same PyMOL triangulation, not two
    independently-modelled scenes.

    Geometry is centred on the origin exactly as :func:`cartoon_inc` centres
    it (undoing PyMOL's camera pull-back, then the same bounding-box
    recentring), so the enclosing radius here matches what that function
    would report for the same export.

    **No coordinate flip.**  With the identity view the export script sets,
    ``cmd.get_povray()`` emits the model's own right-handed coordinates,
    offset only by the view centre and the camera pull-back -- PyMOL's POV
    header compensates with a right-handed camera rather than by reflecting
    the geometry.  :mod:`quiltwright.cycles` is right-handed too, so the OBJ
    is the model, recentred, with PyMOL's winding kept.  (It is
    :func:`cartoon_inc`, bound for left-handed POV-Ray, that reflects *z*.)
    An earlier version negated *z* here on the belief that the export was
    already left-handed, and so rendered every structure as its enantiomer;
    ``tests/test_pymol.py`` now checks the mesh against the structure's own
    CA atoms.  Also see ``quiltwright.cycles``'s ``obj`` importer, which must
    be told the file is already in this package's +z-up convention, or
    Blender's default Y-up-to-Z-up remap for Wavefront files turns this mesh
    90 degrees against a camera framed for it.

    **Colour.**  Plain OBJ carries none, so by default (``color=None``) this
    writes geometry only, exactly as before.  Passing a colour bakes it into
    a companion ``.mtl`` instead: ``color="ss"`` runs ``cmd.dss()`` and
    splits the mesh into the three flat colours of :data:`SS_COLORS` (helix,
    strand, loop -- the conventional three-colour cartoon), and any other
    PyMOL colour name or ``"#rrggbb"`` paints the whole thing one flat
    colour.  Either way the colour PyMOL baked into the raw mesh is read back
    out of its own texture list rather than recomputed, so it is exactly what
    PyMOL applied.  ``color="spectrum"`` is rejected: a rainbow ramp is one
    colour per residue, which would mean one OBJ material per residue --
    unworkable, and exactly the case :func:`cartoon_inc` and the POV-Ray
    backend exist for.

    :param source: Anything PyMOL can load -- ``.pdb``, ``.cif``, ``.cif.gz``.
    :param out: Path of the ``.obj`` to write.  A companion ``.mtl`` of the
        same stem is written alongside it when *color* is given.
    :param rep: One of :data:`REPRESENTATIONS`.  Only representations that
        export as mesh triangles are usable here; ``sticks`` and ``spheres``
        raise, since they carry no vertex list to convert (use
        :func:`cartoon_inc` and quiltwright's POV-Ray backend for those, or
        author them directly as :class:`~quiltwright.povgen.Sphere` /
        :class:`~quiltwright.povgen.Cylinder` primitives for either backend).
    :param color: ``None`` for a colourless mesh (default), ``"ss"`` for
        :data:`SS_COLORS` by secondary structure, or any flat PyMOL colour
        name / ``"#rrggbb"``.  ``"spectrum"`` raises -- see above.
    :param roughness: Blender ``Pr`` (roughness) written into the ``.mtl``
        for every material, when *color* is given.  Lower is glossier;
        Blender's own default is ``0.5``, which reads as flat and plasticky
        next to POV-Ray's finish -- this is what "more interesting material
        properties" means in practice for a Cycles render.
    :param selection: PyMOL selection to show; ``"polymer"`` drops waters and
        ligands.
    :param assembly: Biological assembly to load; ``"1"`` is the biological
        unit.
    :param surface_quality: PyMOL's ``surface_quality``, for ``rep="surface"``.
    :return: A :class:`CartoonMeshResult`.
    :raises PyMolNotAvailable: If PyMOL cannot be reached at all.
    :raises ValueError: If *rep* is not a known representation, *color* is
        ``"spectrum"``, or the export produces native POV-Ray primitives
        rather than mesh triangles.
    """
    if rep not in REPRESENTATIONS:
        raise ValueError(f"rep must be one of {REPRESENTATIONS}, got {rep!r}")
    if color == "spectrum":
        raise ValueError(
            "color='spectrum' is not supported by cartoon_obj() -- a rainbow "
            "ramp is one colour per residue, which OBJ has no reasonable way "
            "to carry as materials. Use color='ss', a flat colour, or "
            "cartoon_inc() with the POV-Ray backend for a spectrum."
        )

    out = Path(out)
    with tempfile.TemporaryDirectory() as work:
        body_out = str(Path(work) / "body.pov")
        meta_out = str(Path(work) / "meta.json")
        script = (
            _literals(
                SOURCE=str(Path(source).resolve()),
                REP=rep,
                COLOR=color,
                SS_COLORS=SS_COLORS,
                SELECTION=selection,
                ASSEMBLY=assembly,
                TRANSPARENCY=0.0,
                SURFACE_QUALITY=surface_quality,
                PULL_BACK=_PULL_BACK,
                BODY_OUT=body_out,
                META_OUT=meta_out,
            )
            + _EXPORT_SCRIPT
        )
        _run_export(script, body_out, meta_out)
        body = Path(body_out).read_text()
        meta = json.loads(Path(meta_out).read_text())

    mesh = coalesce_mesh2(body)
    if "mesh2" not in mesh:
        raise ValueError(
            f"rep={rep!r} exports as native POV-Ray primitives, not mesh "
            "triangles, so there is nothing for cartoon_obj() to convert -- "
            "use cartoon_inc() and the POV-Ray backend for this rep, or "
            "author the same primitives directly for either backend"
        )

    centre, radius, _ = _measure(mesh, meta["pull_back"])
    cx, cy, cz = centre
    vertices = [
        (float(x) - cx, float(y) - cy, float(z) + meta["pull_back"] - cz)
        for block in _VERTEX_BLOCK.finditer(mesh)
        for x, y, z in _VEC.findall(block.group(1))
    ]
    faces = [
        (int(a), int(b), int(c))
        for block in _FACE_BLOCK.finditer(mesh)
        for a, b, c in _VEC.findall(block.group(1))
    ]

    labels: list[str] = []
    materials: dict[str, tuple[float, float, float]] = {}
    if color is not None:
        labels, materials = _face_materials(mesh, color, len(faces))

    out.parent.mkdir(parents=True, exist_ok=True)
    with out.open("w") as fh:
        fh.write(f"# {out.stem} -- {rep}, via quiltwright.pymol.cartoon_obj\n")
        fh.write(f"# {meta['atoms']} atoms, {len(vertices)} vertices, {len(faces)} faces\n")
        if materials:
            fh.write(f"mtllib {out.with_suffix('.mtl').name}\n")
        for x, y, z in vertices:
            fh.write(f"v {x:.6g} {y:.6g} {z:.6g}\n")
        if materials:
            for name in materials:
                fh.write(f"usemtl {name}\n")
                for (a, b, c), label in zip(faces, labels, strict=True):
                    if label == name:
                        fh.write(f"f {a + 1} {b + 1} {c + 1}\n")
        else:
            for a, b, c in faces:
                fh.write(f"f {a + 1} {b + 1} {c + 1}\n")

    if materials:
        with out.with_suffix(".mtl").open("w") as fh:
            for name, (r, g, b) in materials.items():
                fh.write(f"newmtl {name}\nKd {r:.4f} {g:.4f} {b:.4f}\nPr {roughness:.3f}\n\n")

    return CartoonMeshResult(
        path=out,
        enclosing_radius=radius,
        vertices=len(vertices),
        faces=len(faces),
        backend=available() or "subprocess",
    )

pov_identifier(stem, fallback='molecule')

Derive a legal POV-Ray identifier from an output path.

POV identifiers are alphanumerics and underscores and may not begin with a digit, so 2omf -- the obvious name for a structure -- becomes _2omf. Kept deliberately identical to pypdb2pov's function of the same name, so a cartoon and an atom scene of the same structure declare the same identifier and a host scene can swap one for the other.

Parameters:

Name Type Description Default
stem str

Output path or bare stem.

required
fallback str

Used when stem has no usable characters.

'molecule'

Returns:

Type Description
str

A legal identifier.

Source code in src/quiltwright/pymol.py
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
def pov_identifier(stem: str, fallback: str = "molecule") -> str:
    """Derive a legal POV-Ray identifier from an output path.

    POV identifiers are alphanumerics and underscores and may not begin with a
    digit, so ``2omf`` -- the obvious name for a structure -- becomes
    ``_2omf``.  Kept deliberately identical to ``pypdb2pov``'s function of the
    same name, so a cartoon and an atom scene of the same structure declare
    the same identifier and a host scene can swap one for the other.

    :param stem: Output path or bare stem.
    :param fallback: Used when *stem* has no usable characters.
    :return: A legal identifier.
    """
    base = Path(stem).name
    for suffix in (".pov", ".inc"):
        if base.lower().endswith(suffix):
            base = base[: -len(suffix)]
            break
    if not base:
        base = fallback
    if base[0].isdigit():
        base = "_" + base
    return "".join(ch if (ch.isalnum() and ch.isascii()) or ch == "_" else "_" for ch in base)