studio2ped 0.2.0__tar.gz → 0.2.1__tar.gz
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.
- {studio2ped-0.2.0 → studio2ped-0.2.1}/CHANGELOG.md +21 -0
- {studio2ped-0.2.0/studio2ped.egg-info → studio2ped-0.2.1}/PKG-INFO +11 -9
- {studio2ped-0.2.0 → studio2ped-0.2.1}/README.md +10 -8
- {studio2ped-0.2.0 → studio2ped-0.2.1}/pyproject.toml +1 -1
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped/__init__.py +1 -1
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped/converter.py +40 -22
- {studio2ped-0.2.0 → studio2ped-0.2.1/studio2ped.egg-info}/PKG-INFO +11 -9
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped_standalone.py +42 -24
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/test_studio2ped.py +57 -4
- {studio2ped-0.2.0 → studio2ped-0.2.1}/LICENSE +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/MANIFEST.in +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/setup.cfg +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped/__main__.py +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped/cli.py +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped.egg-info/SOURCES.txt +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped.egg-info/dependency_links.txt +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped.egg-info/entry_points.txt +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped.egg-info/requires.txt +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/studio2ped.egg-info/top_level.txt +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/cf-cousins.studio.json +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/expected/cf-cousins.mped +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/expected/features.mped +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/expected/lynch.mped +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/expected/okafor.mped +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/expected/price.mped +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/features.studio.json +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/lynch.studio.json +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/okafor.studio.json +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/data/price.studio.json +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/test_standalone.py +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tests/test_version.py +0 -0
- {studio2ped-0.2.0 → studio2ped-0.2.1}/tools/make_standalone.py +0 -0
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.2.1
|
|
4
|
+
|
|
5
|
+
- A parent marked G is a gestational carrier, and is not written as a genetic
|
|
6
|
+
parent. Since the 2025 correction to the pedigree nomenclature (Steinhaus
|
|
7
|
+
French, Bennett and Resta, *J Genet Couns* 2025;34(2):e2020), G marks a
|
|
8
|
+
gestational carrier, and Pedigree Studio draws G where it used to draw S.
|
|
9
|
+
S, which older drawings use, still means the same.
|
|
10
|
+
- The note says "gestational carrier" and gives the mark (G or S), not
|
|
11
|
+
"surrogate": the corrected nomenclature keeps "surrogate" for a woman who
|
|
12
|
+
gives the egg and carries the pregnancy, who is a genetic parent.
|
|
13
|
+
- A symbol divided into thirds, which Pedigree Studio now draws for three
|
|
14
|
+
conditions, gives each of its colours as a condition the person has, as
|
|
15
|
+
quarters do.
|
|
16
|
+
- The "no children" and "infertility" symbols no longer count when a row is
|
|
17
|
+
numbered. The nomenclature numbers individuals (Bennett et al., *J Genet
|
|
18
|
+
Couns* 2022;31(6):1238-48) and these symbols are not people, so Pedigree
|
|
19
|
+
Studio now gives them no number, and studio2ped numbers only people too:
|
|
20
|
+
someone to the right of one of these symbols in the same row has a number
|
|
21
|
+
one lower than before (II-3 rather than II-4), and a row with nothing but
|
|
22
|
+
these symbols is not a generation.
|
|
23
|
+
|
|
3
24
|
## 0.2.0
|
|
4
25
|
|
|
5
26
|
- People are now identified by their pedigree numbers (I-1, II-3 and so on),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: studio2ped
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Convert Pedigree Studio session JSON to PED/MPED pedigree files
|
|
5
5
|
Author-email: Tim Hearn <tjh70@cam.ac.uk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -99,11 +99,12 @@ The converter reads a Pedigree Studio session file and:
|
|
|
99
99
|
4. **Gives genetic parents only.** The father and mother columns of a PED
|
|
100
100
|
file are genetic parents. A child joined to a couple by a dashed line (an
|
|
101
101
|
adoption) gets 0 for that couple, and so does the place of a parent marked
|
|
102
|
-
|
|
103
|
-
donor) is a genetic parent.
|
|
104
|
-
same
|
|
102
|
+
G (a gestational carrier), with a note in each case; a parent marked D (a
|
|
103
|
+
donor) is a genetic parent. Drawings made before the 2025 correction to the
|
|
104
|
+
pedigree nomenclature mark a gestational carrier S, which is read the same
|
|
105
|
+
way. studio2ga4gh's PED-style pedigree gives the same.
|
|
105
106
|
5. **Extracts phenotypes** from the colour key: solid fills, half fills,
|
|
106
|
-
|
|
107
|
+
symbols in thirds or quarters, and shading patterns are all recognised.
|
|
107
108
|
6. **Detects carrier notation**: centre dots (●) and half-filled symbols mark
|
|
108
109
|
carriers.
|
|
109
110
|
7. **Chooses the format**:
|
|
@@ -136,9 +137,10 @@ would be `0`, the person is given their pedigree number instead, and a note
|
|
|
136
137
|
says so.
|
|
137
138
|
|
|
138
139
|
Pedigree numbers follow the drawing, as in Pedigree Studio: a new generation
|
|
139
|
-
starts 80 pixels or more below the first
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
starts 80 pixels or more below the first person of the generation above, and
|
|
141
|
+
each generation is numbered from the left. Only people are numbered: the "no
|
|
142
|
+
children" and "infertility" symbols are not people, so they have no number
|
|
143
|
+
and do not count in their row.
|
|
142
144
|
|
|
143
145
|
## Output formats
|
|
144
146
|
|
|
@@ -199,7 +201,7 @@ colour key:
|
|
|
199
201
|
| Full shading (stripes/dots) | `shading:pattern:full` | Affected (2) |
|
|
200
202
|
| Half colour fill (one side only) | `#rrggbb` | Carrier (3) |
|
|
201
203
|
| Half shading | `shading:pattern:half-left` | Carrier (3) |
|
|
202
|
-
|
|
|
204
|
+
| Thirds or quarters (any part) | `#rrggbb` | Affected (2) |
|
|
203
205
|
| Centre dot (●) | — | Carrier (3) |
|
|
204
206
|
| No fill | — | Unaffected (1) |
|
|
205
207
|
|
|
@@ -76,11 +76,12 @@ The converter reads a Pedigree Studio session file and:
|
|
|
76
76
|
4. **Gives genetic parents only.** The father and mother columns of a PED
|
|
77
77
|
file are genetic parents. A child joined to a couple by a dashed line (an
|
|
78
78
|
adoption) gets 0 for that couple, and so does the place of a parent marked
|
|
79
|
-
|
|
80
|
-
donor) is a genetic parent.
|
|
81
|
-
same
|
|
79
|
+
G (a gestational carrier), with a note in each case; a parent marked D (a
|
|
80
|
+
donor) is a genetic parent. Drawings made before the 2025 correction to the
|
|
81
|
+
pedigree nomenclature mark a gestational carrier S, which is read the same
|
|
82
|
+
way. studio2ga4gh's PED-style pedigree gives the same.
|
|
82
83
|
5. **Extracts phenotypes** from the colour key: solid fills, half fills,
|
|
83
|
-
|
|
84
|
+
symbols in thirds or quarters, and shading patterns are all recognised.
|
|
84
85
|
6. **Detects carrier notation**: centre dots (●) and half-filled symbols mark
|
|
85
86
|
carriers.
|
|
86
87
|
7. **Chooses the format**:
|
|
@@ -113,9 +114,10 @@ would be `0`, the person is given their pedigree number instead, and a note
|
|
|
113
114
|
says so.
|
|
114
115
|
|
|
115
116
|
Pedigree numbers follow the drawing, as in Pedigree Studio: a new generation
|
|
116
|
-
starts 80 pixels or more below the first
|
|
117
|
-
|
|
118
|
-
|
|
117
|
+
starts 80 pixels or more below the first person of the generation above, and
|
|
118
|
+
each generation is numbered from the left. Only people are numbered: the "no
|
|
119
|
+
children" and "infertility" symbols are not people, so they have no number
|
|
120
|
+
and do not count in their row.
|
|
119
121
|
|
|
120
122
|
## Output formats
|
|
121
123
|
|
|
@@ -176,7 +178,7 @@ colour key:
|
|
|
176
178
|
| Full shading (stripes/dots) | `shading:pattern:full` | Affected (2) |
|
|
177
179
|
| Half colour fill (one side only) | `#rrggbb` | Carrier (3) |
|
|
178
180
|
| Half shading | `shading:pattern:half-left` | Carrier (3) |
|
|
179
|
-
|
|
|
181
|
+
| Thirds or quarters (any part) | `#rrggbb` | Affected (2) |
|
|
180
182
|
| Centre dot (●) | — | Carrier (3) |
|
|
181
183
|
| No fill | — | Unaffected (1) |
|
|
182
184
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""studio2ped — Convert Pedigree Studio session JSON to PED/MPED pedigree files."""
|
|
2
2
|
|
|
3
|
-
__version__ = "0.2.
|
|
3
|
+
__version__ = "0.2.1"
|
|
4
4
|
|
|
5
5
|
from .converter import ( # noqa: E402
|
|
6
6
|
ID_CHOICES, ConversionResult, convert, convert_file, individual_ids, parse_session, pedigree_numbers,
|
|
@@ -37,6 +37,12 @@ SHAPE_TO_SEX = {
|
|
|
37
37
|
# couple, so they have to be taken out before the family is written.
|
|
38
38
|
MARKERS = {"no-offspring": "no children", "infertile": "infertility"}
|
|
39
39
|
|
|
40
|
+
# The marks inside a symbol that make someone a gestational carrier, who carried a
|
|
41
|
+
# child but is not their genetic parent: G since the 2025 correction to the pedigree
|
|
42
|
+
# nomenclature, and S, which drawings from before it use (a woman who gives the egg
|
|
43
|
+
# and carries the pregnancy is a donor, D, and so a genetic parent)
|
|
44
|
+
GESTATIONAL_CARRIER = ("G", "S")
|
|
45
|
+
|
|
40
46
|
# The kinds of identifier --ids offers (see individual_ids)
|
|
41
47
|
ID_CHOICES = ("numbers", "drawing", "studio", "text")
|
|
42
48
|
|
|
@@ -52,13 +58,14 @@ class Person:
|
|
|
52
58
|
shape: str
|
|
53
59
|
sex: int
|
|
54
60
|
text: str # annotation text (may contain name, dob, etc.)
|
|
55
|
-
fill_mode: str # 'none'|'solid'|'half'|'quartered'|'shading'
|
|
61
|
+
fill_mode: str # 'none'|'solid'|'half'|'thirds'|'quartered'|'shading'
|
|
56
62
|
fill_color: Optional[str] # hex colour for solid fill
|
|
57
63
|
halves: list # [left_colour|None, right_colour|None]
|
|
58
64
|
quarters: list # [TR, BR, BL, TL] colours or None
|
|
65
|
+
thirds: list # [top right, bottom, top left] colours or None
|
|
59
66
|
shading_pattern: str # 'stripes'|'dots'
|
|
60
67
|
shading_coverage: str # 'full'|'half-left'|'half-right'
|
|
61
|
-
center_text: str # 'S'|'P'|'D'|'dot'|''
|
|
68
|
+
center_text: str # 'G'|'S'|'P'|'D'|'dot'|'line'|''
|
|
62
69
|
deceased: bool
|
|
63
70
|
custom_label: str
|
|
64
71
|
father_id: Optional[str] = None # the genetic parents, resolved during graph analysis
|
|
@@ -142,6 +149,7 @@ def parse_session(data: dict) -> tuple[dict[str, Person], list[Partnership], lis
|
|
|
142
149
|
fill_color=pd.get("fillColor"),
|
|
143
150
|
halves=pd.get("halves", [None, None]),
|
|
144
151
|
quarters=pd.get("quarters", [None, None, None, None]),
|
|
152
|
+
thirds=pd.get("thirds") or [None, None, None],
|
|
145
153
|
shading_pattern=pd.get("shadingPattern", "stripes"),
|
|
146
154
|
shading_coverage=pd.get("shadingCoverage", "full"),
|
|
147
155
|
center_text=pd.get("centerText", ""),
|
|
@@ -259,11 +267,13 @@ def resolve_parents(family: Family) -> list:
|
|
|
259
267
|
``drawn_father_id`` and ``drawn_mother_id`` are the couple the drawing
|
|
260
268
|
joins a person to. ``father_id`` and ``mother_id`` are their genetic
|
|
261
269
|
parents, which the PED file gives: a dashed line (an adoption) gives no
|
|
262
|
-
genetic parents, and a parent marked
|
|
263
|
-
|
|
270
|
+
genetic parents, and a parent marked G, a gestational carrier, is not a
|
|
271
|
+
genetic parent, as in studio2ga4gh's PED-style pedigree. Drawings from
|
|
272
|
+
before the 2025 correction to the pedigree nomenclature mark a gestational
|
|
273
|
+
carrier S, which is read the same way.
|
|
264
274
|
|
|
265
275
|
Returns what was not written as a parent, as ``("adopted", child, (a, b))``
|
|
266
|
-
and ``("
|
|
276
|
+
and ``("gestational carrier", child, carrier, "mother" or "father")``.
|
|
267
277
|
"""
|
|
268
278
|
pship_map = {p.id: p for p in family.partnerships}
|
|
269
279
|
left_out = []
|
|
@@ -297,8 +307,8 @@ def resolve_parents(family: Family) -> list:
|
|
|
297
307
|
left_out.append(("adopted", child.id, (p1.id, p2.id)))
|
|
298
308
|
continue
|
|
299
309
|
for parent, role in ((father, "father"), (mother, "mother")):
|
|
300
|
-
if parent.center_text
|
|
301
|
-
left_out.append(("
|
|
310
|
+
if parent.center_text in GESTATIONAL_CARRIER:
|
|
311
|
+
left_out.append(("gestational carrier", child.id, parent.id, role))
|
|
302
312
|
elif role == "father":
|
|
303
313
|
child.father_id = parent.id
|
|
304
314
|
else:
|
|
@@ -426,7 +436,7 @@ def classify_person_phenotypes(
|
|
|
426
436
|
- Full shading matching a shading phenotype → affected (2)
|
|
427
437
|
- Half fill with one half matching a colour phenotype → carrier (3)
|
|
428
438
|
- Half shading matching a shading phenotype with half coverage → carrier (3)
|
|
429
|
-
-
|
|
439
|
+
- Thirds or quarters with any part matching a colour phenotype → affected (2)
|
|
430
440
|
- Centre dot ('dot') → add carrier status for all phenotypes that person has
|
|
431
441
|
- No matching fill → unaffected (1)
|
|
432
442
|
"""
|
|
@@ -451,11 +461,12 @@ def classify_person_phenotypes(
|
|
|
451
461
|
else:
|
|
452
462
|
is_half_colour[c] = False
|
|
453
463
|
|
|
454
|
-
elif person.fill_mode
|
|
455
|
-
|
|
464
|
+
elif person.fill_mode in ("quartered", "thirds"):
|
|
465
|
+
# a symbol in thirds or quarters: one part for each condition, each affected
|
|
466
|
+
for c in ((person.quarters if person.fill_mode == "quartered" else person.thirds) or []):
|
|
456
467
|
if c:
|
|
457
468
|
person_colours.add(c)
|
|
458
|
-
is_half_colour[c] = False
|
|
469
|
+
is_half_colour[c] = False
|
|
459
470
|
|
|
460
471
|
# Match colour phenotypes
|
|
461
472
|
for ph in phenotypes:
|
|
@@ -520,12 +531,13 @@ def has_carrier_notation(persons: dict[str, Person]) -> bool:
|
|
|
520
531
|
def drawn_rows(persons: dict[str, Person]) -> list[list[Person]]:
|
|
521
532
|
"""The rows (generations) of the drawing as Pedigree Studio numbers them.
|
|
522
533
|
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
534
|
+
The people are sorted from top to bottom; a new row starts 80 px or more
|
|
535
|
+
below the first person of the row above, and each row is read from left to
|
|
536
|
+
right. The "no children" and "infertile" symbols are not people, so they
|
|
537
|
+
are left out and do not count in their row.
|
|
526
538
|
"""
|
|
527
539
|
rows: list[list[Person]] = []
|
|
528
|
-
for p in sorted(persons.values(), key=lambda q: q.y):
|
|
540
|
+
for p in sorted((q for q in persons.values() if not q.is_marker), key=lambda q: q.y):
|
|
529
541
|
if rows and p.y - rows[-1][0].y < 80:
|
|
530
542
|
rows[-1].append(p)
|
|
531
543
|
else:
|
|
@@ -539,7 +551,8 @@ def _generation_label(index: int) -> str:
|
|
|
539
551
|
|
|
540
552
|
def pedigree_numbers(persons: dict[str, Person]) -> dict[str, str]:
|
|
541
553
|
"""Pedigree numbers as Pedigree Studio shows them: the generation in Roman
|
|
542
|
-
numerals and the position from the left, such as ``II-3``.
|
|
554
|
+
numerals and the position from the left, such as ``II-3``. Only people are
|
|
555
|
+
numbered: the "no children" and "infertile" symbols have no number."""
|
|
543
556
|
out = {}
|
|
544
557
|
for gi, row in enumerate(drawn_rows(persons)):
|
|
545
558
|
for pi, p in enumerate(row):
|
|
@@ -556,14 +569,17 @@ def _letters(n: int) -> str:
|
|
|
556
569
|
|
|
557
570
|
|
|
558
571
|
def drawing_labels(persons: dict[str, Person], numbering_mode: str = "numbers") -> dict[str, str]:
|
|
559
|
-
"""The label each
|
|
572
|
+
"""The label each person shows in the drawing under its numbering setting:
|
|
560
573
|
pedigree numbers (``numbers``, and ``off``), letters (``letters``: II-A;
|
|
561
574
|
``lettersonly``: A, B, C ...) or the labels typed in (``freetext``, where a
|
|
562
|
-
|
|
575
|
+
person without one shows their pedigree number). The "no children" and
|
|
576
|
+
"infertile" symbols have no label."""
|
|
563
577
|
numbers = pedigree_numbers(persons)
|
|
564
578
|
if numbering_mode == "freetext":
|
|
565
579
|
out = {}
|
|
566
580
|
for pid, p in persons.items():
|
|
581
|
+
if p.is_marker:
|
|
582
|
+
continue
|
|
567
583
|
label = (p.custom_label or "").strip()
|
|
568
584
|
out[pid] = label if label and label != "Lbl" else numbers[pid]
|
|
569
585
|
return out
|
|
@@ -598,8 +614,9 @@ def individual_ids(
|
|
|
598
614
|
* ``text``: version 0.1.0's identifiers (see :func:`make_individual_id`),
|
|
599
615
|
which are often names.
|
|
600
616
|
|
|
601
|
-
``persons``
|
|
602
|
-
|
|
617
|
+
``persons`` may include the "no children" and "infertile" symbols: they
|
|
618
|
+
are not people, so they are given no identifier and, as in Pedigree
|
|
619
|
+
Studio, do not count when a row is numbered. Identifiers must
|
|
603
620
|
be unique, so anyone whose identifier would be shared with someone else, or
|
|
604
621
|
would be empty or ``0`` (which PED reads as "no parent"), is given their
|
|
605
622
|
pedigree number instead, and a note says so.
|
|
@@ -900,8 +917,9 @@ def convert(
|
|
|
900
917
|
f"parents are not genetic parents, so they are not written as {id_map[child]}'s parents")
|
|
901
918
|
else:
|
|
902
919
|
_, child, carrier, role = item
|
|
903
|
-
|
|
904
|
-
|
|
920
|
+
mark = fam.persons[carrier].center_text
|
|
921
|
+
notes.append(f"{id_map[carrier]} carried {id_map[child]} as a gestational carrier ({mark}), and a "
|
|
922
|
+
f"gestational carrier is not a genetic parent, so {id_map[child]}'s {role} is written as 0")
|
|
905
923
|
assign_generations(fam)
|
|
906
924
|
|
|
907
925
|
# Extract phenotypes from legend
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: studio2ped
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Convert Pedigree Studio session JSON to PED/MPED pedigree files
|
|
5
5
|
Author-email: Tim Hearn <tjh70@cam.ac.uk>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -99,11 +99,12 @@ The converter reads a Pedigree Studio session file and:
|
|
|
99
99
|
4. **Gives genetic parents only.** The father and mother columns of a PED
|
|
100
100
|
file are genetic parents. A child joined to a couple by a dashed line (an
|
|
101
101
|
adoption) gets 0 for that couple, and so does the place of a parent marked
|
|
102
|
-
|
|
103
|
-
donor) is a genetic parent.
|
|
104
|
-
same
|
|
102
|
+
G (a gestational carrier), with a note in each case; a parent marked D (a
|
|
103
|
+
donor) is a genetic parent. Drawings made before the 2025 correction to the
|
|
104
|
+
pedigree nomenclature mark a gestational carrier S, which is read the same
|
|
105
|
+
way. studio2ga4gh's PED-style pedigree gives the same.
|
|
105
106
|
5. **Extracts phenotypes** from the colour key: solid fills, half fills,
|
|
106
|
-
|
|
107
|
+
symbols in thirds or quarters, and shading patterns are all recognised.
|
|
107
108
|
6. **Detects carrier notation**: centre dots (●) and half-filled symbols mark
|
|
108
109
|
carriers.
|
|
109
110
|
7. **Chooses the format**:
|
|
@@ -136,9 +137,10 @@ would be `0`, the person is given their pedigree number instead, and a note
|
|
|
136
137
|
says so.
|
|
137
138
|
|
|
138
139
|
Pedigree numbers follow the drawing, as in Pedigree Studio: a new generation
|
|
139
|
-
starts 80 pixels or more below the first
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
starts 80 pixels or more below the first person of the generation above, and
|
|
141
|
+
each generation is numbered from the left. Only people are numbered: the "no
|
|
142
|
+
children" and "infertility" symbols are not people, so they have no number
|
|
143
|
+
and do not count in their row.
|
|
142
144
|
|
|
143
145
|
## Output formats
|
|
144
146
|
|
|
@@ -199,7 +201,7 @@ colour key:
|
|
|
199
201
|
| Full shading (stripes/dots) | `shading:pattern:full` | Affected (2) |
|
|
200
202
|
| Half colour fill (one side only) | `#rrggbb` | Carrier (3) |
|
|
201
203
|
| Half shading | `shading:pattern:half-left` | Carrier (3) |
|
|
202
|
-
|
|
|
204
|
+
| Thirds or quarters (any part) | `#rrggbb` | Affected (2) |
|
|
203
205
|
| Centre dot (●) | — | Carrier (3) |
|
|
204
206
|
| No fill | — | Unaffected (1) |
|
|
205
207
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"""
|
|
3
3
|
studio2ped_standalone.py - Convert Pedigree Studio session JSON to PED/MPED files.
|
|
4
4
|
|
|
5
|
-
Standalone single-file script: studio2ped 0.2.
|
|
5
|
+
Standalone single-file script: studio2ped 0.2.1 in one file, so no
|
|
6
6
|
installation is required - it needs only Python 3.9+.
|
|
7
7
|
|
|
8
8
|
Usage:
|
|
@@ -23,7 +23,7 @@ from collections import Counter, defaultdict
|
|
|
23
23
|
from dataclasses import dataclass, field
|
|
24
24
|
from typing import Optional
|
|
25
25
|
|
|
26
|
-
__version__ = "0.2.
|
|
26
|
+
__version__ = "0.2.1"
|
|
27
27
|
|
|
28
28
|
# ── PED phenotype codes ───────────────────────────────────────────────────
|
|
29
29
|
PHENO_UNKNOWN = 0
|
|
@@ -43,6 +43,12 @@ SHAPE_TO_SEX = {
|
|
|
43
43
|
# couple, so they have to be taken out before the family is written.
|
|
44
44
|
MARKERS = {"no-offspring": "no children", "infertile": "infertility"}
|
|
45
45
|
|
|
46
|
+
# The marks inside a symbol that make someone a gestational carrier, who carried a
|
|
47
|
+
# child but is not their genetic parent: G since the 2025 correction to the pedigree
|
|
48
|
+
# nomenclature, and S, which drawings from before it use (a woman who gives the egg
|
|
49
|
+
# and carries the pregnancy is a donor, D, and so a genetic parent)
|
|
50
|
+
GESTATIONAL_CARRIER = ("G", "S")
|
|
51
|
+
|
|
46
52
|
# The kinds of identifier --ids offers (see individual_ids)
|
|
47
53
|
ID_CHOICES = ("numbers", "drawing", "studio", "text")
|
|
48
54
|
|
|
@@ -58,13 +64,14 @@ class Person:
|
|
|
58
64
|
shape: str
|
|
59
65
|
sex: int
|
|
60
66
|
text: str # annotation text (may contain name, dob, etc.)
|
|
61
|
-
fill_mode: str # 'none'|'solid'|'half'|'quartered'|'shading'
|
|
67
|
+
fill_mode: str # 'none'|'solid'|'half'|'thirds'|'quartered'|'shading'
|
|
62
68
|
fill_color: Optional[str] # hex colour for solid fill
|
|
63
69
|
halves: list # [left_colour|None, right_colour|None]
|
|
64
70
|
quarters: list # [TR, BR, BL, TL] colours or None
|
|
71
|
+
thirds: list # [top right, bottom, top left] colours or None
|
|
65
72
|
shading_pattern: str # 'stripes'|'dots'
|
|
66
73
|
shading_coverage: str # 'full'|'half-left'|'half-right'
|
|
67
|
-
center_text: str # 'S'|'P'|'D'|'dot'|''
|
|
74
|
+
center_text: str # 'G'|'S'|'P'|'D'|'dot'|'line'|''
|
|
68
75
|
deceased: bool
|
|
69
76
|
custom_label: str
|
|
70
77
|
father_id: Optional[str] = None # the genetic parents, resolved during graph analysis
|
|
@@ -148,6 +155,7 @@ def parse_session(data: dict) -> tuple[dict[str, Person], list[Partnership], lis
|
|
|
148
155
|
fill_color=pd.get("fillColor"),
|
|
149
156
|
halves=pd.get("halves", [None, None]),
|
|
150
157
|
quarters=pd.get("quarters", [None, None, None, None]),
|
|
158
|
+
thirds=pd.get("thirds") or [None, None, None],
|
|
151
159
|
shading_pattern=pd.get("shadingPattern", "stripes"),
|
|
152
160
|
shading_coverage=pd.get("shadingCoverage", "full"),
|
|
153
161
|
center_text=pd.get("centerText", ""),
|
|
@@ -265,11 +273,13 @@ def resolve_parents(family: Family) -> list:
|
|
|
265
273
|
``drawn_father_id`` and ``drawn_mother_id`` are the couple the drawing
|
|
266
274
|
joins a person to. ``father_id`` and ``mother_id`` are their genetic
|
|
267
275
|
parents, which the PED file gives: a dashed line (an adoption) gives no
|
|
268
|
-
genetic parents, and a parent marked
|
|
269
|
-
|
|
276
|
+
genetic parents, and a parent marked G, a gestational carrier, is not a
|
|
277
|
+
genetic parent, as in studio2ga4gh's PED-style pedigree. Drawings from
|
|
278
|
+
before the 2025 correction to the pedigree nomenclature mark a gestational
|
|
279
|
+
carrier S, which is read the same way.
|
|
270
280
|
|
|
271
281
|
Returns what was not written as a parent, as ``("adopted", child, (a, b))``
|
|
272
|
-
and ``("
|
|
282
|
+
and ``("gestational carrier", child, carrier, "mother" or "father")``.
|
|
273
283
|
"""
|
|
274
284
|
pship_map = {p.id: p for p in family.partnerships}
|
|
275
285
|
left_out = []
|
|
@@ -303,8 +313,8 @@ def resolve_parents(family: Family) -> list:
|
|
|
303
313
|
left_out.append(("adopted", child.id, (p1.id, p2.id)))
|
|
304
314
|
continue
|
|
305
315
|
for parent, role in ((father, "father"), (mother, "mother")):
|
|
306
|
-
if parent.center_text
|
|
307
|
-
left_out.append(("
|
|
316
|
+
if parent.center_text in GESTATIONAL_CARRIER:
|
|
317
|
+
left_out.append(("gestational carrier", child.id, parent.id, role))
|
|
308
318
|
elif role == "father":
|
|
309
319
|
child.father_id = parent.id
|
|
310
320
|
else:
|
|
@@ -432,7 +442,7 @@ def classify_person_phenotypes(
|
|
|
432
442
|
- Full shading matching a shading phenotype → affected (2)
|
|
433
443
|
- Half fill with one half matching a colour phenotype → carrier (3)
|
|
434
444
|
- Half shading matching a shading phenotype with half coverage → carrier (3)
|
|
435
|
-
-
|
|
445
|
+
- Thirds or quarters with any part matching a colour phenotype → affected (2)
|
|
436
446
|
- Centre dot ('dot') → add carrier status for all phenotypes that person has
|
|
437
447
|
- No matching fill → unaffected (1)
|
|
438
448
|
"""
|
|
@@ -457,11 +467,12 @@ def classify_person_phenotypes(
|
|
|
457
467
|
else:
|
|
458
468
|
is_half_colour[c] = False
|
|
459
469
|
|
|
460
|
-
elif person.fill_mode
|
|
461
|
-
|
|
470
|
+
elif person.fill_mode in ("quartered", "thirds"):
|
|
471
|
+
# a symbol in thirds or quarters: one part for each condition, each affected
|
|
472
|
+
for c in ((person.quarters if person.fill_mode == "quartered" else person.thirds) or []):
|
|
462
473
|
if c:
|
|
463
474
|
person_colours.add(c)
|
|
464
|
-
is_half_colour[c] = False
|
|
475
|
+
is_half_colour[c] = False
|
|
465
476
|
|
|
466
477
|
# Match colour phenotypes
|
|
467
478
|
for ph in phenotypes:
|
|
@@ -526,12 +537,13 @@ def has_carrier_notation(persons: dict[str, Person]) -> bool:
|
|
|
526
537
|
def drawn_rows(persons: dict[str, Person]) -> list[list[Person]]:
|
|
527
538
|
"""The rows (generations) of the drawing as Pedigree Studio numbers them.
|
|
528
539
|
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
540
|
+
The people are sorted from top to bottom; a new row starts 80 px or more
|
|
541
|
+
below the first person of the row above, and each row is read from left to
|
|
542
|
+
right. The "no children" and "infertile" symbols are not people, so they
|
|
543
|
+
are left out and do not count in their row.
|
|
532
544
|
"""
|
|
533
545
|
rows: list[list[Person]] = []
|
|
534
|
-
for p in sorted(persons.values(), key=lambda q: q.y):
|
|
546
|
+
for p in sorted((q for q in persons.values() if not q.is_marker), key=lambda q: q.y):
|
|
535
547
|
if rows and p.y - rows[-1][0].y < 80:
|
|
536
548
|
rows[-1].append(p)
|
|
537
549
|
else:
|
|
@@ -545,7 +557,8 @@ def _generation_label(index: int) -> str:
|
|
|
545
557
|
|
|
546
558
|
def pedigree_numbers(persons: dict[str, Person]) -> dict[str, str]:
|
|
547
559
|
"""Pedigree numbers as Pedigree Studio shows them: the generation in Roman
|
|
548
|
-
numerals and the position from the left, such as ``II-3``.
|
|
560
|
+
numerals and the position from the left, such as ``II-3``. Only people are
|
|
561
|
+
numbered: the "no children" and "infertile" symbols have no number."""
|
|
549
562
|
out = {}
|
|
550
563
|
for gi, row in enumerate(drawn_rows(persons)):
|
|
551
564
|
for pi, p in enumerate(row):
|
|
@@ -562,14 +575,17 @@ def _letters(n: int) -> str:
|
|
|
562
575
|
|
|
563
576
|
|
|
564
577
|
def drawing_labels(persons: dict[str, Person], numbering_mode: str = "numbers") -> dict[str, str]:
|
|
565
|
-
"""The label each
|
|
578
|
+
"""The label each person shows in the drawing under its numbering setting:
|
|
566
579
|
pedigree numbers (``numbers``, and ``off``), letters (``letters``: II-A;
|
|
567
580
|
``lettersonly``: A, B, C ...) or the labels typed in (``freetext``, where a
|
|
568
|
-
|
|
581
|
+
person without one shows their pedigree number). The "no children" and
|
|
582
|
+
"infertile" symbols have no label."""
|
|
569
583
|
numbers = pedigree_numbers(persons)
|
|
570
584
|
if numbering_mode == "freetext":
|
|
571
585
|
out = {}
|
|
572
586
|
for pid, p in persons.items():
|
|
587
|
+
if p.is_marker:
|
|
588
|
+
continue
|
|
573
589
|
label = (p.custom_label or "").strip()
|
|
574
590
|
out[pid] = label if label and label != "Lbl" else numbers[pid]
|
|
575
591
|
return out
|
|
@@ -604,8 +620,9 @@ def individual_ids(
|
|
|
604
620
|
* ``text``: version 0.1.0's identifiers (see :func:`make_individual_id`),
|
|
605
621
|
which are often names.
|
|
606
622
|
|
|
607
|
-
``persons``
|
|
608
|
-
|
|
623
|
+
``persons`` may include the "no children" and "infertile" symbols: they
|
|
624
|
+
are not people, so they are given no identifier and, as in Pedigree
|
|
625
|
+
Studio, do not count when a row is numbered. Identifiers must
|
|
609
626
|
be unique, so anyone whose identifier would be shared with someone else, or
|
|
610
627
|
would be empty or ``0`` (which PED reads as "no parent"), is given their
|
|
611
628
|
pedigree number instead, and a note says so.
|
|
@@ -906,8 +923,9 @@ def convert(
|
|
|
906
923
|
f"parents are not genetic parents, so they are not written as {id_map[child]}'s parents")
|
|
907
924
|
else:
|
|
908
925
|
_, child, carrier, role = item
|
|
909
|
-
|
|
910
|
-
|
|
926
|
+
mark = fam.persons[carrier].center_text
|
|
927
|
+
notes.append(f"{id_map[carrier]} carried {id_map[child]} as a gestational carrier ({mark}), and a "
|
|
928
|
+
f"gestational carrier is not a genetic parent, so {id_map[child]}'s {role} is written as 0")
|
|
911
929
|
assign_generations(fam)
|
|
912
930
|
|
|
913
931
|
# Extract phenotypes from legend
|
|
@@ -66,16 +66,19 @@ def test_people_are_given_their_pedigree_numbers():
|
|
|
66
66
|
|
|
67
67
|
|
|
68
68
|
def test_numbers_follow_pedigree_studio():
|
|
69
|
-
"""A new generation starts 80 px or more below the first
|
|
70
|
-
numbered from the left
|
|
69
|
+
"""A new generation starts 80 px or more below the first person of the one above; each generation is
|
|
70
|
+
numbered from the left. The "no children" and "infertile" symbols are not people: as in Pedigree Studio,
|
|
71
|
+
they have no number and do not count in their row."""
|
|
71
72
|
session = {"persons": [
|
|
72
73
|
person("a", x=300, y=100), person("b", "female", x=100, y=150), # 50 px lower: still generation I
|
|
73
74
|
person("c", x=200, y=179), # 79 px below a: still I
|
|
74
75
|
person("d", "female", x=50, y=180), # 80 px below a: generation II
|
|
75
76
|
person("m", "no-offspring", x=10, y=200), person("e", x=400, y=210),
|
|
77
|
+
person("n", "infertile", x=20, y=300), # a row with only a bar is no generation
|
|
78
|
+
person("f", x=30, y=390),
|
|
76
79
|
], "partnerships": [], "childLinks": []}
|
|
77
80
|
numbers = studio2ped.pedigree_numbers(studio2ped.parse_session(session)[0])
|
|
78
|
-
assert numbers == {"b": "I-1", "c": "I-2", "a": "I-3", "
|
|
81
|
+
assert numbers == {"b": "I-1", "c": "I-2", "a": "I-3", "d": "II-1", "e": "II-2", "f": "III-1"}
|
|
79
82
|
deep = {"persons": [person("p{}".format(i), y=i * 100) for i in range(14)], "partnerships": [], "childLinks": []}
|
|
80
83
|
numbers = studio2ped.pedigree_numbers(studio2ped.parse_session(deep)[0])
|
|
81
84
|
assert numbers["p11"] == "XII-1" and numbers["p12"] == "13-1"
|
|
@@ -106,6 +109,26 @@ def test_ids_text_gives_version_0_1_0s_identifiers():
|
|
|
106
109
|
assert any("'26'" in n and "IV-1 and IV-3" in n for n in result.notes)
|
|
107
110
|
|
|
108
111
|
|
|
112
|
+
def test_the_bars_have_no_label_in_any_numbering():
|
|
113
|
+
"""Letters, and labels typed in, skip the "no children" and "infertile" symbols as numbers do."""
|
|
114
|
+
session = {"persons": [
|
|
115
|
+
person("f", x=100, y=0), person("m", "female", x=200, y=0),
|
|
116
|
+
person("bar", "no-offspring", x=0, y=150, customLabel="from an older drawing"),
|
|
117
|
+
person("c1", x=100, y=150, customLabel="Kid"), person("c2", "female", x=200, y=150),
|
|
118
|
+
], "partnerships": [{"id": "pr", "fromId": "f", "toId": "m"}],
|
|
119
|
+
"childLinks": [{"id": "l1", "partnershipId": "pr", "childId": "c1"},
|
|
120
|
+
{"id": "l2", "partnershipId": "pr", "childId": "c2"}]}
|
|
121
|
+
ids = {mode: studio2ped.convert(dict(session, numberingMode=mode), ids="drawing").ids
|
|
122
|
+
for mode in ("numbers", "letters", "lettersonly", "freetext")}
|
|
123
|
+
assert ids["numbers"] == {"f": "I-1", "m": "I-2", "c1": "II-1", "c2": "II-2"}
|
|
124
|
+
assert ids["letters"] == {"f": "I-A", "m": "I-B", "c1": "II-A", "c2": "II-B"}
|
|
125
|
+
assert ids["lettersonly"] == {"f": "A", "m": "B", "c1": "C", "c2": "D"}
|
|
126
|
+
assert ids["freetext"] == {"f": "I-1", "m": "I-2", "c1": "Kid", "c2": "II-2"}
|
|
127
|
+
from studio2ped.converter import drawing_labels
|
|
128
|
+
everyone = studio2ped.parse_session(dict(session, numberingMode="freetext"))[0]
|
|
129
|
+
assert "bar" not in drawing_labels(everyone, "freetext") and "bar" not in drawing_labels(everyone, "letters")
|
|
130
|
+
|
|
131
|
+
|
|
109
132
|
def test_ids_studio_and_drawing():
|
|
110
133
|
session = load("okafor")
|
|
111
134
|
studio_ids = studio2ped.convert(session, ids="studio").ids
|
|
@@ -180,7 +203,7 @@ def test_adoptive_and_surrogate_parents_are_not_genetic_parents():
|
|
|
180
203
|
assert table["III-1"][2:4] == ["II-2", "0"] # II-3, the mother in the drawing, is a surrogate
|
|
181
204
|
assert table["III-3"][2:4] == ["0", "0"] # adopted by II-6 and II-7
|
|
182
205
|
assert table["III-2"][2:4] == ["II-5", "II-4"] # a donor (D) is a genetic parent
|
|
183
|
-
assert any("II-3 carried III-1 as a
|
|
206
|
+
assert any("II-3 carried III-1 as a gestational carrier (S)" in n for n in features.notes)
|
|
184
207
|
assert any("II-6 and II-7 adopted III-3" in n for n in features.notes)
|
|
185
208
|
ids = [r[1] for r in rows(features.files[0][1])]
|
|
186
209
|
assert ids.index("III-3") > ids.index("II-7") # still listed with their generation
|
|
@@ -188,6 +211,36 @@ def test_adoptive_and_surrogate_parents_are_not_genetic_parents():
|
|
|
188
211
|
assert {r[1]: r for r in rows(price.files[0][1])}["III-5"][2:4] == ["0", "0"]
|
|
189
212
|
|
|
190
213
|
|
|
214
|
+
def test_a_parent_marked_g_is_a_gestational_carrier():
|
|
215
|
+
"""Since the 2025 correction to the nomenclature, Pedigree Studio marks a gestational carrier G: read as S was."""
|
|
216
|
+
session = load("features")
|
|
217
|
+
marked = [p for p in session["persons"] if p.get("centerText") == "S"]
|
|
218
|
+
assert len(marked) == 1
|
|
219
|
+
marked[0]["centerText"] = "G"
|
|
220
|
+
features = studio2ped.convert(session)
|
|
221
|
+
table = {r[1]: r for r in rows(features.files[0][1])}
|
|
222
|
+
assert table["III-1"][2:4] == ["II-2", "0"] # II-3, the mother in the drawing, is the gestational carrier
|
|
223
|
+
assert any("II-3 carried III-1 as a gestational carrier (G)" in n for n in features.notes)
|
|
224
|
+
assert not any("surrogate" in n for n in features.notes)
|
|
225
|
+
assert studio2ped.convert(load("features")).files == features.files # G and S give the same file
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def test_a_symbol_in_thirds_has_each_condition():
|
|
229
|
+
"""Pedigree Studio can divide a symbol into thirds, one part for each of up to three conditions (Bennett et al.
|
|
230
|
+
2022): each colour in it is a condition the person has, as with quarters."""
|
|
231
|
+
session = {"persons": [person("p1", "male", 0, 0), person("p2", "female", 104, 0),
|
|
232
|
+
person("p3", "male", 52, 130, fillMode="thirds", thirds=["#c0392b", "#2980b9", None]),
|
|
233
|
+
person("p4", "female", 156, 130, fillMode="quartered", quarters=["#c0392b", None, None, None])],
|
|
234
|
+
"partnerships": [{"id": "ps1", "fromId": "p1", "toId": "p2"}],
|
|
235
|
+
"childLinks": [{"id": "c1", "partnershipId": "ps1", "childId": "p3"},
|
|
236
|
+
{"id": "c2", "partnershipId": "ps1", "childId": "p4"}],
|
|
237
|
+
"legendLabels": {"#c0392b": "Condition A", "#2980b9": "Condition B"}}
|
|
238
|
+
table = {r[1]: r for r in rows(studio2ped.convert(session).files[0][1])}
|
|
239
|
+
assert table["II-1"][5:] == ["2", "2"]
|
|
240
|
+
assert table["II-2"][5:] == ["2", "1"]
|
|
241
|
+
assert table["I-1"][5:] == ["1", "1"]
|
|
242
|
+
|
|
243
|
+
|
|
191
244
|
def test_labels_become_identifiers_as_in_studio2ga4gh():
|
|
192
245
|
"""Spaces become underscores and other punctuation is dropped, the same rule studio2ga4gh uses, so a person
|
|
193
246
|
has the same identifier in the PED file and the GA4GH files."""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|