Skip to content

Commit 2d69b34

Browse files
Support node_labels in write_nexus via TRANSLATE
Co-authored-by: Jerome Kelleher <jk@well.ox.ac.uk>
1 parent 1835ea3 commit 2d69b34

4 files changed

Lines changed: 96 additions & 1 deletion

File tree

python/CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ In development
77
- Add ``json+struct`` metadata codec that allows storing binary data using a struct
88
schema alongside JSON metadata. (:user:`benjeffery`, :pr:`3306`)
99

10+
- Add `node_labels` parameter to `write_nexus`. (:user:`kaathewisegit`, :pr:`3442`)
11+
1012
--------------------
1113
[1.0.2] - 2026-03-06
1214
--------------------

python/tests/test_phylo_formats.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,73 @@ def test_as_nexus_precision_1(self):
474474
)
475475
assert ts.as_nexus(precision=1) == expected
476476

477+
def test_as_nexus_labels_basic(self):
478+
ts = self.tree().tree_sequence
479+
labels = {0: "human", 1: "chimp", 2: "bonobo"}
480+
expected = textwrap.dedent(
481+
"""\
482+
#NEXUS
483+
BEGIN TAXA;
484+
DIMENSIONS NTAX=3;
485+
TAXLABELS human chimp bonobo;
486+
END;
487+
BEGIN TREES;
488+
TRANSLATE n0 human, n1 chimp, n2 bonobo;
489+
TREE t0^1 = [&R] (n0:2,(n1:1,n2:1):1);
490+
END;
491+
"""
492+
)
493+
assert expected == ts.as_nexus(
494+
include_alignments=False,
495+
node_labels=labels,
496+
)
497+
498+
def test_as_nexus_labels_partial(self):
499+
ts = self.tree().tree_sequence
500+
labels = {0: "human", 2: "bonobo"}
501+
expected = textwrap.dedent(
502+
"""\
503+
#NEXUS
504+
BEGIN TAXA;
505+
DIMENSIONS NTAX=3;
506+
TAXLABELS human n1 bonobo;
507+
END;
508+
BEGIN TREES;
509+
TRANSLATE n0 human, n2 bonobo;
510+
TREE t0^1 = [&R] (n0:2,(n1:1,n2:1):1);
511+
END;
512+
"""
513+
)
514+
assert expected == ts.as_nexus(
515+
include_alignments=False,
516+
node_labels=labels,
517+
)
518+
519+
def test_as_nexus_labels_empty(self):
520+
ts = self.tree().tree_sequence
521+
522+
with pytest.raises(ValueError, match="`node_labels` cannot be empty"):
523+
ts.as_nexus(include_alignments=False, node_labels={})
524+
525+
def test_as_nexus_labels_none(self):
526+
ts = self.tree().tree_sequence
527+
expected = textwrap.dedent(
528+
"""\
529+
#NEXUS
530+
BEGIN TAXA;
531+
DIMENSIONS NTAX=3;
532+
TAXLABELS n0 n1 n2;
533+
END;
534+
BEGIN TREES;
535+
TREE t0^1 = [&R] (n0:2,(n1:1,n2:1):1);
536+
END;
537+
"""
538+
)
539+
assert expected == ts.as_nexus(
540+
include_alignments=False,
541+
node_labels=None,
542+
)
543+
477544

478545
class TestFractionalBranchLengths:
479546
# 0.67┊ 4 ┊

python/tskit/text_formats.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ def write_nexus(
120120
include_alignments,
121121
reference_sequence,
122122
missing_data_character,
123+
node_labels,
123124
isolated_as_missing=None,
124125
):
125126
# See TreeSequence.write_nexus for documentation on parameters.
@@ -134,7 +135,21 @@ def write_nexus(
134135
print("#NEXUS", file=out)
135136
print("BEGIN TAXA;", file=out)
136137
print("", f"DIMENSIONS NTAX={ts.num_samples};", sep=indent, file=out)
137-
taxlabels = " ".join(f"n{u}" for u in ts.samples())
138+
139+
if node_labels is not None:
140+
if len(node_labels) == 0:
141+
raise ValueError("`node_labels` cannot be empty")
142+
143+
if max(node_labels) >= ts.num_samples:
144+
raise ValueError(
145+
"Can only remap samples, got node with id {max(node_labels)}"
146+
)
147+
148+
taxlabels = " ".join(
149+
node_labels[u] if u in node_labels else f"n{u}" for u in ts.samples()
150+
)
151+
else:
152+
taxlabels = " ".join(f"n{u}" for u in ts.samples())
138153
print("", f"TAXLABELS {taxlabels};", sep=indent, file=out)
139154
print("END;", file=out)
140155

@@ -166,6 +181,11 @@ def write_nexus(
166181
include_trees = True if include_trees is None else include_trees
167182
if include_trees:
168183
print("BEGIN TREES;", file=out)
184+
185+
if node_labels is not None:
186+
translations = ", ".join(f"n{u} {name}" for u, name in node_labels.items())
187+
print(f" TRANSLATE {translations};", file=out)
188+
169189
for tree in ts.trees():
170190
start_interval = "{0:.{1}f}".format(tree.interval.left, pos_precision)
171191
end_interval = "{0:.{1}f}".format(tree.interval.right, pos_precision)

python/tskit/trees.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6797,6 +6797,7 @@ def write_nexus(
67976797
reference_sequence=None,
67986798
missing_data_character=None,
67996799
isolated_as_missing=None,
6800+
node_labels=None,
68006801
):
68016802
"""
68026803
Returns a `nexus encoding <https://en.wikipedia.org/wiki/Nexus_file>`_
@@ -6896,6 +6897,10 @@ def write_nexus(
68966897
:param str missing_data_character: As for the :meth:`.alignments` method,
68976898
but defaults to "?".
68986899
:param bool isolated_as_missing: As for the :meth:`.alignments` method.
6900+
:param node_labels: A map of type `{index: name}`. Samples present in
6901+
the map will have the given name instead of `n{index}`. Note that
6902+
the names must not have whitespace (spaces should be replaced by
6903+
underscores) or puncuation in them.
68996904
:return: A nexus representation of this :class:`TreeSequence`
69006905
:rtype: str
69016906
"""
@@ -6908,6 +6913,7 @@ def write_nexus(
69086913
reference_sequence=reference_sequence,
69096914
missing_data_character=missing_data_character,
69106915
isolated_as_missing=isolated_as_missing,
6916+
node_labels=node_labels,
69116917
)
69126918

69136919
def as_nexus(self, **kwargs):

0 commit comments

Comments
 (0)