molforge.folding¶
folding ¶
Data types for multi-component structure prediction.
molforge's single-sequence folding interface
(:meth:FoldingEngine.predict) covers the bread-and-butter case of
"predict the structure of one protein chain." For the AlphaFold-3-
class engines :class:Boltz and :class:Chai1, that's a small
fraction of what the underlying models can do — both engines fold
complexes of multiple proteins, nucleic acids, and small-molecule
ligands in a single forward pass. Modelling those complexes is the
headline drug-discovery and structural-biology use case.
This module provides the shared input shape for multi-component prediction:
- :class:
Entity— one component of a complex (a protein chain, a ligand, a DNA strand, etc.). - :class:
ComplexSpec— an ordered list of entities defining the complete system to fold. - :class:
TemplatePolicy— whether, and how, an engine may lean on structural templates. Ablating templates is how you find out what a model can predict without being shown the answer.
Engines that support multi-component prediction expose a
predict_complex(spec) method (see :class:Boltz.predict_complex
and :class:Chai1.predict_complex). The method returns a multi-chain
:class:Protein whose atom_array has one logical chain per
entity (or per copy, for homo-oligomers).
Engines that don't support complexes (:class:ESMFold,
:class:AlphaFold in single-chain mode) simply don't expose
predict_complex. Check with hasattr(engine, "predict_complex")
or by importing the engine and reading its documented capabilities.
Design notes¶
The :class:Entity shape is a common denominator of Boltz YAML
and Chai-1 FASTA. Both engines support:
- Polymer entities (protein / DNA / RNA) specified by one-letter sequence.
- Ligand entities specified by SMILES or CCD code (mutually exclusive).
- Multiple copies of an entity (homo-oligomers).
- Per-entity chain IDs (auto-assigned A, B, C, ... if omitted).
What this v1 deliberately doesn't model:
- Modified residues (Boltz uses
modificationslists; Chai needs a separate mechanism). Carry the unmodified sequence here; add engine-specific modification kwargs as a follow-up. - Custom MSAs per entity (use
use_msa_serverfor v1). MSA depth is controllable — see the engines'msa_depthparameter. - Per-entity templates. :class:
TemplatePolicyis set on the engine and applies to the whole prediction; targeting one chain means dropping down to the engine's own input format. - Restraints (covalent bonds, pocket constraints). Boltz supports these via top-level YAML keys; deferred.
- Ions specified as a separate entity type — Boltz models them as
CCD ligand entries with codes like
ZN/MG; Chai accepts them asligand|name=SMILES like[Zn+2]. We treat them as ligands.
For these advanced features, drop down to the engine's raw input format using the engine instance's documented advanced API.
Entity
dataclass
¶
Entity(
kind: EntityKind,
sequence: str | None = None,
smiles: str | None = None,
ccd: str | None = None,
chain_id: str | None = None,
copies: int = 1,
name: str | None = None,
msa: MultipleSequenceAlignment
| Literal["empty"]
| None = None,
)
One component of a biomolecular complex.
An Entity describes a single chain (for polymers) or a single small molecule (for ligands). It does not specify a 3D structure; that's what the folding engine produces.
For polymer entities (protein, dna, rna), supply sequence as
a one-letter string. For ligand entities, supply exactly one of
smiles (a SMILES string) or ccd (a 3-letter PDB Chemical
Component Dictionary code, e.g. "ATP", "NAD", "ZN").
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
kind
|
EntityKind
|
One of |
required |
sequence
|
str | None
|
One-letter sequence string. Required for polymer
kinds; must be |
None
|
smiles
|
str | None
|
SMILES string. Only valid for |
None
|
ccd
|
str | None
|
3-letter CCD code. Only valid for |
None
|
chain_id
|
str | None
|
Logical chain identifier (single letter A-Z, or
two-letter for large complexes). |
None
|
copies
|
int
|
Number of identical copies for homo-oligomers.
|
1
|
name
|
str | None
|
Human-readable identifier for logs and reports.
|
None
|
msa
|
MultipleSequenceAlignment | Literal['empty'] | None
|
A precomputed alignment for this chain. To use an alignment from disk, read it first —
|
None
|
Examples:
Single protein chain::
Entity(kind="protein", sequence="MKQHKAMIVAL...")
Aspirin (small molecule via SMILES)::
Entity(kind="ligand", smiles="CC(=O)OC1=CC=CC=C1C(=O)O")
ATP cofactor (via CCD code)::
Entity(kind="ligand", ccd="ATP")
Homodimer (two identical protein chains)::
Entity(kind="protein", sequence="MKQH...", copies=2)
DNA double helix (two complementary strands)::
Entity(kind="dna", sequence="ATCGATCG", chain_id="A")
Entity(kind="dna", sequence="CGATCGAT", chain_id="B")
ComplexSpec
dataclass
¶
An ordered specification of a biomolecular complex to fold.
A :class:ComplexSpec is the input to multi-component prediction
via :meth:Boltz.predict_complex and :meth:Chai1.predict_complex.
It's the engine-agnostic shape; serializers in each wrapper convert
it to the engine's native format (Boltz YAML or Chai typed FASTA).
The spec is ordered: the order of entities maps deterministically
to chain IDs A, B, C, ... when none are explicitly assigned. For
multi-copy entities (Entity.copies > 1), each copy gets its
own chain ID in sequence.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entities
|
tuple[Entity, ...]
|
Tuple of :class: |
required |
Validation
- At least one entity required.
- All explicitly-set chain_ids must be unique across the spec.
- At most one ligand entity may carry an
affinity_bindermarker (deferred; not exposed in v1).
Examples:
Single-protein "complex" (degenerate; usually go through
:meth:FoldingEngine.predict instead)::
spec = ComplexSpec.from_protein("MKQH...")
Protein + small-molecule drug::
spec = ComplexSpec.protein_ligand(
protein_sequence="MKQHKAMIVAL...",
ligand_smiles="CC(=O)OC1=CC=CC=C1C(=O)O",
)
Antibody-antigen (3-chain protein complex: heavy, light, antigen)::
spec = ComplexSpec(entities=(
Entity(kind="protein", sequence=heavy_chain_seq,
chain_id="H", name="heavy"),
Entity(kind="protein", sequence=light_chain_seq,
chain_id="L", name="light"),
Entity(kind="protein", sequence=antigen_seq,
chain_id="A", name="antigen"),
))
Transcription factor on DNA::
spec = ComplexSpec(entities=(
Entity(kind="protein", sequence=tf_seq, copies=1),
Entity(kind="dna", sequence=forward_strand),
Entity(kind="dna", sequence=reverse_strand),
))
from_protein
classmethod
¶
Single-protein convenience constructor.
Equivalent to ComplexSpec(entities=(Entity(kind="protein",
sequence=sequence, chain_id=chain_id),)). Used internally
so :meth:Boltz.predict(sequence) can delegate to the same
code path as :meth:Boltz.predict_complex(...).
protein_ligand
classmethod
¶
protein_ligand(
*,
protein_sequence: str,
ligand_smiles: str | None = None,
ligand_ccd: str | None = None,
protein_chain_id: str = "A",
ligand_chain_id: str = "B",
) -> ComplexSpec
Headline protein + small-molecule constructor.
The standard drug-discovery shape: one protein chain plus one
small molecule. Supply the ligand as ligand_smiles or
ligand_ccd (mutually exclusive).
Example::
spec = ComplexSpec.protein_ligand(
protein_sequence="MVTPEG...",
ligand_smiles="N[C@@H](Cc1ccc(O)cc1)C(=O)O",
)
to_provenance ¶
Flatten to the JSON-safe shape recorded in :class:Provenance.
Provenance.inputs must be JSON-serialisable and a
:class:ComplexSpec is not — it's a frozen dataclass of
:class:Entity tuples. This flattens it to a list of dicts with
engine-agnostic keys, so the same structure appears in provenance
whichever engine produced the prediction, and a Boltz run can be
compared with a Chai-1 one side by side.
Chain IDs are recorded as assigned, not as declared, so the record says which chains actually came back rather than leaving the reader to re-run the allocator.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
|
from_provenance
classmethod
¶
Rebuild a spec from :meth:to_provenance output.
The inverse exists so a recorded complex prediction can be replayed: without it a manifest can describe a multi-component fold perfectly well and still be unable to re-run it.
Each entity's first assigned chain ID is pinned explicitly and the remaining copies are counted, which reproduces the original assignment exactly — including when some entities had explicit IDs and others were auto-assigned around them.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
payload
|
Mapping[str, Any]
|
A dict shaped like :meth: |
required |
Returns:
| Name | Type | Description |
|---|---|---|
A |
ComplexSpec
|
class: |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
assigned_chain_ids ¶
Return chain IDs per entity after auto-assignment.
Returns a list of length len(self.entities). Each element
is itself a list of length entity.copies containing the
chain IDs that will appear in the final structure for that
entity's copies.
Auto-assignment uses A, B, C, ..., Z, AA, AB, ... in spec
order, skipping any IDs already claimed by explicit
Entity.chain_id values.
Example::
spec = ComplexSpec(entities=(
Entity(kind="protein", sequence="MKQ", copies=2), # auto
Entity(kind="protein", sequence="HIS", chain_id="X"), # explicit
Entity(kind="ligand", smiles="O"), # auto
))
spec.assigned_chain_ids()
# → [["A", "B"], ["X"], ["C"]]
TemplatePolicy
dataclass
¶
TemplatePolicy(
mode: Literal["none", "structures", "hits", "server"],
structures: tuple[str, ...] = (),
hits_file: str | None = None,
)
Whether — and how — a folding engine may use structural templates.
A template lets the model copy a known fold rather than derive one. That is usually what you want, and occasionally exactly what you don't: to see whether a model can produce a conformation on its own, you have to be able to take its templates away. Comparing runs across template settings is an ablation, and an ablation needs the setting to be a parameter rather than an engine default.
The engines disagree about what a template is, so the policy names the mechanism rather than pretending they're interchangeable:
================= ====================================== ===========
Mode Means Supported by
================= ====================================== ===========
"none" No templates. Predict from sequence. Boltz, Chai-1
"structures" These specific structure files. Boltz
"hits" A precomputed template-hits file. Chai-1
"server" Let the engine search for templates. Chai-1
================= ====================================== ===========
An engine raises :class:ValueError for a mode it cannot honour,
naming what it does support — rather than quietly folding without
the templates you asked for.
Note that molforge does not decide which templates are
appropriate. Restricting a run to, say, only unbound structures is a
judgement about your targets that molforge has no way to make; you
make it by choosing what to pass to :meth:structures or by
filtering the hits file. What molforge guarantees is that whatever
you chose is applied, recorded in provenance, and therefore part of
the cache key — so two policies never share a cached result.
Attributes:
| Name | Type | Description |
|---|---|---|
mode |
Literal['none', 'structures', 'hits', 'server']
|
One of |
structures |
tuple[str, ...]
|
Paths to template structure files, for
|
hits_file |
str | None
|
Path to a template-hits file, for |
Example
from molforge.folding import TemplatePolicy TemplatePolicy.none().mode 'none' TemplatePolicy.from_structures("4hhb.cif", "1ubq.cif").structures ('4hhb.cif', '1ubq.cif')
none
classmethod
¶
No templates: make the model predict from sequence alone.
from_structures
classmethod
¶
Use these structure files as templates.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
*paths
|
str | PathLike[str]
|
Template structure files. Boltz reads |
()
|
Raises:
| Type | Description |
|---|---|
ValueError
|
If no paths are given. |
from_hits
classmethod
¶
Use a precomputed template-hits file (Chai-1's .m8).
from_server
classmethod
¶
Let the engine search a template server for itself.
to_provenance ¶
A JSON-safe summary for :class:~molforge.core.Provenance.
Only the keys that carry information are emitted, so a policy's contribution to a cache key stays as small as what it actually says.