Skip to content

Commit 09b8efb

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

4 files changed

Lines changed: 101 additions & 2 deletions

File tree

python/CHANGELOG.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ In development
1111

1212
- Add ``TreeSequence.ld_matrix`` stats method and documentation, for computing
1313
two-locus statistics in site and branch mode.
14-
(:user:`lkirk`, :user:`apragsdale`, :pr:`3416`)
14+
(:user:`lkirk`, :user:`apragsdale`, :pr:`3416`)
15+
- Add `node_labels` parameter to `write_nexus`. (:user:`kaathewisegit`, :pr:`3442`)
1516

1617
--------------------
1718
[1.0.2] - 2026-03-06

python/tests/test_phylo_formats.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626

2727
import functools
2828
import io
29+
import random
2930
import textwrap
3031

3132
import dendropy
@@ -334,6 +335,85 @@ def test_nexus_no_trees_or_alignments(self):
334335
)
335336

336337

338+
class TestNexusNodeLabels:
339+
@tests.cached_example
340+
def balanced_tree(self):
341+
# 4
342+
# ┏━┻┓
343+
# ┃ 3
344+
# ┃ ┏┻┓
345+
# 0 1 2
346+
return tskit.Tree.generate_balanced(3)
347+
348+
def test_as_nexus_labels_basic(self):
349+
ts = self.balanced_tree().tree_sequence
350+
labels = {0: "human", 1: "chimp", 2: "bonobo"}
351+
expected = textwrap.dedent(
352+
"""\
353+
#NEXUS
354+
BEGIN TAXA;
355+
DIMENSIONS NTAX=3;
356+
TAXLABELS human chimp bonobo;
357+
END;
358+
BEGIN TREES;
359+
TRANSLATE n0 human, n1 chimp, n2 bonobo;
360+
TREE t0^1 = [&R] (n0:2,(n1:1,n2:1):1);
361+
END;
362+
"""
363+
)
364+
assert expected == ts.as_nexus(include_alignments=False, node_labels=labels)
365+
366+
def test_as_nexus_labels_partial(self):
367+
ts = self.balanced_tree().tree_sequence
368+
labels = {0: "human", 2: "bonobo"}
369+
expected = textwrap.dedent(
370+
"""\
371+
#NEXUS
372+
BEGIN TAXA;
373+
DIMENSIONS NTAX=3;
374+
TAXLABELS human n1 bonobo;
375+
END;
376+
BEGIN TREES;
377+
TRANSLATE n0 human, n2 bonobo;
378+
TREE t0^1 = [&R] (n0:2,(n1:1,n2:1):1);
379+
END;
380+
"""
381+
)
382+
assert expected == ts.as_nexus(include_alignments=False, node_labels=labels)
383+
384+
def test_as_nexus_labels_none(self):
385+
ts = self.balanced_tree().tree_sequence
386+
expected = textwrap.dedent(
387+
"""\
388+
#NEXUS
389+
BEGIN TAXA;
390+
DIMENSIONS NTAX=3;
391+
TAXLABELS n0 n1 n2;
392+
END;
393+
BEGIN TREES;
394+
TREE t0^1 = [&R] (n0:2,(n1:1,n2:1):1);
395+
END;
396+
"""
397+
)
398+
assert expected == ts.as_nexus(include_alignments=False, node_labels=None)
399+
400+
@pytest.mark.parametrize("ts", get_example_tree_sequences())
401+
def test_parseable(self, ts):
402+
for tree in ts.trees():
403+
if not tree.has_single_root:
404+
return
405+
406+
labels = {}
407+
samples = ts.samples()
408+
k = random.randint(1, len(samples))
409+
for node in random.sample(list(samples), k):
410+
labels[node] = f"new_node_which_was_{node}"
411+
412+
nexus = ts.as_nexus(include_alignments=False, node_labels=labels)
413+
print(nexus)
414+
dendropy.DataSet.get(data=nexus, schema="nexus")
415+
416+
337417
class TestNewickCodePaths:
338418
"""
339419
Test that the different code paths we use under the hood lead to

python/tskit/text_formats.py

Lines changed: 13 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,13 @@ 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+
taxlabels = " ".join(
141+
node_labels[u] if u in node_labels else f"n{u}" for u in ts.samples()
142+
)
143+
else:
144+
taxlabels = " ".join(f"n{u}" for u in ts.samples())
138145
print("", f"TAXLABELS {taxlabels};", sep=indent, file=out)
139146
print("END;", file=out)
140147

@@ -166,6 +173,11 @@ def write_nexus(
166173
include_trees = True if include_trees is None else include_trees
167174
if include_trees:
168175
print("BEGIN TREES;", file=out)
176+
177+
if node_labels is not None:
178+
translations = ", ".join(f"n{u} {name}" for u, name in node_labels.items())
179+
print(f" TRANSLATE {translations};", file=out)
180+
169181
for tree in ts.trees():
170182
start_interval = "{0:.{1}f}".format(tree.interval.left, pos_precision)
171183
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)