Skip to content

Commit bfc98b2

Browse files
authored
Merge pull request #555 from petrelharp/canonicalize_json
reordered metadata schema and added tests for ordering; closes #543
2 parents 4dd07db + 04da38e commit bfc98b2

2 files changed

Lines changed: 110 additions & 2 deletions

File tree

core/slim_globals.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,7 +1734,7 @@ R"V0G0N({"$schema":"http://json-schema.org/schema#","additionalProperties":false
17341734
// (The string replaced is "%d" *including* the quotes, because the format string needs to
17351735
// itself be a legal JSON string in order to pass SLiM's own internal checks, so beware.)
17361736
const std::string gSLiM_tsk_node_metadata_schema_FORMAT =
1737-
R"V0G0N({"$schema":"http://json-schema.org/schema#","additionalProperties":false,"codec":"struct","description":"SLiM schema for node metadata.","examples":[{"slim_id":123,"is_vacant":0}],"properties":{"slim_id":{"binaryFormat":"q","description":"The 'pedigree ID' of the haplosomes associated with this node in SLiM.","index":0,"type":"integer"},"is_vacant":{"description":"A vector of byte (uint8_t) values, with each bit representing whether the node represents a vacant position, either unused or a null haplosome (1), or a non-null haplosome (0), in the corresponding chromosome. This field encodes vacancy for all of the chromosomes in the model, not just the chromosome represented in this file (so that the node table is identical across all chromosomes for a multi-chromosome model). Each chromosome receives one bit here; there are two node table entries per individual, used for the two haplosomes of every chromosome, so only one bit is needed in each entry (making two bits total per chromosome, across the two node table entries). The least significant bit of the first byte is used first (for one haplosome of the first chromosome); the most significant bit of the last byte is used last. The number of bytes present in this field is indicated by this schema's 'binaryFormat' field, which is variable (!), and can also be deduced from the number of chromosomes in the model as given in the top-level 'chromosomes' metadata key, which should always be present if this metadata is present.","index":1,"type":"array","length":"%d","items":{"type":"number","binaryFormat":"B"}}},"required":["slim_id","is_vacant"],"type":["object","null"]})V0G0N";
1737+
R"V0G0N({"$schema":"http://json-schema.org/schema#","additionalProperties":false,"codec":"struct","description":"SLiM schema for node metadata.","examples":[{"is_vacant":0,"slim_id":123}],"properties":{"is_vacant":{"description":"A vector of byte (uint8_t) values, with each bit representing whether the node represents a vacant position, either unused or a null haplosome (1), or a non-null haplosome (0), in the corresponding chromosome. This field encodes vacancy for all of the chromosomes in the model, not just the chromosome represented in this file (so that the node table is identical across all chromosomes for a multi-chromosome model). Each chromosome receives one bit here; there are two node table entries per individual, used for the two haplosomes of every chromosome, so only one bit is needed in each entry (making two bits total per chromosome, across the two node table entries). The least significant bit of the first byte is used first (for one haplosome of the first chromosome); the most significant bit of the last byte is used last. The number of bytes present in this field is indicated by this schema's 'binaryFormat' field, which is variable (!), and can also be deduced from the number of chromosomes in the model as given in the top-level 'chromosomes' metadata key, which should always be present if this metadata is present.","index":1,"items":{"binaryFormat":"B","type":"number"},"length":"%d","type":"array"},"slim_id":{"binaryFormat":"q","description":"The 'pedigree ID' of the haplosomes associated with this node in SLiM.","index":0,"type":"integer"}},"required":["slim_id","is_vacant"],"type":["object","null"]})V0G0N";
17381738

