pedview 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pedview/__init__.py +5 -0
- pedview/__main__.py +3 -0
- pedview/cli.py +199 -0
- pedview/insights.py +301 -0
- pedview/layout.py +1105 -0
- pedview/models.py +134 -0
- pedview/parser.py +428 -0
- pedview/phenotypes.py +89 -0
- pedview/relatedness.py +52 -0
- pedview/render.py +3740 -0
- pedview/segregation.py +132 -0
- pedview/validate.py +154 -0
- pedview-1.0.0.dist-info/METADATA +138 -0
- pedview-1.0.0.dist-info/RECORD +18 -0
- pedview-1.0.0.dist-info/WHEEL +5 -0
- pedview-1.0.0.dist-info/entry_points.txt +2 -0
- pedview-1.0.0.dist-info/licenses/LICENSE +21 -0
- pedview-1.0.0.dist-info/top_level.txt +1 -0
pedview/__init__.py
ADDED
pedview/__main__.py
ADDED
pedview/cli.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from .layout import build_family_layout
|
|
8
|
+
from .parser import PedigreeFormatError, parse_pedigree
|
|
9
|
+
from .render import render_report
|
|
10
|
+
from .validate import summarize_pedigree, validate_pedigree
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
14
|
+
parser = argparse.ArgumentParser(
|
|
15
|
+
prog="pedview",
|
|
16
|
+
description="Build interactive pedigree reports from pedigree-style family files.",
|
|
17
|
+
)
|
|
18
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
19
|
+
|
|
20
|
+
validate_parser = subparsers.add_parser(
|
|
21
|
+
"validate", help="Validate a pedigree file."
|
|
22
|
+
)
|
|
23
|
+
validate_parser.add_argument(
|
|
24
|
+
"input_path", help="Path to a pedigree file (.ped, .fam)."
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
preview_parser = subparsers.add_parser(
|
|
28
|
+
"preview", help="Summarize pedigree contents."
|
|
29
|
+
)
|
|
30
|
+
preview_parser.add_argument(
|
|
31
|
+
"input_path", help="Path to a pedigree file (.ped, .fam)."
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
build_parser_ = subparsers.add_parser(
|
|
35
|
+
"build", help="Generate a standalone HTML report."
|
|
36
|
+
)
|
|
37
|
+
build_parser_.add_argument(
|
|
38
|
+
"input_path", help="Path to a pedigree file (.ped, .fam)."
|
|
39
|
+
)
|
|
40
|
+
build_parser_.add_argument(
|
|
41
|
+
"-o",
|
|
42
|
+
"--output",
|
|
43
|
+
help="Path to the output HTML file. Defaults to <input>.html.",
|
|
44
|
+
)
|
|
45
|
+
build_parser_.add_argument(
|
|
46
|
+
"--family",
|
|
47
|
+
help="Optional family ID filter. Only render the requested family.",
|
|
48
|
+
)
|
|
49
|
+
build_parser_.add_argument(
|
|
50
|
+
"--title",
|
|
51
|
+
help="Optional report title. Defaults to a title derived from the input filename.",
|
|
52
|
+
)
|
|
53
|
+
build_parser_.add_argument(
|
|
54
|
+
"--ancestor-inbreeding",
|
|
55
|
+
type=float,
|
|
56
|
+
help="Global Wright ancestor inbreeding coefficient (f_a). Defaults to 0.0 when omitted.",
|
|
57
|
+
)
|
|
58
|
+
build_parser_.add_argument(
|
|
59
|
+
"--variant-name",
|
|
60
|
+
help="Optional label or name of the candidate variant being analyzed (e.g. SCN1A:c.1234G>A).",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
return parser
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main(argv: list[str] | None = None) -> int:
|
|
67
|
+
parser = build_parser()
|
|
68
|
+
args = parser.parse_args(argv)
|
|
69
|
+
|
|
70
|
+
if args.command == "validate":
|
|
71
|
+
return run_validate(args.input_path)
|
|
72
|
+
if args.command == "preview":
|
|
73
|
+
return run_preview(args.input_path)
|
|
74
|
+
if args.command == "build":
|
|
75
|
+
return run_build(
|
|
76
|
+
args.input_path,
|
|
77
|
+
args.output,
|
|
78
|
+
args.family,
|
|
79
|
+
args.title,
|
|
80
|
+
args.ancestor_inbreeding,
|
|
81
|
+
args.variant_name,
|
|
82
|
+
)
|
|
83
|
+
parser.error("Unknown command.")
|
|
84
|
+
return 2
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def run_validate(input_path: str) -> int:
|
|
88
|
+
pedigree, messages = load_with_messages(input_path)
|
|
89
|
+
print_validation_result(pedigree.source_path, messages)
|
|
90
|
+
return 1 if any(message.severity == "error" for message in messages) else 0
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def run_preview(input_path: str) -> int:
|
|
94
|
+
pedigree, messages = load_with_messages(input_path)
|
|
95
|
+
summaries = summarize_pedigree(pedigree)
|
|
96
|
+
print(f"Source: {pedigree.source_path}")
|
|
97
|
+
print(f"Families: {len(summaries)}")
|
|
98
|
+
print(f"Individuals: {pedigree.people_count()}")
|
|
99
|
+
for summary in summaries:
|
|
100
|
+
print(
|
|
101
|
+
f"- {summary.family_id}: {summary.people_count} individuals, "
|
|
102
|
+
f"{summary.founders_count} founders, "
|
|
103
|
+
f"{summary.relationship_count} parent references, "
|
|
104
|
+
f"{summary.generation_count} generations"
|
|
105
|
+
)
|
|
106
|
+
if messages:
|
|
107
|
+
print()
|
|
108
|
+
print_validation_messages(messages)
|
|
109
|
+
return 1 if any(message.severity == "error" for message in messages) else 0
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def run_build(
|
|
113
|
+
input_path: str,
|
|
114
|
+
output_path: str | None,
|
|
115
|
+
family_filter: str | None,
|
|
116
|
+
title: str | None,
|
|
117
|
+
ancestor_inbreeding: float | None,
|
|
118
|
+
variant_name: str | None = None,
|
|
119
|
+
) -> int:
|
|
120
|
+
pedigree, messages = load_with_messages(input_path)
|
|
121
|
+
if family_filter:
|
|
122
|
+
if family_filter not in pedigree.families:
|
|
123
|
+
print(
|
|
124
|
+
f"Family '{family_filter}' was not found in the input.", file=sys.stderr
|
|
125
|
+
)
|
|
126
|
+
return 1
|
|
127
|
+
pedigree.families = {family_filter: pedigree.families[family_filter]}
|
|
128
|
+
if any(message.severity == "error" for message in messages):
|
|
129
|
+
print_validation_result(pedigree.source_path, messages)
|
|
130
|
+
return 1
|
|
131
|
+
|
|
132
|
+
summaries = summarize_pedigree(pedigree)
|
|
133
|
+
family_layouts = {
|
|
134
|
+
summary.family_id: build_family_layout(pedigree.families[summary.family_id])
|
|
135
|
+
for summary in summaries
|
|
136
|
+
}
|
|
137
|
+
ancestor_inbreeding_value = (
|
|
138
|
+
ancestor_inbreeding if ancestor_inbreeding is not None else 0.0
|
|
139
|
+
)
|
|
140
|
+
output = (
|
|
141
|
+
Path(output_path)
|
|
142
|
+
if output_path
|
|
143
|
+
else default_output_path(input_path, family_filter)
|
|
144
|
+
)
|
|
145
|
+
report_title = title or f"pedview report: {Path(input_path).stem}"
|
|
146
|
+
html = render_report(
|
|
147
|
+
pedigree=pedigree,
|
|
148
|
+
family_layouts=family_layouts,
|
|
149
|
+
family_summaries=summaries,
|
|
150
|
+
messages=[message for message in messages if message.severity == "warning"],
|
|
151
|
+
title=report_title,
|
|
152
|
+
ancestor_inbreeding=ancestor_inbreeding_value,
|
|
153
|
+
used_default_ancestor_inbreeding=ancestor_inbreeding is None,
|
|
154
|
+
variant_name=variant_name,
|
|
155
|
+
)
|
|
156
|
+
output.write_text(html, encoding="utf-8")
|
|
157
|
+
if ancestor_inbreeding is None:
|
|
158
|
+
print(
|
|
159
|
+
"Using Wright ancestor inbreeding coefficient f_a = 0.0 (default). "
|
|
160
|
+
"Pass --ancestor-inbreeding to override."
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
print(
|
|
164
|
+
f"Using Wright ancestor inbreeding coefficient f_a = {ancestor_inbreeding_value:.4f}."
|
|
165
|
+
)
|
|
166
|
+
print(f"Wrote {output}")
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def load_with_messages(input_path: str):
|
|
171
|
+
try:
|
|
172
|
+
pedigree = parse_pedigree(input_path)
|
|
173
|
+
except PedigreeFormatError as exc:
|
|
174
|
+
print(f"[ERROR] {exc}", file=sys.stderr)
|
|
175
|
+
raise SystemExit(1) from exc
|
|
176
|
+
messages = [*pedigree.messages, *validate_pedigree(pedigree)]
|
|
177
|
+
return pedigree, messages
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def default_output_path(input_path: str, family_filter: str | None) -> Path:
|
|
181
|
+
input_file = Path(input_path)
|
|
182
|
+
suffix = f".{family_filter}" if family_filter else ""
|
|
183
|
+
return input_file.with_suffix(f"{suffix}.html")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def print_validation_result(source_path: str, messages) -> None:
|
|
187
|
+
print(f"Validation result for {source_path}")
|
|
188
|
+
if not messages:
|
|
189
|
+
print("No validation issues found.")
|
|
190
|
+
return
|
|
191
|
+
print_validation_messages(messages)
|
|
192
|
+
error_count = sum(message.severity == "error" for message in messages)
|
|
193
|
+
warning_count = sum(message.severity == "warning" for message in messages)
|
|
194
|
+
print(f"{error_count} error(s), {warning_count} warning(s)")
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def print_validation_messages(messages) -> None:
|
|
198
|
+
for message in messages:
|
|
199
|
+
print(message.format_for_cli())
|
pedview/insights.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from functools import cache
|
|
4
|
+
|
|
5
|
+
from .layout import assign_generations
|
|
6
|
+
from .models import Family
|
|
7
|
+
from .phenotypes import affected_status_from_metadata
|
|
8
|
+
from .segregation import analyze_family_segregation
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def build_family_insights(
|
|
12
|
+
family: Family, variant_name: str | None = None
|
|
13
|
+
) -> dict[str, object]:
|
|
14
|
+
family.ensure_relationships()
|
|
15
|
+
generations = assign_generations(family)
|
|
16
|
+
order_index = family.order_index()
|
|
17
|
+
people = family.members
|
|
18
|
+
person_ids = list(family.order)
|
|
19
|
+
founder_ids = [
|
|
20
|
+
person_id
|
|
21
|
+
for person_id, person in people.items()
|
|
22
|
+
if person.father_id not in people and person.mother_id not in people
|
|
23
|
+
]
|
|
24
|
+
founder_id_set = set(founder_ids)
|
|
25
|
+
affected_status = {
|
|
26
|
+
person_id: affected_status_from_metadata(person.metadata)
|
|
27
|
+
for person_id, person in people.items()
|
|
28
|
+
}
|
|
29
|
+
parent_map = {
|
|
30
|
+
person_id: (
|
|
31
|
+
person.father_id if person.father_id in people else None,
|
|
32
|
+
person.mother_id if person.mother_id in people else None,
|
|
33
|
+
)
|
|
34
|
+
for person_id, person in people.items()
|
|
35
|
+
}
|
|
36
|
+
children_by_parent = {person_id: [] for person_id in person_ids}
|
|
37
|
+
partners_by_person = {person_id: set() for person_id in person_ids}
|
|
38
|
+
for relationship in family.relationships:
|
|
39
|
+
if relationship.father_id in people and relationship.mother_id in people:
|
|
40
|
+
partners_by_person[relationship.father_id].add(relationship.mother_id)
|
|
41
|
+
partners_by_person[relationship.mother_id].add(relationship.father_id)
|
|
42
|
+
for parent_id in relationship.parent_ids:
|
|
43
|
+
if parent_id in people:
|
|
44
|
+
children_by_parent[parent_id].extend(relationship.child_ids)
|
|
45
|
+
|
|
46
|
+
for parent_id, child_ids in children_by_parent.items():
|
|
47
|
+
children_by_parent[parent_id] = sorted(
|
|
48
|
+
set(child_ids),
|
|
49
|
+
key=lambda child_id: order_index.get(child_id, 0),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
@cache
|
|
53
|
+
def descendants(person_id: str) -> tuple[str, ...]:
|
|
54
|
+
found: set[str] = set()
|
|
55
|
+
for child_id in children_by_parent.get(person_id, ()):
|
|
56
|
+
if child_id in found:
|
|
57
|
+
continue
|
|
58
|
+
found.add(child_id)
|
|
59
|
+
found.update(descendants(child_id))
|
|
60
|
+
return tuple(sorted(found, key=lambda child_id: order_index.get(child_id, 0)))
|
|
61
|
+
|
|
62
|
+
def summarize_people(
|
|
63
|
+
target_ids: list[str] | tuple[str, ...] | set[str],
|
|
64
|
+
) -> dict[str, int]:
|
|
65
|
+
ids = list(target_ids)
|
|
66
|
+
return {
|
|
67
|
+
"total": len(ids),
|
|
68
|
+
"affected": sum(
|
|
69
|
+
affected_status.get(person_id) == "affected" for person_id in ids
|
|
70
|
+
),
|
|
71
|
+
"unaffected": sum(
|
|
72
|
+
affected_status.get(person_id) == "unaffected" for person_id in ids
|
|
73
|
+
),
|
|
74
|
+
"unknown_status": sum(
|
|
75
|
+
affected_status.get(person_id) == "unknown" for person_id in ids
|
|
76
|
+
),
|
|
77
|
+
"male": sum(people[person_id].sex == "male" for person_id in ids),
|
|
78
|
+
"female": sum(people[person_id].sex == "female" for person_id in ids),
|
|
79
|
+
"unknown_sex": sum(
|
|
80
|
+
people[person_id].sex not in {"male", "female"} for person_id in ids
|
|
81
|
+
),
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
generation_stats: list[dict[str, int]] = []
|
|
85
|
+
for generation in sorted(set(generations.values())):
|
|
86
|
+
ids = [
|
|
87
|
+
person_id
|
|
88
|
+
for person_id in person_ids
|
|
89
|
+
if generations.get(person_id) == generation
|
|
90
|
+
]
|
|
91
|
+
stats = summarize_people(ids)
|
|
92
|
+
stats["generation"] = generation
|
|
93
|
+
generation_stats.append(stats)
|
|
94
|
+
|
|
95
|
+
founder_stats = summarize_people(founder_ids)
|
|
96
|
+
|
|
97
|
+
def sibling_ids_for(person_id: str) -> list[str]:
|
|
98
|
+
father_id, mother_id = parent_map[person_id]
|
|
99
|
+
sibling_ids: set[str] = set()
|
|
100
|
+
for parent_id in (father_id, mother_id):
|
|
101
|
+
if parent_id is None:
|
|
102
|
+
continue
|
|
103
|
+
sibling_ids.update(children_by_parent.get(parent_id, ()))
|
|
104
|
+
sibling_ids.discard(person_id)
|
|
105
|
+
return sorted(
|
|
106
|
+
sibling_ids, key=lambda sibling_id: order_index.get(sibling_id, 0)
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
person_summaries: dict[str, dict[str, object]] = {}
|
|
110
|
+
family_flags: list[dict[str, object]] = []
|
|
111
|
+
flags_by_person = {person_id: [] for person_id in person_ids}
|
|
112
|
+
|
|
113
|
+
def add_flag(
|
|
114
|
+
*,
|
|
115
|
+
code: str,
|
|
116
|
+
severity: str,
|
|
117
|
+
message: str,
|
|
118
|
+
person_id: str,
|
|
119
|
+
related_ids: list[str] | None = None,
|
|
120
|
+
) -> None:
|
|
121
|
+
flag = {
|
|
122
|
+
"code": code,
|
|
123
|
+
"severity": severity,
|
|
124
|
+
"message": message,
|
|
125
|
+
"person_id": person_id,
|
|
126
|
+
"related_ids": related_ids or [],
|
|
127
|
+
}
|
|
128
|
+
flags_by_person[person_id].append(flag)
|
|
129
|
+
family_flags.append(flag)
|
|
130
|
+
|
|
131
|
+
for person_id in person_ids:
|
|
132
|
+
father_id, mother_id = parent_map[person_id]
|
|
133
|
+
parent_ids = [parent_id for parent_id in (father_id, mother_id) if parent_id]
|
|
134
|
+
sibling_ids = sibling_ids_for(person_id)
|
|
135
|
+
child_ids = children_by_parent.get(person_id, [])
|
|
136
|
+
descendant_ids = list(descendants(person_id))
|
|
137
|
+
partner_ids = sorted(
|
|
138
|
+
partners_by_person.get(person_id, set()),
|
|
139
|
+
key=lambda partner_id: order_index.get(partner_id, 0),
|
|
140
|
+
)
|
|
141
|
+
first_degree_ids = sorted(
|
|
142
|
+
set(parent_ids) | set(sibling_ids) | set(child_ids),
|
|
143
|
+
key=lambda relative_id: order_index.get(relative_id, 0),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
person_summaries[person_id] = {
|
|
147
|
+
"partners": partner_ids,
|
|
148
|
+
"parents": {
|
|
149
|
+
"ids": parent_ids,
|
|
150
|
+
**summarize_people(parent_ids),
|
|
151
|
+
},
|
|
152
|
+
"siblings": {
|
|
153
|
+
"ids": sibling_ids,
|
|
154
|
+
**summarize_people(sibling_ids),
|
|
155
|
+
},
|
|
156
|
+
"children": {
|
|
157
|
+
"ids": child_ids,
|
|
158
|
+
**summarize_people(child_ids),
|
|
159
|
+
},
|
|
160
|
+
"descendants": {
|
|
161
|
+
"ids": descendant_ids,
|
|
162
|
+
**summarize_people(descendant_ids),
|
|
163
|
+
},
|
|
164
|
+
"first_degree": {
|
|
165
|
+
"ids": first_degree_ids,
|
|
166
|
+
**summarize_people(first_degree_ids),
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
person_status = affected_status.get(person_id)
|
|
171
|
+
explicit_parent_statuses = [
|
|
172
|
+
affected_status[parent_id]
|
|
173
|
+
for parent_id in parent_ids
|
|
174
|
+
if affected_status.get(parent_id) in {"affected", "unaffected"}
|
|
175
|
+
]
|
|
176
|
+
if (
|
|
177
|
+
person_status == "affected"
|
|
178
|
+
and len(parent_ids) == 2
|
|
179
|
+
and explicit_parent_statuses == ["unaffected", "unaffected"]
|
|
180
|
+
):
|
|
181
|
+
add_flag(
|
|
182
|
+
code="affected_with_unaffected_parents",
|
|
183
|
+
severity="warning",
|
|
184
|
+
message="Affected individual has two recorded unaffected parents.",
|
|
185
|
+
person_id=person_id,
|
|
186
|
+
related_ids=parent_ids,
|
|
187
|
+
)
|
|
188
|
+
if (
|
|
189
|
+
person_status == "unaffected"
|
|
190
|
+
and len(parent_ids) == 2
|
|
191
|
+
and explicit_parent_statuses == ["affected", "affected"]
|
|
192
|
+
):
|
|
193
|
+
add_flag(
|
|
194
|
+
code="unaffected_with_affected_parents",
|
|
195
|
+
severity="warning",
|
|
196
|
+
message="Unaffected individual has two recorded affected parents.",
|
|
197
|
+
person_id=person_id,
|
|
198
|
+
related_ids=parent_ids,
|
|
199
|
+
)
|
|
200
|
+
if (
|
|
201
|
+
person_status == "affected"
|
|
202
|
+
and parent_ids
|
|
203
|
+
and not any(
|
|
204
|
+
affected_status.get(relative_id) == "affected"
|
|
205
|
+
for relative_id in first_degree_ids
|
|
206
|
+
)
|
|
207
|
+
):
|
|
208
|
+
add_flag(
|
|
209
|
+
code="isolated_affected_case",
|
|
210
|
+
severity="notice",
|
|
211
|
+
message="Affected individual has no affected first-degree relatives recorded.",
|
|
212
|
+
person_id=person_id,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
branch_summaries: list[dict[str, object]] = []
|
|
216
|
+
for relationship in family.relationships:
|
|
217
|
+
if relationship.parent_ids and not all(
|
|
218
|
+
parent_id in founder_id_set for parent_id in relationship.parent_ids
|
|
219
|
+
):
|
|
220
|
+
continue
|
|
221
|
+
branch_member_ids = set(relationship.parent_ids)
|
|
222
|
+
branch_descendant_ids: set[str] = set()
|
|
223
|
+
for child_id in relationship.child_ids:
|
|
224
|
+
branch_descendant_ids.add(child_id)
|
|
225
|
+
branch_descendant_ids.update(descendants(child_id))
|
|
226
|
+
branch_member_ids.update(branch_descendant_ids)
|
|
227
|
+
member_ids = sorted(
|
|
228
|
+
branch_member_ids, key=lambda person_id: order_index.get(person_id, 0)
|
|
229
|
+
)
|
|
230
|
+
descendant_ids = sorted(
|
|
231
|
+
branch_descendant_ids,
|
|
232
|
+
key=lambda person_id: order_index.get(person_id, 0),
|
|
233
|
+
)
|
|
234
|
+
label = (
|
|
235
|
+
" + ".join(relationship.parent_ids)
|
|
236
|
+
if relationship.parent_ids
|
|
237
|
+
else relationship.relationship_id
|
|
238
|
+
)
|
|
239
|
+
branch_summaries.append(
|
|
240
|
+
{
|
|
241
|
+
"label": label,
|
|
242
|
+
"root_ids": list(relationship.parent_ids),
|
|
243
|
+
"member_count": len(member_ids),
|
|
244
|
+
"descendant_count": len(descendant_ids),
|
|
245
|
+
"affected_count": sum(
|
|
246
|
+
affected_status.get(member_id) == "affected"
|
|
247
|
+
for member_id in member_ids
|
|
248
|
+
),
|
|
249
|
+
"affected_descendant_count": sum(
|
|
250
|
+
affected_status.get(descendant_id) == "affected"
|
|
251
|
+
for descendant_id in descendant_ids
|
|
252
|
+
),
|
|
253
|
+
}
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
branch_summaries.sort(
|
|
257
|
+
key=lambda item: (
|
|
258
|
+
-int(item["affected_descendant_count"]),
|
|
259
|
+
-int(item["descendant_count"]),
|
|
260
|
+
str(item["label"]),
|
|
261
|
+
)
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
segregation = analyze_family_segregation(family, variant_name=variant_name)
|
|
265
|
+
if segregation:
|
|
266
|
+
for err in segregation.get("mendelian_errors", []):
|
|
267
|
+
flag = {
|
|
268
|
+
"severity": "warning",
|
|
269
|
+
"person_id": err["person_id"],
|
|
270
|
+
"message": f"Mendelian Inconsistency: {err['message']}",
|
|
271
|
+
}
|
|
272
|
+
family_flags.append(flag)
|
|
273
|
+
flags_by_person[err["person_id"]].append(flag)
|
|
274
|
+
for dn_id in segregation.get("de_novo_ids", []):
|
|
275
|
+
flag = {
|
|
276
|
+
"severity": "notice",
|
|
277
|
+
"person_id": dn_id,
|
|
278
|
+
"message": "Candidate De Novo variant: mutation observed in child but neither biological parent.",
|
|
279
|
+
}
|
|
280
|
+
family_flags.append(flag)
|
|
281
|
+
flags_by_person[dn_id].append(flag)
|
|
282
|
+
|
|
283
|
+
family_flags.sort(
|
|
284
|
+
key=lambda flag: (
|
|
285
|
+
{"warning": 0, "notice": 1}.get(str(flag["severity"]), 2),
|
|
286
|
+
order_index.get(str(flag["person_id"]), 0),
|
|
287
|
+
str(flag["message"]),
|
|
288
|
+
)
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
for person_id in person_ids:
|
|
292
|
+
person_summaries[person_id]["flags"] = flags_by_person[person_id]
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
"generation_stats": generation_stats,
|
|
296
|
+
"founder_stats": founder_stats,
|
|
297
|
+
"branch_summaries": branch_summaries,
|
|
298
|
+
"family_flags": family_flags,
|
|
299
|
+
"person_summaries": person_summaries,
|
|
300
|
+
"segregation": segregation,
|
|
301
|
+
}
|