SeqMut.trace_lineage
- static SeqMut.trace_lineage(lineage, candidate_id)[source]
Reconstruct the chain of lineage records that leads to one candidate.
A design campaign is a chain: a candidate of one round becomes the parent 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 that came from different runs or different classes.- Parameters:
lineage (list of dict) – Lineage records collected over one or more design rounds, as produced by
SeqMut.combine()orSeqOpt.run()withlineageopted in (read fromSeqMut.lineage_/SeqOpt.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 source.- 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
SeqMut.combine(): whoselineageparameter produces the records.SeqOpt.trace_lineage(): the same tracer on the optimizer.
Added in version 1.2.0.
Examples
:meth:
SeqMut.trace_lineagereads the candidate-lineage record that :meth:SeqMut.combineattaches whenlineageis opted in. Each candidate gets a plain, JSON-serializable record: a content-hashcandidate_idover its source sequence and its ordered mutations, itsparent_id, the parent-relativemutations, the generatingmethod, theobjective_values, the effectiveseedand a digest of the applied design limits. The record is opt-in and purely additive: the default table is unchanged.import json import pandas as pd import aaanalysis as aa aa.options["verbose"] = False # Data + CPP features the delta-CPP engine scores the candidates on df_seq = aa.load_dataset(name="DOM_GSEC", n=10) labels = df_seq["label"].to_list() sf = aa.SequenceFeature() df_parts = sf.get_df_parts(df_seq=df_seq) df_scales = aa.load_scales() cpp = aa.CPP(df_parts=df_parts, split_kws=sf.get_split_kws(), df_scales=df_scales) df_feat = cpp.run(labels=labels, n_filter=25) entry = df_seq["entry"].iloc[0] seq_wt = df_seq["sequence"].iloc[0] ts = int(df_seq.set_index("entry").loc[entry, "tmd_start"]) # Round 1: two candidates of the wild-type, with the lineage record switched on variants = pd.DataFrame({"entry": [entry] * 3, "variant": ["double", "double", "single"], "pos": [ts, ts + 1, ts + 3], "to_aa": ["W", "P", "K"]}) seqm = aa.SeqMut() df_variant = seqm.combine(df_seq=df_seq, variants=variants, df_feat=df_feat, lineage=True) aa.display_df(df_variant[["variant", "n_mut", "delta_cpp", "candidate_id"]], n_rows=10, show_shape=True) print(json.dumps(seqm.lineage_[0], indent=1))
DataFrame shape: (2, 4)
variant n_mut delta_cpp candidate_id 1 G40K 1 0.000000 sha256:8e74a619...c3215b217ecd139 2 L37W+Q38P 2 0.020000 sha256:06f78686...7d77de73cad02e6 { "candidate_id": "sha256:8e74a61925dc714836c3f0472ef91e2a123fd75637e205799c3215b217ecd139", "parent_id": "sha256:6292830e38478ba10f73868d050a2d353cb2011b5e40b9022a80db325eb43f23", "source_seq_id": "sha256:6292830e38478ba10f73868d050a2d353cb2011b5e40b9022a80db325eb43f23", "mutations": [ { "pos": 40, "from_aa": "G", "to_aa": "K" } ], "method": "SeqMut.combine", "objective_values": { "delta_cpp": 0.0, "shift_score": 0.0 }, "seed": null, "constraints_digest": null }
Round 2: a candidate becomes the parent. Passing the record of a round-1 candidate instead of
Truedeclares that parent, so the new records carry itscandidate_idas theirparent_id. That is how the rounds of a multi-generation design chain up.trace_lineagethen walks the chain back: pass the collected records aslineageand the candidate to trace ascandidate_id.# The round-1 double becomes the parent of round 2 record_parent = [r for r in seqm.lineage_ if len(r["mutations"]) == 2][0] seq_parent = df_variant[df_variant["n_mut"] == 2]["sequence_mut"].iloc[0] df_seq_parent = pd.DataFrame({"entry": [entry], "sequence": [seq_parent], "tmd_start": [ts], "tmd_stop": [int(df_seq["tmd_stop"].iloc[0])]}) variants_2 = pd.DataFrame({"entry": [entry] * 2, "variant": ["r2a", "r2b"], "pos": [ts + 5, ts + 6], "to_aa": ["C", "Y"]}) seqm = aa.SeqMut() df_variant_2 = seqm.combine(df_seq=df_seq_parent, variants=variants_2, df_feat=df_feat, lineage=record_parent) lineage = [record_parent] + seqm.lineage_ list_lineage = aa.SeqMut.trace_lineage(lineage=lineage, candidate_id=seqm.lineage_[0]["candidate_id"]) df_chain = pd.DataFrame([{"step": i, "method": r["method"], "mutations": "+".join(f"{m['from_aa']}{m['pos']}{m['to_aa']}" for m in r["mutations"]), "candidate_id": r["candidate_id"][:19] + "..."} for i, r in enumerate(list_lineage)]) aa.display_df(df_chain, n_rows=10, show_shape=True)
DataFrame shape: (2, 4)
step method mutations candidate_id 1 0 SeqMut.combine L37W+Q38P sha256:06f78686198d... 2 1 SeqMut.combine L42C sha256:f7e250a9c157... The chain is root first, so concatenating the
mutationsof its records replays the design from the wild-type to the candidate. Because every identifier is a content hash, the records survive a JSON round trip unchanged, and two runs that build the same mutant from the same source assign the samecandidate_id, which is what makes duplicates detectable across runs.# Replay the path: apply every mutation of the chain to the wild-type sequence = seq_wt for record in list_lineage: for m in record["mutations"]: sequence = sequence[:m["pos"] - 1] + m["to_aa"] + sequence[m["pos"]:] print("replay reproduces the candidate:", sequence == df_variant_2["sequence_mut"].iloc[0]) # Loss-free JSON round trip, then trace again from the reloaded records reloaded = json.loads(json.dumps(lineage)) print("round trip is loss-free:", reloaded == lineage) print("same chain after reload:", aa.SeqMut.trace_lineage(lineage=reloaded, candidate_id=seqm.lineage_[0]["candidate_id"]) == list_lineage) # The same mutant, reached in a second run, gets the same identifier seqm = aa.SeqMut() seqm.combine(df_seq=df_seq, df_feat=df_feat, lineage=True, variants=pd.DataFrame({"entry": [entry] * 2, "variant": ["again"] * 2, "pos": [ts + 1, ts], "to_aa": ["P", "W"]})) print("duplicate detected:", seqm.lineage_[0]["candidate_id"] == record_parent["candidate_id"])
replay reproduces the candidate: True round trip is loss-free: True same chain after reload: True duplicate detected: True