17391739
const std::string gSLiM_tsk_individual_metadata_schema =
17401740
R"V0G0N({"$schema":"http://json-schema.org/schema#","additionalProperties":false,"codec":"struct","description":"SLiM schema for individual metadata.","examples":[{"age":-1,"flags":0,"pedigree_id":123,"pedigree_p1":12,"pedigree_p2":23,"sex":0,"subpopulation":0}],"flags":{"SLIM_INDIVIDUAL_METADATA_MIGRATED":{"description":"Whether this individual was a migrant, either in the tick when the tree sequence was written out (if the individual was alive then), or in the tick of the last time they were Remembered (if not).","value":1}},"properties":{"age":{"binaryFormat":"i","description":"The age of this individual, either when the tree sequence was written out (if the individual was alive then), or the last time they were Remembered (if not).","index":4,"type":"integer"},"flags":{"binaryFormat":"I","description":"Other information about the individual: see 'flags'.","index":7,"type":"integer"},"pedigree_id":{"binaryFormat":"q","description":"The 'pedigree ID' of this individual in SLiM.","index":1,"type":"integer"},"pedigree_p1":{"binaryFormat":"q","description":"The 'pedigree ID' of this individual's first parent in SLiM.","index":2,"type":"integer"},"pedigree_p2":{"binaryFormat":"q","description":"The 'pedigree ID' of this individual's second parent in SLiM.","index":3,"type":"integer"},"sex":{"binaryFormat":"i","description":"The sex of the individual (0 for female, 1 for male, -1 for hermaphrodite).","index":6,"type":"integer"},"subpopulation":{"binaryFormat":"i","description":"The ID of the subpopulation the individual was part of, either when the tree sequence was written out (if the individual was alive then), or the last time they were Remembered (if not).","index":5,"type":"integer"}},"required":["pedigree_id","pedigree_p1","pedigree_p2","age","subpopulation","sex","flags"],"type":"object"})V0G0N";
@@ -1749,7 +1749,7 @@ R"V0G0N({"$schema":"http://json-schema.org/schema#","additionalProperties":false
17491749
// schema in SLiM since we don't check/validate schemas anyway in SLiM; we will just write out this new schema in
17501750
// SLiM 4, and on read we won't care whether the schema is the 3.7 or the 4.0 schema.
17511751
const std::string gSLiM_tsk_population_metadata_schema =
1752-
R"V0G0N({"$schema":"http://json-schema.org/schema#","additionalProperties":true,"codec":"json","description":"SLiM schema for population metadata.","examples":[{"bounds_x0":0.0,"bounds_x1":100.0,"bounds_y0":0.0,"bounds_y1":100.0,"female_cloning_fraction":0.25,"male_cloning_fraction":0.0,"migration_records":[{"migration_rate":0.9,"source_subpop":1},{"migration_rate":0.1,"source_subpop":2}],"selfing_fraction":0.5,"sex_ratio":0.5,"slim_id":2,"name":"p2"}],"properties":{"bounds_x0":{"description":"The minimum x-coordinate in this subpopulation.","type":"number"},"bounds_x1":{"description":"The maximum x-coordinate in this subpopulation.","type":"number"},"bounds_y0":{"description":"The minimum y-coordinate in this subpopulation.","type":"number"},"bounds_y1":{"description":"The maximum y-coordinate in this subpopulation.","type":"number"},"bounds_z0":{"description":"The minimum z-coordinate in this subpopulation.","type":"number"},"bounds_z1":{"description":"The maximum z-coordinate in this subpopulation.","type":"number"},"description":{"description":"A description of this subpopulation.","type":"string"},"female_cloning_fraction":{"description":"The frequency with which females in this subpopulation reproduce clonally (for WF models).","type":"number"},"male_cloning_fraction":{"description":"The frequency with which males in this subpopulation reproduce clonally (for WF models).","type":"number"},"migration_records":{"items":{"properties":{"migration_rate":{"description":"The fraction of children in this subpopulation that are composed of 'migrants' from the source subpopulation (in WF models).","type":"number"},"source_subpop":{"description":"The ID of the subpopulation migrants come from (in WF models).","type":"integer"}},"required":["source_subpop","migration_rate"],"type":"object"},"type":"array"},"name":{"description":"A human-readable name for this subpopulation.","type":"string"},"selfing_fraction":{"description":"The frequency with which individuals in this subpopulation self (for WF models).","type":"number"},"sex_ratio":{"description":"This subpopulation's sex ratio (for WF models).","type":"number"},"slim_id":{"description":"The ID of this population in SLiM. Note that this is called a 'subpopulation' in SLiM.","type":"integer"}},"required":[],"type":["object","null"]})V0G0N";
1752+
R"V0G0N({"$schema":"http://json-schema.org/schema#","additionalProperties":true,"codec":"json","description":"SLiM schema for population metadata.","examples":[{"bounds_x0":0.0,"bounds_x1":100.0,"bounds_y0":0.0,"bounds_y1":100.0,"female_cloning_fraction":0.25,"male_cloning_fraction":0.0,"migration_records":[{"migration_rate":0.9,"source_subpop":1},{"migration_rate":0.1,"source_subpop":2}],"name":"p2","selfing_fraction":0.5,"sex_ratio":0.5,"slim_id":2}],"properties":{"bounds_x0":{"description":"The minimum x-coordinate in this subpopulation.","type":"number"},"bounds_x1":{"description":"The maximum x-coordinate in this subpopulation.","type":"number"},"bounds_y0":{"description":"The minimum y-coordinate in this subpopulation.","type":"number"},"bounds_y1":{"description":"The maximum y-coordinate in this subpopulation.","type":"number"},"bounds_z0":{"description":"The minimum z-coordinate in this subpopulation.","type":"number"},"bounds_z1":{"description":"The maximum z-coordinate in this subpopulation.","type":"number"},"description":{"description":"A description of this subpopulation.","type":"string"},"female_cloning_fraction":{"description":"The frequency with which females in this subpopulation reproduce clonally (for WF models).","type":"number"},"male_cloning_fraction":{"description":"The frequency with which males in this subpopulation reproduce clonally (for WF models).","type":"number"},"migration_records":{"items":{"properties":{"migration_rate":{"description":"The fraction of children in this subpopulation that are composed of 'migrants' from the source subpopulation (in WF models).","type":"number"},"source_subpop":{"description":"The ID of the subpopulation migrants come from (in WF models).","type":"integer"}},"required":["source_subpop","migration_rate"],"type":"object"},"type":"array"},"name":{"description":"A human-readable name for this subpopulation.","type":"string"},"selfing_fraction":{"description":"The frequency with which individuals in this subpopulation self (for WF models).","type":"number"},"sex_ratio":{"description":"This subpopulation's sex ratio (for WF models).","type":"number"},"slim_id":{"description":"The ID of this population in SLiM. Note that this is called a 'subpopulation' in SLiM.","type":"integer"}},"required":[],"type":["object","null"]})V0G0N";
17531753

17541754

17551755
// *************************************
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
"""
2+
Tests for metadata schema stuff.
3+
Individual recipes can be specified by decorating with
4+
@pytest.mark.parametrize('recipe', ['test_____my_specific_recipe.slim'], indirect=True)
5+
"""
6+
import json
7+
import tskit
8+
import msprime
9+
import pyslim
10+
import pytest
11+
12+
from recipe_specs import recipe_eq
13+
14+
table_names = [
15+
'edges', 'individuals', 'migrations', 'mutations',
16+
'nodes', 'populations', 'sites'
17+
]
18+
19+
# this is for indexing pyslim.slim_metadata_schemas
20+
table_namex = [
21+
'edge', 'individual', 'mutation',
22+
'node', 'population', 'site'
23+
]
24+
25+
class TestMetadataSchemas:
26+
27+
def round_trip_schema(self, table):
28+
a = table.metadata_schema
29+
new_table = table.copy()
30+
new_table.assert_equals(table)
31+
b = new_table.metadata_schema
32+
assert a == b
33+
assert a.schema == b.schema
34+
new_table.clear()
35+
new_table.set_columns(**table.asdict())
36+
new_table.assert_equals(table)
37+
b = new_table.metadata_schema
38+
assert a == b
39+
assert a.schema == b.schema
40+
41+
def copy_schema(self, table):
42+
new_table = table.__class__()
43+
new_table.schema = table.schema
44+
assert new_table.schema == table.schema
45+
assert new_table.schema.schema == table.schema.schema
46+
47+
@pytest.mark.parametrize('recipe, table_name', indirect=['recipe'], argvalues=[
48+
('test_____pop_names_pX.slim', x) for x in table_names
49+
])
50+
def test_slim_canonical_json(self, recipe, table_name):
51+
# Here we want to test that whatever tskit does to the schema
52+
# when copying tables doesn't change the underlying .schema;
53+
# I can't figure out a good way to actually do that besides
54+
# making a copy of the tables.
55+
assert len(recipe["results"]) == 1
56+
ts = recipe["results"][0].get_normal_ts()
57+
t = ts.tables.table_name_map[table_name]
58+
tc = t.copy()
59+
# this should be true since it's been canonicalized
60+
# to produce the repr() which gets compared:
61+
assert t.metadata_schema == tc.metadata_schema
62+
if t.metadata_schema.schema != tc.metadata_schema.schema:
63+
# print out JSON that should give us a correct schema
64+
# so we can put it in the SLiM code
65+
j = tskit.canonical_json(tc.metadata_schema.schema)
66+
# this should succeed because json.loads returns an unordered
67+
# dictionary, and the two differ only on ordering
68+
assert json.loads(j) == t.metadata_schema.schema
69+
# put back in the placeholder
70+
if table_name == "nodes":
71+
u = '"length":1'
72+
ix = j.find(u)
73+
j = j[:ix] + '"length":"%d"' + j[(ix+len(u)):]
74+
print(f"Schema for {table_name} does not match. "
75+
"Use this (equivalent) JSON.")
76+
print(".................... begin")
77+
print(j)
78+
print("...................... end")
79+
assert t.metadata_schema.schema == tc.metadata_schema.schema
80+
81+
@pytest.mark.parametrize('recipe, table_name', indirect=['recipe'], argvalues=[
82+
('test_____pop_names_pX.slim', x) for x in table_names
83+
])
84+
def test_slim_round_trip(self, recipe, table_name):
85+
assert len(recipe["results"]) == 1
86+
ts = recipe["results"][0].get_normal_ts()
87+
self.round_trip_schema(ts.tables.table_name_map[table_name])
88+
89+
@pytest.mark.parametrize('table_name', argvalues=table_names)
90+
def test_annotate_round_trip(self, table_name):
91+
ts = msprime.sim_ancestry(1, sequence_length=10, random_seed=123)
92+
ts = msprime.sim_mutations(ts, rate=1, random_seed=123)
93+
assert ts.num_mutations > 0
94+
t = ts.dump_tables()
95+
pyslim.annotate_tables(t, model_type='WF', tick=1)
96+
self.round_trip_schema(t.table_name_map[table_name])
97+
98+
@pytest.mark.parametrize('table_name', argvalues=table_namex)
99+
def test_json_round_trip(self, table_name):
100+
# pyslim schema -> json -> schema
101+
orig = pyslim.slim_metadata_schemas[table_name]
102+
j = tskit.canonical_json(orig.schema)
103+
new = tskit.MetadataSchema(json.loads(j))
104+
assert new == orig
105+
assert new.schema == orig.schema
106+
107+
108+

0 commit comments

Comments
 (0)