diff --git a/docs/metadata.md b/docs/metadata.md index ce0ffc4c42..51ba511b8a 100644 --- a/docs/metadata.md +++ b/docs/metadata.md @@ -153,6 +153,70 @@ time *units* of a tree sequence should be stored in the metadata. For example, we could set `tables.time_units = "generations"`. ::: + +(sec_metadata_top_level_pitfalls)= + +### Pitfalls related to metadata + +Briefly, the pitfalls are: + +1. editing metadata directly can silently do nothing, and +2. making repeated calls to top-level metadata will make your code very slow + if that metadata is large. + +One thing that it is important to know about metadata is that metadata access methods +return a *copy* of the *decoded* information. +So, for instance, each time you call +```{code-cell} +tables.metadata +``` +(or ``ts.metadata``), it gives you a dictionary containing a *new copy* of the +underlying information. So, this means that you cannot edit metadata +by direct assignment. For instance, trying to change the metadata like this +silently does nothing: +```{code-cell} +tables.metadata["taxonomy"]["subspecies"] = "lyrata" +print(tables.metadata["taxonomy"]["subspecies"]) +``` +This is true for all types of metadata: +for instance, editing `ts.mutation(0).metadata` will not change the metadata +of the underlying mutation. +See {ref}`sec_tutorial_metadata` for examples of modifying metadata in tables. +To edit top-level metadata, first copy out the metadata, edit *that*, +and then put it back: +```{code-cell} +md = tables.metadata +md["taxonomy"]["species"] = "lyrata" +tables.metadata = md +``` + +This fact about metadata can have important consequences for performance. +For instance, if we'd like to convert all mutation times to generations, +we should **definitely not** run +``[mut.time / ts.metadata["generation_time"] for mut in ts.mutations()]``. +This is because a new copy of the top-level metadata will be created +for *every* mutation in the tree sequence. +This may not be a big deal for some tree sequences, +but this will take prohibitively long for tree sequences +that have a large amount of information in top-level metadata +(for instance, those produced by SLiM v6). +Instead, it is good practice to make a copy of the top-level metadata, +and use that copy henceforth, like so: +```{code-cell} +:tags: ["skip-execution"] +top_md = ts.metadata +mut_times = [ + mut.time / top_md["generation_time"] for mut in ts.mutations() +] +``` + +Because of this, tskit will produce a Warning if the top-level metadata +is large (greater than 100Kb) and is accessed many times (more than 20 times). +This behavior can be changed, by setting +{data}`tskit.METADATA_ACCESS_WARNING_THRESHHOLD` +or {data}`tskit.METADATA_ACCESS_WARNING_SIZE`. + + (sec_metadata_examples_reference_sequence)= ### Reference sequence @@ -284,8 +348,9 @@ must be encoded and decoded. The C API does not do this, but the Python API will use the schema to decode the metadata to Python objects. The encoding for doing this is specified in the top-level schema property `codec`. Currently the Python API supports the `json` codec which encodes metadata as -[JSON](https://www.json.org/json-en.html), and the `struct` codec which encodes -metadata in an efficient schema-defined binary format using {func}`python:struct.pack` . +[JSON](https://www.json.org/json-en.html), the `struct` codec which encodes +metadata in an efficient schema-defined binary format using {func}`python:struct.pack`, +and the `json+struct` codec which is a combination of the two. (sec_metadata_codecs_json)= @@ -436,6 +501,12 @@ The supported numeric and boolean types are: - 8 ``` +In addition to the `binaryFormat` encoding given in the table above, +the `type` key must also be set to the appropriate value. +For boolean values the `type` should be `boolean` (not bool); +for integer binary formats it should be `integer` (not number), +and for floating-point binary formats it should be `number`. + When attempting to pack a non-integer using any of the integer conversion codes, if the non-integer has a `__index__` method then that method is called to convert the argument to an integer before packing. @@ -602,7 +673,7 @@ into 8-byte alignment; and (7) the binary data. The JSON data is encoded as ASCII, without a null terminating byte, and the format of the binary data is specified using the "struct" portion -of the metadata schema, described :ref:`above `. +of the metadata schema, described {ref}`above `. (sec_metadata_schema_examples)= diff --git a/docs/python-api.md b/docs/python-api.md index 6ed59f981c..e8f7572890 100644 --- a/docs/python-api.md +++ b/docs/python-api.md @@ -54,6 +54,7 @@ sequences. TreeSequence.discrete_genome TreeSequence.discrete_time TreeSequence.metadata + TreeSequence.metadata_size TreeSequence.metadata_schema TreeSequence.reference_sequence ``` @@ -723,6 +724,7 @@ Other properties TableCollection.nbytes TableCollection.table_name_map TableCollection.metadata + TableCollection.metadata_size TableCollection.metadata_bytes TableCollection.metadata_schema TableCollection.sequence_length diff --git a/python/CHANGELOG.rst b/python/CHANGELOG.rst index 190b762e1c..bdd0543206 100644 --- a/python/CHANGELOG.rst +++ b/python/CHANGELOG.rst @@ -10,6 +10,13 @@ - The returned object from ``variant.counts()`` and ``variant.frequencies()`` now stores alleles in the order defined in ``variant.alleles``. (:user:`hyanwong`, :pr:`3471`) +- If top-level metadata is large, then repeatedly accessing it can be costly, + so now it throws a warning if the metadata is more than 200Kb and it is + accessed more than 20 times alerting the user to this possibility (both + constants are modifiable, however). + (:user:`petrelharp`, :issue:`3472`, :pr:`3475`) +- TreeSequences and TableCollections now have a ``metadata_size`` property, + returning their size in bytes. (:user:`petrelharp`, :pr:`3475`) -------------------- [1.0.3] - 2026-05-14 diff --git a/python/_tskitmodule.c b/python/_tskitmodule.c index 0e0c1c5ed5..01bcf9fb7a 100644 --- a/python/_tskitmodule.c +++ b/python/_tskitmodule.c @@ -4050,6 +4050,19 @@ TableCollection_get_metadata(TableCollection *self, void *closure) return ret; } +static PyObject * +TableCollection_get_metadata_size(TableCollection *self, void *closure) +{ + PyObject *ret = NULL; + + if (TableCollection_check_state(self) != 0) { + goto out; + } + ret = Py_BuildValue("n", (Py_ssize_t) self->tables->metadata_length); +out: + return ret; +} + static int TableCollection_set_metadata(TableCollection *self, PyObject *arg, void *closure) { @@ -5075,6 +5088,9 @@ static PyGetSetDef TableCollection_getsetters[] = { .get = (getter) TableCollection_get_metadata, .set = (setter) TableCollection_set_metadata, .doc = "The metadata." }, + { .name = "metadata_size", + .get = (getter) TableCollection_get_metadata_size, + .doc = "Returns the size of the metadata, in bytes." }, { .name = "metadata_schema", .get = (getter) TableCollection_get_metadata_schema, .set = (setter) TableCollection_set_metadata_schema, @@ -5590,6 +5606,19 @@ TreeSequence_get_metadata(TreeSequence *self) return ret; } +static PyObject * +TreeSequence_get_metadata_size(TreeSequence *self) +{ + PyObject *ret = NULL; + + if (TreeSequence_check_state(self) != 0) { + goto out; + } + ret = Py_BuildValue("n", (Py_ssize_t) self->tree_sequence->tables->metadata_length); +out: + return ret; +} + static PyObject * TreeSequence_get_metadata_schema(TreeSequence *self) { @@ -8759,6 +8788,10 @@ static PyMethodDef TreeSequence_methods[] = { .ml_meth = (PyCFunction) TreeSequence_get_metadata, .ml_flags = METH_NOARGS, .ml_doc = "Returns the metadata for the tree sequence" }, + { .ml_name = "get_metadata_size", + .ml_meth = (PyCFunction) TreeSequence_get_metadata_size, + .ml_flags = METH_NOARGS, + .ml_doc = "Returns the size of the metadata, in bytes." }, { .ml_name = "get_metadata_schema", .ml_meth = (PyCFunction) TreeSequence_get_metadata_schema, .ml_flags = METH_NOARGS, diff --git a/python/tests/test_highlevel.py b/python/tests/test_highlevel.py index e9531451d3..42390a79d1 100644 --- a/python/tests/test_highlevel.py +++ b/python/tests/test_highlevel.py @@ -42,7 +42,7 @@ import unittest import uuid as _uuid import warnings -from xml.etree import ElementTree +from html.parser import HTMLParser import kastore import msprime @@ -1988,7 +1988,7 @@ def test_load_tables(self, ts): def test_html_repr(self, ts): html = ts._repr_html_() # Parse to check valid - ElementTree.fromstring(html) + HTMLParser().feed(html) assert len(html) > 5000 assert f"Trees{ts.num_trees:,}" in html assert f"Time Units{ts.time_units}" in html @@ -3059,19 +3059,24 @@ def test_tree_sequence_metadata(self): tc = tskit.TableCollection(1) ts = tc.tree_sequence() assert ts.metadata == b"" + assert ts.metadata_size == 0 tc.metadata_schema = self.metadata_schema data = { "table": "tree-sequence", "string_prop": "stringy", "num_prop": 42, } + data_enc = self.metadata_schema.validate_and_encode_row(data) tc.metadata = data ts = tc.tree_sequence() assert ts.metadata == data + assert ts.metadata_size == len(data_enc) with pytest.raises(AttributeError): ts.metadata = {"should": "fail"} with pytest.raises(AttributeError): del ts.metadata + with pytest.raises(AttributeError): + ts.metadata_size = 0 def test_tree_sequence_time_units(self): tc = tskit.TableCollection(1) @@ -3712,7 +3717,7 @@ def test_str(self, ts_fixture): def test_html_repr(self, ts_fixture): html = ts_fixture.first()._repr_html_() # Parse to check valid - ElementTree.fromstring(html) + HTMLParser().feed(html) assert len(html) > 1900 assert "Total Branch Length" in html diff --git a/python/tests/test_immutable_table_collection.py b/python/tests/test_immutable_table_collection.py index 5c244fb1f5..ea49d8b451 100644 --- a/python/tests/test_immutable_table_collection.py +++ b/python/tests/test_immutable_table_collection.py @@ -30,6 +30,7 @@ def test_basic_properties_match(self, ts): assert mutable.file_uuid == immutable.file_uuid assert mutable.metadata_schema == immutable.metadata_schema assert mutable.metadata == immutable.metadata + assert mutable.metadata_size == immutable.metadata_size assert mutable.metadata_schema.encode_row(mutable.metadata) == bytes( immutable.metadata_bytes ) diff --git a/python/tests/test_metadata.py b/python/tests/test_metadata.py index 8faa54e3e3..889134181b 100644 --- a/python/tests/test_metadata.py +++ b/python/tests/test_metadata.py @@ -2775,3 +2775,117 @@ def test_explicit_ordering(self): dtype = metadata.MetadataSchema(schema).numpy_dtype() assert dtype.names == ("id", "name", "age") + + +class TestMetadataAccessCounter: + """ + Test for top-level metadata multiple access warnings. + """ + + big_size = tskit.METADATA_ACCESS_WARNING_SIZE + + def get_example(self, n, what): + # note this cannot be a fixture because the _metadata_access_counter + # will then persist across tests + t = tskit.TableCollection(sequence_length=1) + schema = metadata.MetadataSchema( + { + "codec": "json+struct", + "json": { + "codec": "json", + "type": "object", + "properties": { + "a": {"type": "string"}, + }, + }, + "struct": { + "codec": "struct", + "type": "object", + "properties": { + "x": { + "type": "array", + "arrayLengthFormat": "Q", + "items": {"type": "integer", "binaryFormat": "q"}, + } + }, + }, + } + ) + md = {"a": "bcde", "x": list(range(n))} + t.metadata_schema = schema + t.metadata = md + if what != "tables": + t = t.tree_sequence() + if what == "immutable_tables": + t = t.tables + return t + + def check_not_warns(self, t, num_checks=None): + if num_checks is None: + num_checks = tskit.METADATA_ACCESS_WARNING_THRESHHOLD + 5 + md = t.metadata + for _ in range(num_checks): + x = t.metadata + assert md == x + x["a"] = "this should have no effect" + + def check_warns(self, t): + md = t.metadata + # should warn after METADATA_ACCESS_WARNING_THRESHHOLD times + for _ in range(tskit.METADATA_ACCESS_WARNING_THRESHHOLD - 1): + x = t.metadata + assert md == x + x["a"] = "this should have no effect" + with pytest.warns(UserWarning, match="metadata is large"): + assert md == t.metadata + # and no more after that + for _ in range(5): + x = t.metadata + assert md == x + x["a"] = "this should have no effect" + + @pytest.mark.parametrize("what", ["tables", "ts", "immutable_tables"]) + def test_warns(self, what): + t = self.get_example(self.big_size, what) + self.check_warns(t) + + @pytest.mark.parametrize("what", ["tables", "ts", "immutable_tables"]) + def test_not_warns(self, what): + t = self.get_example(5, what) + self.check_not_warns(t) + + @pytest.mark.parametrize("what", ["tables", "ts", "immutable_tables"]) + def test_change_threshhold_not_warns(self, what, monkeypatch): + orig_threshhold = tskit.METADATA_ACCESS_WARNING_THRESHHOLD + t = self.get_example(self.big_size, what) + monkeypatch.setattr(tskit, "METADATA_ACCESS_WARNING_THRESHHOLD", 1000) + self.check_not_warns(t, num_checks=orig_threshhold + 5) + + @pytest.mark.parametrize("what", ["tables", "ts", "immutable_tables"]) + def test_change_threshhold_warns(self, what, monkeypatch): + t = self.get_example(self.big_size, what) + monkeypatch.setattr(tskit, "METADATA_ACCESS_WARNING_THRESHHOLD", 0) + with pytest.warns(UserWarning, match="metadata is large"): + _ = t.metadata + + @pytest.mark.parametrize("what", ["tables", "ts", "immutable_tables"]) + def test_change_size_warns(self, what, monkeypatch): + t = self.get_example(5, what) + monkeypatch.setattr(tskit, "METADATA_ACCESS_WARNING_SIZE", 1) + self.check_warns(t) + + @pytest.mark.parametrize("what", ["tables", "ts", "immutable_tables"]) + def test_change_size_not_warns(self, what, monkeypatch): + t = self.get_example(self.big_size, what) + # put down the threshhold so this doesn't take forever + monkeypatch.setattr(tskit, "METADATA_ACCESS_WARNING_SIZE", 2**32) + monkeypatch.setattr(tskit, "METADATA_ACCESS_WARNING_THRESHHOLD", 2) + self.check_not_warns(t) + + def test_set_resets_counter(self, monkeypatch): + monkeypatch.setattr(tskit, "METADATA_ACCESS_WARNING_THRESHHOLD", 5) + t = self.get_example(self.big_size, "tables") + self.check_warns(t) + self.check_not_warns(t) + t.metadata = t.metadata + self.check_warns(t) diff --git a/python/tests/test_python_c.py b/python/tests/test_python_c.py index 15f9967f3f..c003a61a54 100644 --- a/python/tests/test_python_c.py +++ b/python/tests/test_python_c.py @@ -311,10 +311,12 @@ def test_set_metadata_errors(self): def test_set_metadata(self): tables = _tskit.TableCollection(1) assert tables.metadata == b"" + assert tables.metadata_size == 0 for value in [b"foo", b"", "💩".encode(), b"null char \0 in string"]: tables.metadata = value tables.metadata_schema = "Test we have two separate fields" assert tables.metadata == value + assert tables.metadata_size == len(value) def test_set_metadata_schema_errors(self): tables = _tskit.TableCollection(1) @@ -1562,11 +1564,13 @@ def test_metadata(self): ts = _tskit.TreeSequence() ts.load_tables(tables) assert ts.get_metadata() == b"" + assert ts.get_metadata_size() == 0 for value in [b"foo", b"", "💩".encode(), b"null char \0 in string"]: tables.metadata = value ts = _tskit.TreeSequence() ts.load_tables(tables) assert ts.get_metadata() == value + assert ts.get_metadata_size() == len(value) def test_metadata_schema(self): tables = _tskit.TableCollection(1) diff --git a/python/tests/test_tables.py b/python/tests/test_tables.py index b706381bfc..d89eca507b 100644 --- a/python/tests/test_tables.py +++ b/python/tests/test_tables.py @@ -4484,18 +4484,23 @@ def test_set_metadata(self): # Default is empty bytes assert tc.metadata == b"" assert tc.metadata_bytes == b"" + assert tc.metadata_size == 0 tc.metadata_schema = self.metadata_schema md1 = self.metadata_example_data() + md1_enc = tskit.canonical_json(md1).encode() md2 = self.metadata_example_data(val=2) + md2_enc = tskit.canonical_json(md2).encode() # Set tc.metadata = md1 assert tc.metadata == md1 - assert tc.metadata_bytes == tskit.canonical_json(md1).encode() + assert tc.metadata_bytes == md1_enc + assert tc.metadata_size == len(md1_enc) # Overwrite tc.metadata = md2 assert tc.metadata == md2 - assert tc.metadata_bytes == tskit.canonical_json(md2).encode() + assert tc.metadata_bytes == md2_enc + assert tc.metadata_size == len(md2_enc) # Del should fail with pytest.raises(AttributeError): del tc.metadata @@ -4507,6 +4512,9 @@ def test_set_metadata(self): # Setting bytes should fail with pytest.raises(AttributeError): tc.metadata_bytes = b"123" + # Setting size should fail + with pytest.raises(AttributeError): + tc.metadata_size = 0 def test_set_time_units(self): tc = tskit.TableCollection(1) @@ -5368,7 +5376,8 @@ def test_one_empty(self): ) tables.assert_equals(ts.dump_tables(), ignore_provenance=True) - # empty union with tables should be tables + # empty union with tables should be tables, + # except for top-level metadata (which should be empty) empty.union( tables, node_mapping=np.full(tables.nodes.num_rows, tskit.NULL), @@ -5376,7 +5385,8 @@ def test_one_empty(self): all_mutations=True, check_shared_equality=False, ) - empty.assert_equals(tables, ignore_provenance=True) + empty.assert_equals(tables, ignore_provenance=True, ignore_ts_metadata=True) + assert empty.metadata == b"" def test_reciprocal_empty(self): # reciprocally add mutations from one table and edges from the other diff --git a/python/tests/tsutil.py b/python/tests/tsutil.py index 0037e06391..a3d3bdab75 100644 --- a/python/tests/tsutil.py +++ b/python/tests/tsutil.py @@ -397,7 +397,7 @@ def single_childify(ts): def add_random_metadata(ts, seed=1, max_length=10): """ Returns a copy of the specified tree sequence with random metadata assigned - to the nodes, sites and mutations. + to the nodes, sites, mutations, and mutations. """ tables = ts.dump_tables() np.random.seed(seed) @@ -464,6 +464,12 @@ def add_random_metadata(ts, seed=1, max_length=10): populations = tables.populations populations.set_columns(metadata_offset=offset, metadata=metadata) + tables.metadata_schema = tskit.MetadataSchema.permissive_json() + tables.metadata = { + "test": [int(x) for x in np.random.randint(0, max_length, 25)], + "foo": "top-level metadata: 🌲🌴🌳", + } + add_provenance(tables.provenances, "add_random_metadata") ts = tables.tree_sequence() return ts diff --git a/python/tskit/__init__.py b/python/tskit/__init__.py index 5777064b31..014cc38a94 100644 --- a/python/tskit/__init__.py +++ b/python/tskit/__init__.py @@ -70,6 +70,13 @@ "provenances", ] +#: Threshhold for warning about multiple top-level metadata use. +#: A warning will be thrown if top-level metadata is larger than "size" bytes and is +#: accessed more that "threshhold" times. To disable, set threshhold to a large number, +#: for instance: ``tskit.METADATA_ACCESS_WARNING_THRESHOLD = 2**32``. +METADATA_ACCESS_WARNING_THRESHHOLD = 20 +#: The minimum size of top-level metadata before a multiple access warning is produced. +METADATA_ACCESS_WARNING_SIZE = 200_000 from tskit.provenance import __version__ # NOQA from tskit.provenance import validate_provenance # NOQA diff --git a/python/tskit/metadata.py b/python/tskit/metadata.py index 76882d3c6b..9d265ecf40 100644 --- a/python/tskit/metadata.py +++ b/python/tskit/metadata.py @@ -34,6 +34,7 @@ import pprint import struct import types +import warnings from collections.abc import Mapping from itertools import islice from typing import Any @@ -81,6 +82,24 @@ def replace_root_refs(obj): TSKITMetadataSchemaValidator.META_SCHEMA = deref_meta_schema +def metadata_access_warning(obj, name): + if obj.metadata_size > tskit.METADATA_ACCESS_WARNING_SIZE: + # Some users of this function are marked immutable + object.__setattr__( + obj, "_metadata_access_counter", obj._metadata_access_counter + 1 + ) + if obj._metadata_access_counter == tskit.METADATA_ACCESS_WARNING_THRESHHOLD + 1: + warnings.warn( + "It looks like you're making repeated calls to " + f"<{name}>.metadata. " + "If metadata is large, this can slow scripts down considerably. " + "Instead, assign metadata to an object and use that, e.g.: " + "ts_metadata = ts.metadata", + UserWarning, + stacklevel=2, + ) + + class AbstractMetadataCodec(metaclass=abc.ABCMeta): """ Superclass of all MetadataCodecs. diff --git a/python/tskit/tables.py b/python/tskit/tables.py index c8d6622d71..f9577fdc44 100644 --- a/python/tskit/tables.py +++ b/python/tskit/tables.py @@ -3058,6 +3058,7 @@ def __init__(self, sequence_length=0, *, ll_tables=None): self._mutations = MutationTable(ll_table=self._ll_tables.mutations) self._populations = PopulationTable(ll_table=self._ll_tables.populations) self._provenances = ProvenanceTable(ll_table=self._ll_tables.provenances) + self._metadata_access_counter = 0 @property def individuals(self) -> IndividualTable: @@ -3196,6 +3197,24 @@ def has_reference_sequence(self): """ return bool(self._ll_tables.has_reference_sequence()) + @property + def metadata_size(self): + """ + The size of the top-level metadata, in bytes. + """ + return self._ll_tables.metadata_size + + @property + def metadata(self): + metadata.metadata_access_warning(self, "table collection") + return self.metadata_schema.decode_row(self.metadata_bytes) + + @metadata.setter + def metadata(self, metadata): + encoded = self.metadata_schema.validate_and_encode_row(metadata) + self._metadata_access_counter = 0 + self._ll_object.metadata = encoded + @property def reference_sequence(self): """ @@ -4640,6 +4659,7 @@ def __init__(self, ll_tree_sequence): self.mutations = ImmutableMutationTable(ll_tree_sequence) self.populations = ImmutablePopulationTable(ll_tree_sequence) self.provenances = ImmutableProvenanceTable(ll_tree_sequence) + self._metadata_access_counter = 0 object.__setattr__(self, "_initialised", True) @property @@ -4662,8 +4682,13 @@ def reference_sequence(self): def metadata_schema(self): return metadata.parse_metadata_schema(self._llts.get_metadata_schema()) + @property + def metadata_size(self): + return self._llts.get_metadata_size() + @property def metadata(self): + metadata.metadata_access_warning(self, "table collection") return self.metadata_schema.decode_row(self.metadata_bytes) @property diff --git a/python/tskit/trees.py b/python/tskit/trees.py index f4014e3978..98659c765f 100644 --- a/python/tskit/trees.py +++ b/python/tskit/trees.py @@ -27,7 +27,6 @@ from __future__ import annotations import base64 -import builtins import collections import concurrent.futures import functools @@ -118,7 +117,7 @@ def store_tree_sequence(cls): # Intercept the init to record the tree_sequence def new_init(self, *args, tree_sequence=None, **kwargs): - builtins.object.__setattr__(self, "_tree_sequence", tree_sequence) + object.__setattr__(self, "_tree_sequence", tree_sequence) wrapped_init(self, *args, **kwargs) cls.__init__ = new_init @@ -4166,6 +4165,7 @@ def __init__(self, ll_tree_sequence): if not name.startswith("_") } self._table_metadata_schemas = TableMetadataSchemas(**metadata_schema_instances) + self._metadata_access_counter = 0 self._individuals_time = None self._individuals_population = None self._individuals_location = None @@ -4609,11 +4609,26 @@ def sequence_length(self): def get_sequence_length(self): return self._ll_tree_sequence.get_sequence_length() + @property + def metadata_size(self): + """ + The size of the top-level metadata, in bytes. + """ + return self._ll_tree_sequence.get_metadata_size() + @property def metadata(self) -> Any: """ The decoded metadata for this TreeSequence. + + This is a *property*, so each time you call TreeSequence.metadata, + the underlying metadata is decoded, and a new copy of the result is + returned (usually as a dictionary). So, calling this many times is + inefficient, and for repeated use the metadata should be stored. For + instance, instead of ``[ts.metadata['t'] - m.time for m in ts.mutations()]``, + do ``md = ts.metadata; [md['t'] - m.time for m in ts.mutations()]``. """ + metadata_module.metadata_access_warning(self, "tree sequence") return self.metadata_schema.decode_row(self._ll_tree_sequence.get_metadata()) @property @@ -7503,7 +7518,7 @@ def subset( Returns a tree sequence containing only information directly referencing the provided list of nodes to retain. The result will retain only the nodes whose IDs are listed in ``nodes``, only edges for - which both parent and child are in ``nodes```, only mutations whose + which both parent and child are in ``nodes``, only mutations whose node is in ``nodes``, and only individuals that are referred to by one of the retained nodes. Note that this does *not* retain the ancestry of these nodes - for that, see :meth:`.simplify`.