修复设计计算错误并优化分享
This commit is contained in:
parent
dd21623cb5
commit
30cdd05914
5 changed files with 916 additions and 28 deletions
|
|
@ -82,8 +82,11 @@ UNIT_SCALE = {
|
|||
}
|
||||
IUPAC_CODES = "ACGTUWSMKRYBDHVN"
|
||||
IUPAC_CONSTRAINT_TOKEN = re.compile(rf"[{IUPAC_CODES}](?:\d+)?")
|
||||
FIXED_BASES = set("ACGTU")
|
||||
COMPLEMENT_BASE = str.maketrans({"A": "T", "C": "G", "G": "C", "T": "A", "U": "A"})
|
||||
|
||||
VALID_COMPUTE = {"pfunc", "pairs", "mfe", "sample", "subopt", "ensemble_size"}
|
||||
FIXED_TARGET_POLICIES = {"exclude_from_optimization", "include"}
|
||||
CANCEL_REQUESTED_STATUS = "cancel_requested"
|
||||
CANCELED_STATUS = "canceled"
|
||||
TERMINAL_JOB_STATUSES = {"success", "error", CANCELED_STATUS}
|
||||
|
|
@ -245,6 +248,54 @@ def is_valid_iupac_constraint(sequence):
|
|||
return True
|
||||
|
||||
|
||||
def expand_iupac_constraint(sequence):
|
||||
seq = normalize_design_sequence(sequence)
|
||||
output = []
|
||||
index = 0
|
||||
while index < len(seq):
|
||||
match = IUPAC_CONSTRAINT_TOKEN.match(seq, index)
|
||||
if not match:
|
||||
raise ValueError(f"Invalid sequence constraint near: {seq[index:]}")
|
||||
token = match.group(0)
|
||||
base = token[0]
|
||||
count = int(token[1:] or "1")
|
||||
output.append(base * count)
|
||||
index = match.end()
|
||||
return "".join(output)
|
||||
|
||||
|
||||
def is_mutable_iupac_constraint(sequence):
|
||||
expanded = expand_iupac_constraint(sequence)
|
||||
return any(base not in FIXED_BASES for base in expanded)
|
||||
|
||||
|
||||
def reverse_complement_fixed(sequence):
|
||||
return sequence.upper().translate(COMPLEMENT_BASE)[::-1]
|
||||
|
||||
|
||||
def parse_domain_tokens(text):
|
||||
tokens = [token.strip() for token in re.split(r"[\s,]+", str(text or "").strip()) if token.strip()]
|
||||
output = []
|
||||
for token in tokens:
|
||||
complement = False
|
||||
domain_name = token
|
||||
if token.startswith("~"):
|
||||
complement = True
|
||||
domain_name = token[1:]
|
||||
elif token.endswith("*"):
|
||||
complement = True
|
||||
domain_name = token[:-1]
|
||||
output.append((domain_name, complement))
|
||||
return output
|
||||
|
||||
|
||||
def get_mapping_value(mapping, key, default=None):
|
||||
try:
|
||||
return mapping[key]
|
||||
except Exception:
|
||||
return default
|
||||
|
||||
|
||||
def ensure_unit_interval_limits(lower, upper, label):
|
||||
if not (0 <= lower <= upper <= 1):
|
||||
raise ValueError(f"{label} limits must satisfy 0 <= lower <= upper <= 1.")
|
||||
|
|
@ -320,6 +371,7 @@ def parse_design_options(payload):
|
|||
"seed": int(raw.get("seed", 0)),
|
||||
"wobble_mutations": bool(raw.get("wobble_mutations", False)),
|
||||
"max_time_seconds": int(raw.get("max_time_seconds", 0)),
|
||||
"fixed_target_policy": str(raw.get("fixed_target_policy", "exclude_from_optimization")).strip().lower(),
|
||||
}
|
||||
if options["trials"] < 1 or options["trials"] > 8:
|
||||
raise ValueError("design trials must be between 1 and 8.")
|
||||
|
|
@ -331,6 +383,8 @@ def parse_design_options(payload):
|
|||
raise ValueError("design stop_condition must be between 0 and 1.")
|
||||
if options["max_time_seconds"] < 0:
|
||||
raise ValueError("design max_time_seconds must be non-negative.")
|
||||
if options["fixed_target_policy"] not in FIXED_TARGET_POLICIES:
|
||||
raise ValueError("design fixed_target_policy must be exclude_from_optimization or include.")
|
||||
return options
|
||||
|
||||
|
||||
|
|
@ -388,11 +442,14 @@ def build_design_domains(domain_payload):
|
|||
|
||||
domain = Domain(sequence_constraint, name=name)
|
||||
domain_map[name] = domain
|
||||
mutable = is_mutable_iupac_constraint(sequence_constraint)
|
||||
ordered.append(
|
||||
{
|
||||
"name": name,
|
||||
"constraint": sequence_constraint,
|
||||
"object": domain,
|
||||
"mutable": mutable,
|
||||
"fixed_sequence": None if mutable else expand_iupac_constraint(sequence_constraint),
|
||||
}
|
||||
)
|
||||
return domain_map, ordered
|
||||
|
|
@ -424,11 +481,12 @@ def parse_domain_composition(text, domain_map):
|
|||
return domains
|
||||
|
||||
|
||||
def build_design_strands(strand_payload, domain_map=None):
|
||||
def build_design_strands(strand_payload, domain_map=None, domain_rows=None):
|
||||
if not strand_payload:
|
||||
raise ValueError("At least one design strand is required.")
|
||||
|
||||
domain_map = domain_map or {}
|
||||
domain_info = {item["name"]: item for item in (domain_rows or [])}
|
||||
use_domain_composition = bool(domain_map)
|
||||
target_strand_map = {}
|
||||
ordered = []
|
||||
|
|
@ -457,11 +515,28 @@ def build_design_strands(strand_payload, domain_map=None):
|
|||
target_strand = TargetStrand(strand_domains, name=name)
|
||||
constraint_kind = "sequence_constraint"
|
||||
constraint_value = sequence_constraint
|
||||
mutable = is_mutable_iupac_constraint(sequence_constraint)
|
||||
fixed_sequence = None if mutable else expand_iupac_constraint(sequence_constraint)
|
||||
else:
|
||||
inline_domain = None
|
||||
target_strand = TargetStrand(strand_domains, name=name)
|
||||
constraint_kind = "domain_composition"
|
||||
constraint_value = raw_definition
|
||||
fixed_parts = []
|
||||
mutable = False
|
||||
for domain_name, complement in parse_domain_tokens(raw_definition):
|
||||
info = domain_info.get(domain_name)
|
||||
if info is None:
|
||||
mutable = True
|
||||
fixed_parts = []
|
||||
break
|
||||
if info.get("mutable"):
|
||||
mutable = True
|
||||
fixed_parts = []
|
||||
break
|
||||
part = info.get("fixed_sequence") or ""
|
||||
fixed_parts.append(reverse_complement_fixed(part) if complement else part)
|
||||
fixed_sequence = None if mutable else "".join(fixed_parts)
|
||||
else:
|
||||
if not is_valid_iupac_constraint(sequence_constraint):
|
||||
raise ValueError(f"Design strand {name} contains unsupported constraint characters.")
|
||||
|
|
@ -470,6 +545,8 @@ def build_design_strands(strand_payload, domain_map=None):
|
|||
target_strand = TargetStrand(strand_domains, name=name)
|
||||
constraint_kind = "sequence_constraint"
|
||||
constraint_value = sequence_constraint
|
||||
mutable = is_mutable_iupac_constraint(sequence_constraint)
|
||||
fixed_sequence = None if mutable else expand_iupac_constraint(sequence_constraint)
|
||||
|
||||
target_strand_map[name] = target_strand
|
||||
ordered.append(
|
||||
|
|
@ -481,6 +558,8 @@ def build_design_strands(strand_payload, domain_map=None):
|
|||
"object": target_strand,
|
||||
"domains": strand_domains,
|
||||
"domain": inline_domain,
|
||||
"mutable": mutable,
|
||||
"fixed_sequence": fixed_sequence,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -728,13 +807,14 @@ def build_soft_constraints(payload, domain_map, strand_map, target_complex_map):
|
|||
return constraints
|
||||
|
||||
|
||||
def parse_design_complexes(payload, target_strand_map):
|
||||
def parse_design_complexes(payload, target_strand_map, strand_rows=None):
|
||||
target_rows = payload.get("design_complexes") or payload.get("design_targets") or []
|
||||
if not target_rows:
|
||||
raise ValueError("At least one design target complex is required.")
|
||||
|
||||
targets = []
|
||||
target_complex_map = {}
|
||||
strand_info = {item["name"]: item for item in (strand_rows or [])}
|
||||
|
||||
for idx, row in enumerate(target_rows, start=1):
|
||||
name = (row.get("name") or f"target_{idx}").strip() or f"target_{idx}"
|
||||
|
|
@ -761,11 +841,14 @@ def parse_design_complexes(payload, target_strand_map):
|
|||
structure,
|
||||
name=name,
|
||||
)
|
||||
mutable = any(strand_info.get(token, {}).get("mutable", True) for token in tokens)
|
||||
target_payload = {
|
||||
"name": name,
|
||||
"strands": tokens,
|
||||
"structure": structure,
|
||||
"object": target_complex,
|
||||
"mutable": mutable,
|
||||
"optimization_status": "included",
|
||||
}
|
||||
targets.append(target_payload)
|
||||
target_complex_map[name] = target_complex
|
||||
|
|
@ -859,6 +942,21 @@ def parse_design_tubes(payload, target_rows, target_complex_map, default_max_siz
|
|||
return ordered_rows, tubes
|
||||
|
||||
|
||||
def payload_with_allowed_design_targets(payload, allowed_target_names):
|
||||
allowed = set(allowed_target_names)
|
||||
filtered_tubes = []
|
||||
for row in payload.get("design_tubes") or []:
|
||||
on_targets = [entry for entry in (row.get("on_targets") or []) if (entry.get("complex") or "").strip() in allowed]
|
||||
if on_targets:
|
||||
tube_row = dict(row)
|
||||
tube_row["on_targets"] = on_targets
|
||||
filtered_tubes.append(tube_row)
|
||||
|
||||
output = dict(payload)
|
||||
output["design_tubes"] = filtered_tubes
|
||||
return output
|
||||
|
||||
|
||||
def validate_design_object_names(design_domains, design_strands, target_rows, tube_rows):
|
||||
name_map = {}
|
||||
for kind, rows in (
|
||||
|
|
@ -1206,46 +1304,62 @@ def serialize_design_result(
|
|||
ordered_domains,
|
||||
ordered_strands,
|
||||
):
|
||||
analysis_map = getattr(design_result, "to_analysis", {}) or {}
|
||||
designed_domains = []
|
||||
designed_domain_map = getattr(design_result, "domains", {}) or {}
|
||||
for item in ordered_domains:
|
||||
designed_domain = designed_domain_map.get(item["object"])
|
||||
domain_sequence = str(designed_domain) if designed_domain is not None else item.get("fixed_sequence")
|
||||
designed_domains.append(
|
||||
{
|
||||
"name": item["name"],
|
||||
"constraint": item["constraint"],
|
||||
"sequence": str(designed_domain) if designed_domain is not None else None,
|
||||
"length": len(str(designed_domain)) if designed_domain is not None else None,
|
||||
"sequence": domain_sequence,
|
||||
"length": len(domain_sequence) if domain_sequence is not None else None,
|
||||
"mutable": bool(item.get("mutable", True)),
|
||||
}
|
||||
)
|
||||
|
||||
designed_strands = []
|
||||
designed_strand_by_name = {}
|
||||
for item in ordered_strands:
|
||||
target_strand = item["object"]
|
||||
analysis_strand = design_result.to_analysis[target_strand]
|
||||
analysis_strand = get_mapping_value(analysis_map, target_strand)
|
||||
sequence = str(analysis_strand) if analysis_strand is not None else item.get("fixed_sequence")
|
||||
designed_strands.append(
|
||||
{
|
||||
"name": item["name"],
|
||||
"constraint": item["constraint"],
|
||||
"constraint_kind": item.get("constraint_kind", "sequence_constraint"),
|
||||
"definition": item.get("definition", item["constraint"]),
|
||||
"sequence": str(analysis_strand),
|
||||
"length": len(str(analysis_strand)),
|
||||
"sequence": sequence,
|
||||
"length": len(sequence) if sequence is not None else None,
|
||||
"mutable": bool(item.get("mutable", True)),
|
||||
}
|
||||
)
|
||||
if sequence is not None:
|
||||
designed_strand_by_name[item["name"]] = sequence
|
||||
|
||||
target_complexes = []
|
||||
for target in target_rows:
|
||||
target_complex = target["object"]
|
||||
analysis_complex = design_result.to_analysis[target_complex]
|
||||
analysis_complex = get_mapping_value(analysis_map, target_complex)
|
||||
if analysis_complex is not None:
|
||||
display = stringify_complex(analysis_complex)
|
||||
sequence = flatten_sequence(analysis_complex)
|
||||
else:
|
||||
display = " + ".join(target["strands"])
|
||||
sequence = "".join(designed_strand_by_name.get(name, "") for name in target["strands"])
|
||||
target_complexes.append(
|
||||
{
|
||||
"name": target["name"],
|
||||
"display": stringify_complex(analysis_complex),
|
||||
"display": display,
|
||||
"strand_names": list(target["strands"]),
|
||||
"structure": target["structure"],
|
||||
"sequence": flatten_sequence(analysis_complex),
|
||||
"sequence": sequence,
|
||||
"target_concentration_M": target.get("target_concentration_M"),
|
||||
"optimization_status": target.get("optimization_status", "included"),
|
||||
"mutable": bool(target.get("mutable", True)),
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -1291,6 +1405,11 @@ def serialize_design_result(
|
|||
"seed": design_options["seed"],
|
||||
"wobble_mutations": design_options["wobble_mutations"],
|
||||
"max_time_seconds": design_options["max_time_seconds"],
|
||||
"fixed_target_policy": design_options["fixed_target_policy"],
|
||||
},
|
||||
"optimization": {
|
||||
"included_targets": [row["name"] for row in target_rows if row.get("optimization_status") == "included"],
|
||||
"excluded_fixed_targets": [row["name"] for row in target_rows if row.get("optimization_status") == "fixed_excluded"],
|
||||
},
|
||||
"target_tubes": [
|
||||
{
|
||||
|
|
@ -1346,22 +1465,42 @@ def run_job_payload(payload):
|
|||
target_strand_map, design_strands = build_design_strands(
|
||||
payload.get("strands") or [],
|
||||
domain_map=design_domain_map,
|
||||
domain_rows=design_domains,
|
||||
)
|
||||
target_rows, target_complex_map = parse_design_complexes(payload, target_strand_map)
|
||||
target_rows, target_complex_map = parse_design_complexes(payload, target_strand_map, strand_rows=design_strands)
|
||||
optimization_target_rows = target_rows
|
||||
optimization_target_complex_map = target_complex_map
|
||||
if design_options["fixed_target_policy"] == "exclude_from_optimization":
|
||||
optimization_target_rows = [row for row in target_rows if row.get("mutable", True)]
|
||||
fixed_names = {row["name"] for row in target_rows if not row.get("mutable", True)}
|
||||
for row in target_rows:
|
||||
if row["name"] in fixed_names:
|
||||
row["optimization_status"] = "fixed_excluded"
|
||||
optimization_target_complex_map = {
|
||||
row["name"]: row["object"] for row in optimization_target_rows
|
||||
}
|
||||
if not optimization_target_rows:
|
||||
raise ValueError("Design contains no mutable target complexes after fixed-target filtering.")
|
||||
hard_constraints = build_hard_constraints(payload, design_domain_map, target_strand_map)
|
||||
soft_constraints = build_soft_constraints(
|
||||
payload,
|
||||
design_domain_map,
|
||||
target_strand_map,
|
||||
target_complex_map,
|
||||
optimization_target_complex_map,
|
||||
)
|
||||
tube_rows = []
|
||||
design_tubes = []
|
||||
if mode == "tube":
|
||||
optimization_payload = payload
|
||||
if design_options["fixed_target_policy"] == "exclude_from_optimization":
|
||||
optimization_payload = payload_with_allowed_design_targets(
|
||||
payload,
|
||||
{row["name"] for row in optimization_target_rows},
|
||||
)
|
||||
tube_rows, design_tubes = parse_design_tubes(
|
||||
payload,
|
||||
target_rows,
|
||||
target_complex_map,
|
||||
optimization_payload,
|
||||
optimization_target_rows,
|
||||
optimization_target_complex_map,
|
||||
design_options["off_target_max_size"],
|
||||
)
|
||||
validate_design_object_names(design_domains, design_strands, target_rows, tube_rows)
|
||||
|
|
@ -1386,7 +1525,7 @@ def run_job_payload(payload):
|
|||
)
|
||||
elif mode == "complex":
|
||||
design_job = complex_design(
|
||||
complexes=[row["object"] for row in target_rows],
|
||||
complexes=[row["object"] for row in optimization_target_rows],
|
||||
model=model,
|
||||
options=design_job_options,
|
||||
hard_constraints=hard_constraints,
|
||||
|
|
@ -1923,6 +2062,69 @@ def get_share(share_id):
|
|||
return dict(item) if item else None
|
||||
|
||||
|
||||
def public_share_payload(share_id, *, record_access=True):
|
||||
metadata = ACCOUNT_STORE.share_metadata(share_id)
|
||||
if metadata is None:
|
||||
return get_share(share_id)
|
||||
|
||||
live = get_job_data(metadata["job_id"], include_payload=True)
|
||||
if live is not None:
|
||||
if record_access:
|
||||
ACCOUNT_STORE.record_share_access(share_id)
|
||||
payload = live.get("payload")
|
||||
if payload is None:
|
||||
account_job = ACCOUNT_STORE.get_job(metadata["user_id"], metadata["job_id"], include_content=True)
|
||||
payload = account_job.get("payload") if account_job else None
|
||||
result = live.get("result")
|
||||
error = live.get("error")
|
||||
return {
|
||||
"id": share_id,
|
||||
"share_id": share_id,
|
||||
"job_id": metadata["job_id"],
|
||||
"created_at": metadata["created_at"],
|
||||
"status": live.get("status"),
|
||||
"updated_at": live.get("updated_at"),
|
||||
"elapsed_seconds": live.get("elapsed_seconds"),
|
||||
"payload": payload,
|
||||
"result": result,
|
||||
"error": error,
|
||||
"result_summary": build_history_summary(payload, live),
|
||||
}
|
||||
|
||||
return ACCOUNT_STORE.resolve_share(share_id, record_access=record_access)
|
||||
|
||||
|
||||
def build_history_summary(payload, fallback=None):
|
||||
payload = payload or {}
|
||||
fallback = fallback or {}
|
||||
model = payload.get("model") or {}
|
||||
workflow = payload.get("workflow") or fallback.get("workflow") or "analysis"
|
||||
mode = payload.get("mode") or fallback.get("mode") or "tube"
|
||||
strands = payload.get("strands") or []
|
||||
if workflow == "design":
|
||||
complexes = payload.get("design_complexes") or payload.get("design_targets") or []
|
||||
tube_sizes = [int(row.get("max_size", 0) or 0) for row in (payload.get("design_tubes") or [])]
|
||||
max_size = max(tube_sizes, default=int((payload.get("design") or {}).get("off_target_max_size", 0) or 0))
|
||||
else:
|
||||
complexes = [line for line in str(payload.get("complexes_text") or "").splitlines() if line.strip()]
|
||||
max_size = int((payload.get("tube") or {}).get("max_size", 0) or 0)
|
||||
return {
|
||||
"workflow": workflow,
|
||||
"mode": mode,
|
||||
"material": model.get("material", fallback.get("material", "rna")),
|
||||
"celsius": float(model.get("celsius", fallback.get("celsius", 37)) or 37),
|
||||
"sodium": float(model.get("sodium", fallback.get("sodium", 0)) or 0),
|
||||
"magnesium": float(model.get("magnesium", fallback.get("magnesium", 0)) or 0),
|
||||
"max_size": max_size,
|
||||
"strand_count": len(strands),
|
||||
"complex_count": len(complexes),
|
||||
"compute": list(payload.get("compute") or fallback.get("compute") or ([] if workflow != "design" else ["design"])),
|
||||
"trials": int((payload.get("design") or {}).get("trials", fallback.get("trials", 0)) or 0),
|
||||
"stop_condition": float((payload.get("design") or {}).get("stop_condition", fallback.get("stop_condition", 0)) or 0),
|
||||
"max_time_seconds": int((payload.get("design") or {}).get("max_time_seconds", fallback.get("max_time_seconds", 0)) or 0),
|
||||
}
|
||||
|
||||
|
||||
def recover_interrupted_jobs():
|
||||
client = redis_client()
|
||||
recovered = 0
|
||||
|
|
@ -2043,6 +2245,7 @@ EXAMPLE_PAYLOAD = {
|
|||
"seed": 0,
|
||||
"wobble_mutations": False,
|
||||
"max_time_seconds": 0,
|
||||
"fixed_target_policy": "exclude_from_optimization",
|
||||
},
|
||||
"design_domains": [
|
||||
{"name": "a", "sequence": "N10"},
|
||||
|
|
@ -2104,6 +2307,7 @@ DESIGN_TUBE_EXAMPLE_PAYLOAD = {
|
|||
"seed": 1,
|
||||
"wobble_mutations": False,
|
||||
"max_time_seconds": 0,
|
||||
"fixed_target_policy": "exclude_from_optimization",
|
||||
},
|
||||
"design_domains": [
|
||||
{"name": "a", "sequence": "N10"},
|
||||
|
|
@ -2329,7 +2533,7 @@ class AppHandler(BaseHTTPRequestHandler):
|
|||
|
||||
if parsed.path.startswith("/api/shares/"):
|
||||
share_id = parsed.path.rsplit("/", 1)[-1]
|
||||
share = ACCOUNT_STORE.resolve_share(share_id) or get_share(share_id)
|
||||
share = public_share_payload(share_id, record_access=(parse_qs(parsed.query).get("poll") or ["0"])[0] != "1")
|
||||
if share is None:
|
||||
self._respond(*json_bytes({"error": "Share not found"}, status=HTTPStatus.NOT_FOUND))
|
||||
return
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue