@@ -3568,6 +3568,57 @@ def test_compute_mutation_parents_restores_on_index_error(self):
35683568 tables .compute_mutation_parents ()
35693569 assert tables .mutations .parent [0 ] == 123
35703570
3571+ def test_compute_mutation_parents_tolerates_various_invalid_values (self ):
3572+ tables = tskit .TableCollection (sequence_length = 1.0 )
3573+ parent = tables .nodes .add_row (time = 1.0 )
3574+ child = tables .nodes .add_row (time = 0 , flags = tskit .NODE_IS_SAMPLE )
3575+ tables .edges .add_row (left = 0.0 , right = 1.0 , parent = parent , child = child )
3576+ site = tables .sites .add_row (position = 0.0 , ancestral_state = "A" )
3577+ tables .mutations .add_row (site = site , node = child , derived_state = "C" )
3578+ tables .build_index ()
3579+
3580+ # A range of nonsensical parent values should be ignored
3581+ invalid_values = [
3582+ - 2 , # less than NULL sentinel
3583+ 0 , # equal to self for single-row case
3584+ 1 , # out of bounds (>= num_rows)
3585+ 42 , # arbitrary out of bounds
3586+ np .iinfo (np .int32 ).max ,
3587+ ]
3588+ for val in invalid_values :
3589+ tables .mutations .parent [:] = val
3590+ tables .compute_mutation_parents ()
3591+ assert tables .mutations .parent [0 ] == tskit .NULL
3592+
3593+ def test_compute_mutation_parents_tolerates_cross_site_and_loops (self ):
3594+ # Build a simple tree with 2 samples under a common parent
3595+ tables = tskit .TableCollection (sequence_length = 1.0 )
3596+ root = tables .nodes .add_row (time = 2.0 )
3597+ a = tables .nodes .add_row (time = 0 , flags = tskit .NODE_IS_SAMPLE )
3598+ b = tables .nodes .add_row (time = 0 , flags = tskit .NODE_IS_SAMPLE )
3599+ tables .edges .add_row (0.0 , 1.0 , root , a )
3600+ tables .edges .add_row (0.0 , 1.0 , root , b )
3601+ s0 = tables .sites .add_row (0.0 , "A" )
3602+ s1 = tables .sites .add_row (0.5 , "A" )
3603+ m0 = tables .mutations .add_row (site = s0 , node = a , derived_state = "C" )
3604+ m1 = tables .mutations .add_row (site = s1 , node = b , derived_state = "G" )
3605+ assert m0 == 0 and m1 == 1
3606+ tables .build_index ()
3607+
3608+ # Cross-site parent should be ignored by compute_mutation_parents
3609+ tables .mutations .parent [:] = np .array ([tskit .NULL , 0 ], dtype = np .int32 )
3610+ tables .compute_mutation_parents ()
3611+ assert np .array_equal (
3612+ tables .mutations .parent , np .array ([tskit .NULL , tskit .NULL ])
3613+ )
3614+
3615+ # Explicit loop in parents should be ignored by compute_mutation_parents
3616+ tables .mutations .parent [:] = np .array ([1 , 0 ], dtype = np .int32 )
3617+ tables .compute_mutation_parents ()
3618+ assert np .array_equal (
3619+ tables .mutations .parent , np .array ([tskit .NULL , tskit .NULL ])
3620+ )
3621+
35713622 def test_str (self ):
35723623 ts = msprime .simulate (10 , random_seed = 1 )
35733624 tables = ts .tables
@@ -5768,3 +5819,90 @@ def test_ragged_selection_indices_non_monotonic():
57685819 gather = _ragged_selection_indices (indexed_offsets , lengths64 )
57695820 expected = np .array ([5 , 0 , 1 ], dtype = np .int64 )
57705821 assert np .array_equal (gather , expected )
5822+
5823+
5824+ class TestMutationParentValidation :
5825+ def _two_leaf_tree (self ):
5826+ tables = tskit .TableCollection (sequence_length = 1.0 )
5827+ root = tables .nodes .add_row (time = 2.0 )
5828+ a = tables .nodes .add_row (time = 0 , flags = tskit .NODE_IS_SAMPLE )
5829+ b = tables .nodes .add_row (time = 0 , flags = tskit .NODE_IS_SAMPLE )
5830+ tables .edges .add_row (0.0 , 1.0 , root , a )
5831+ tables .edges .add_row (0.0 , 1.0 , root , b )
5832+ return tables , a , b
5833+
5834+ def _chain_tree (self ):
5835+ tables = tskit .TableCollection (sequence_length = 1.0 )
5836+ root = tables .nodes .add_row (time = 2.0 )
5837+ mid = tables .nodes .add_row (time = 1.0 )
5838+ leaf = tables .nodes .add_row (time = 0 , flags = tskit .NODE_IS_SAMPLE )
5839+ tables .edges .add_row (0.0 , 1.0 , root , mid )
5840+ tables .edges .add_row (0.0 , 1.0 , mid , leaf )
5841+ return tables , mid , leaf
5842+
5843+ def test_tree_sequence_bad_mutation_parent_topology (self ):
5844+ tables , a , b = self ._two_leaf_tree ()
5845+ s = tables .sites .add_row (0.0 , "A" )
5846+ tables .mutations .add_row (site = s , node = a , derived_state = "C" ) # id 0
5847+ tables .mutations .add_row (site = s , node = b , derived_state = "G" ) # id 1
5848+ # Make a mutation on a parallel branch the parent
5849+ mut_cols = tables .mutations .asdict ()
5850+ mut_cols ["parent" ] = np .array ([tskit .NULL , 0 ], dtype = np .int32 )
5851+ tables .mutations .set_columns (** mut_cols )
5852+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_BAD_MUTATION_PARENT" ):
5853+ tables .tree_sequence ()
5854+
5855+ def test_tree_sequence_mutation_parent_after_child (self ):
5856+ tables , mid , leaf = self ._chain_tree ()
5857+ s = tables .sites .add_row (0.0 , "A" )
5858+ tables .mutations .add_row (site = s , node = leaf , derived_state = "C" ) # id 0 (child)
5859+ tables .mutations .add_row (site = s , node = mid , derived_state = "G" ) # id 1 (parent)
5860+ tables .sort ()
5861+ mut_cols = tables .mutations .asdict ()
5862+ mut_cols ["parent" ] = np .array ([1 , tskit .NULL ], dtype = np .int32 )
5863+ tables .mutations .set_columns (** mut_cols )
5864+ with pytest .raises (
5865+ tskit .LibraryError , match = "TSK_ERR_MUTATION_PARENT_AFTER_CHILD"
5866+ ):
5867+ tables .tree_sequence ()
5868+
5869+ def test_tree_sequence_mutation_parent_different_site (self ):
5870+ tables , a , _ = self ._two_leaf_tree ()
5871+ s0 = tables .sites .add_row (0.0 , "A" )
5872+ s1 = tables .sites .add_row (0.5 , "A" )
5873+ tables .mutations .add_row (site = s0 , node = a , derived_state = "C" ) # id 0
5874+ tables .mutations .add_row (site = s1 , node = a , derived_state = "G" ) # id 1
5875+ mut_cols = tables .mutations .asdict ()
5876+ mut_cols ["parent" ] = np .array ([tskit .NULL , 0 ], dtype = np .int32 )
5877+ tables .mutations .set_columns (** mut_cols )
5878+ with pytest .raises (
5879+ tskit .LibraryError , match = "TSK_ERR_MUTATION_PARENT_DIFFERENT_SITE"
5880+ ):
5881+ tables .tree_sequence ()
5882+
5883+ def test_tree_sequence_mutation_parent_equal (self ):
5884+ tables , a , _ = self ._two_leaf_tree ()
5885+ s = tables .sites .add_row (0.0 , "A" )
5886+ tables .mutations .add_row (site = s , node = a , derived_state = "C" ) # id 0
5887+ mut_cols = tables .mutations .asdict ()
5888+ mut_cols ["parent" ] = np .array ([0 ], dtype = np .int32 )
5889+ tables .mutations .set_columns (** mut_cols )
5890+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_MUTATION_PARENT_EQUAL" ):
5891+ tables .tree_sequence ()
5892+
5893+ def test_tree_sequence_mutation_parent_out_of_bounds (self ):
5894+ tables , a , _ = self ._two_leaf_tree ()
5895+ s = tables .sites .add_row (0.0 , "A" )
5896+ tables .mutations .add_row (site = s , node = a , derived_state = "C" ) # id 0
5897+ # >= num_rows
5898+ mut_cols = tables .mutations .asdict ()
5899+ mut_cols ["parent" ] = np .array ([1 ], dtype = np .int32 )
5900+ tables .mutations .set_columns (** mut_cols )
5901+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_MUTATION_OUT_OF_BOUNDS" ):
5902+ tables .tree_sequence ()
5903+ # < NULL
5904+ mut_cols = tables .mutations .asdict ()
5905+ mut_cols ["parent" ] = np .array ([- 2 ], dtype = np .int32 )
5906+ tables .mutations .set_columns (** mut_cols )
5907+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_MUTATION_OUT_OF_BOUNDS" ):
5908+ tables .tree_sequence ()
0 commit comments