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_idto 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()orSeqMut.combine()withlineageopted in (read fromSeqOpt.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_idcolumn of the returned table or in thecandidate_idfield of a record.
- Returns:
list_lineage – The records from the root ancestor to
candidate_id, root first, so concatenating theirmutationsreplays 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:
- Raises:
ValueError – If
lineageis not a non-empty list of lineage records, or ifcandidate_idis not the identifier of one of them.RuntimeError – If the records are cyclic, so a candidate would be its own ancestor.
See also
SeqOpt.run(): whoselineageparameter produces the records.SeqMut.trace_lineage(): the same tracer on the mutation class.
Added in version 1.2.0.
Examples
:meth:
SeqOpt.trace_lineagereads the candidate-lineage record that :meth:SeqOpt.runattaches whenlineageis 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_lineagethen reconstructs the whole path: pass every collected record aslineageand the final candidate ascandidate_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
seedand the digest of the applied design limits of every round, so a promising design is traceable back to the run that produced it. Concatenating themutationsreplays 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