SeqOpt.trace_lineage

static SeqOpt.trace_lineage(lineage, candidate_id)[source]

Reconstruct the chain of lineage records that leads to one designed candidate.

Directed evolution over several rounds is a chain: the candidate picked from one run becomes the wild-type of the next, and each record names its parent by content hash. This walks those links backwards from candidate_id to the root the export still contains, so the full parent-to-candidate path is recovered from the records alone – including after a JSON round trip, and including records from different runs.

Parameters:
  • lineage (list of dict) – Lineage records collected over one or more design rounds, as produced by SeqOpt.run() or SeqMut.combine() with lineage opted in (read from SeqOpt.lineage_ / SeqMut.lineage_, or reloaded from JSON). Records of unrelated candidates may be present; only the traced chain is returned.

  • candidate_id (str) – Identifier of the candidate to trace, as found in the candidate_id column of the returned table or in the candidate_id field of a record.

Returns:

list_lineage – The records from the root ancestor to candidate_id, root first, so concatenating their mutations replays the design from the root sequence to the candidate. The chain holds one record when the candidate descends directly from its wild-type.

Return type:

list of dict

Raises:
  • ValueError – If lineage is not a non-empty list of lineage records, or if candidate_id is not the identifier of one of them.

  • RuntimeError – If the records are cyclic, so a candidate would be its own ancestor.

See also

Added in version 1.2.0.

Examples

:meth:SeqOpt.trace_lineage reads the candidate-lineage record that :meth:SeqOpt.run attaches when lineage is opted in. One run optimizes one wild-type, so a directed-evolution campaign is a chain of runs: the candidate picked from one round becomes the wild-type of the next, and each record names its parent by content hash. The record is opt-in and purely additive, so the Pareto table is unchanged unless it is requested.

import json

import pandas as pd
import aaanalysis as aa
aa.options["verbose"] = False

df_feat = aa.load_features(name="DOM_GSEC")
df_seq = aa.load_dataset(name="DOM_GSEC", n=10)
wt = df_seq[df_seq["label"] == 0].iloc[[0]].reset_index(drop=True)
seq_wt = wt["sequence"].iloc[0]

# Model-free objectives: move the feature profile as far as possible, with few mutations
objectives = [("magnitude", "max", "delta_cpp"), ("parsimony", "min", "n_mut")]
seqo = aa.SeqOpt(random_state=42)
df_pareto = seqo.run(df_seq=wt, df_feat=df_feat, objectives=objectives, pop_size=12, n_gen=4,
                     n_mut_max=3, region="tmd", lineage=True)
aa.display_df(df_pareto[["variant", "n_mut", "candidate_id"]], n_rows=10, show_shape=True)
print(json.dumps(seqo.lineage_[0], indent=1))
DataFrame shape: (6, 3)
  variant n_mut candidate_id
1 I53D 1 sha256:601aff44...dda533cbb38a9f7
2 I53P+I55W+A59Q 3 sha256:aae51154...7df1a64080237ee
3 I53S+I55W 2 sha256:eda1af52...fdf1a8ae95722fa
4 G46H+I53S+I55W 3 sha256:6a6ab3dc...746725ece114387
5 I53S 1 sha256:d8c84116...b600ee7dc18f08b
6 I53D+M57Y 2 sha256:8f648087...6966335c4a6890f
{
 "candidate_id": "sha256:601aff44912e218d37949db2ad6b59769eb6828dfda4b7eaddda533cbb38a9f7",
 "parent_id": "sha256:6292830e38478ba10f73868d050a2d353cb2011b5e40b9022a80db325eb43f23",
 "source_seq_id": "sha256:6292830e38478ba10f73868d050a2d353cb2011b5e40b9022a80db325eb43f23",
 "mutations": [
  {
   "pos": 53,
   "from_aa": "I",
   "to_aa": "D"
  }
 ],
 "method": "SeqOpt.run:nsga2",
 "objective_values": {
  "magnitude": 4.08684,
  "parsimony": 1.0
 },
 "seed": 42,
 "constraints_digest": "sha256:0fdc5ae18ab65f75b1798c4654baf8dc9e4dba9f70382bdc2ad550da026c1ffb"
}

Three rounds of directed evolution. Each round starts from the candidate picked out of the previous Pareto front and receives that candidate’s record as lineage, so the rounds link up. trace_lineage then reconstructs the whole path: pass every collected record as lineage and the final candidate as candidate_id.

lineage, sequence, parent = [], seq_wt, True
for seed in [0, 1, 2]:
    df_round = pd.DataFrame({"entry": wt["entry"], "sequence": [sequence],
                             "tmd_start": wt["tmd_start"], "tmd_stop": wt["tmd_stop"]})
    seqo = aa.SeqOpt(random_state=seed)
    df_pareto = seqo.run(df_seq=df_round, df_feat=df_feat, objectives=objectives, pop_size=12,
                         n_gen=4, n_mut_max=2, region="tmd", lineage=parent)
    lineage += seqo.lineage_
    best = df_pareto[df_pareto["n_mut"] > 0].iloc[0]
    parent = [r for r in seqo.lineage_ if r["candidate_id"] == best["candidate_id"]][0]
    sequence = best["sequence_mut"]

list_lineage = aa.SeqOpt.trace_lineage(lineage=lineage, candidate_id=parent["candidate_id"])
df_chain = pd.DataFrame([{"round": i + 1, "method": r["method"], "seed": r["seed"],
                          "mutations": "+".join(f"{m['from_aa']}{m['pos']}{m['to_aa']}"
                                                for m in r["mutations"]),
                          "magnitude": round(r["objective_values"]["magnitude"], 3)}
                         for i, r in enumerate(list_lineage)])
aa.display_df(df_chain, n_rows=10, show_shape=True)
DataFrame shape: (4, 5)
  round method seed mutations magnitude
1 1 SeqOpt.run:nsga2 0 0.000000
2 2 SeqOpt.run:nsga2 0 V56E+A59R 7.667000
3 3 SeqOpt.run:nsga2 1 I53N 4.014000
4 4 SeqOpt.run:nsga2 2 E56V+R59W 7.592000

The chain is root first and carries the effective seed and the digest of the applied design limits of every round, so a promising design is traceable back to the run that produced it. Concatenating the mutations replays the path from the wild-type to the final candidate, and the records survive export and reload unchanged.

# Replay the full three-round path from the original wild-type
replayed = seq_wt
for record in list_lineage:
    for m in record["mutations"]:
        replayed = replayed[:m["pos"] - 1] + m["to_aa"] + replayed[m["pos"]:]
print("rounds:", len(list_lineage), "| replay reproduces the final candidate:",
      replayed == sequence)

# Loss-free JSON round trip of the whole campaign
text = json.dumps(lineage)
print("round trip is loss-free:", json.loads(text) == lineage, "|", len(text), "chars")
print("same chain after reload:",
      aa.SeqOpt.trace_lineage(lineage=json.loads(text),
                              candidate_id=parent["candidate_id"]) == list_lineage)
rounds: 4 | replay reproduces the final candidate: True
round trip is loss-free: True | 7715 chars
same chain after reload: True