@@ -3568,6 +3568,53 @@ 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 (tables .mutations .parent , np .array ([tskit .NULL , tskit .NULL ]))
3612+
3613+ # Explicit loop in parents should be ignored by compute_mutation_parents
3614+ tables .mutations .parent [:] = np .array ([1 , 0 ], dtype = np .int32 )
3615+ tables .compute_mutation_parents ()
3616+ assert np .array_equal (tables .mutations .parent , np .array ([tskit .NULL , tskit .NULL ]))
3617+
35713618 def test_str (self ):
35723619 ts = msprime .simulate (10 , random_seed = 1 )
35733620 tables = ts .tables
@@ -5768,3 +5815,86 @@ def test_ragged_selection_indices_non_monotonic():
57685815 gather = _ragged_selection_indices (indexed_offsets , lengths64 )
57695816 expected = np .array ([5 , 0 , 1 ], dtype = np .int64 )
57705817 assert np .array_equal (gather , expected )
5818+
5819+
5820+ class TestMutationParentValidation :
5821+ def _two_leaf_tree (self ):
5822+ tables = tskit .TableCollection (sequence_length = 1.0 )
5823+ root = tables .nodes .add_row (time = 2.0 )
5824+ a = tables .nodes .add_row (time = 0 , flags = tskit .NODE_IS_SAMPLE )
5825+ b = tables .nodes .add_row (time = 0 , flags = tskit .NODE_IS_SAMPLE )
5826+ tables .edges .add_row (0.0 , 1.0 , root , a )
5827+ tables .edges .add_row (0.0 , 1.0 , root , b )
5828+ return tables , a , b
5829+
5830+ def _chain_tree (self ):
5831+ tables = tskit .TableCollection (sequence_length = 1.0 )
5832+ root = tables .nodes .add_row (time = 2.0 )
5833+ mid = tables .nodes .add_row (time = 1.0 )
5834+ leaf = tables .nodes .add_row (time = 0 , flags = tskit .NODE_IS_SAMPLE )
5835+ tables .edges .add_row (0.0 , 1.0 , root , mid )
5836+ tables .edges .add_row (0.0 , 1.0 , mid , leaf )
5837+ return tables , mid , leaf
5838+
5839+ def test_tree_sequence_bad_mutation_parent_topology (self ):
5840+ tables , a , b = self ._two_leaf_tree ()
5841+ s = tables .sites .add_row (0.0 , "A" )
5842+ tables .mutations .add_row (site = s , node = a , derived_state = "C" ) # id 0
5843+ tables .mutations .add_row (site = s , node = b , derived_state = "G" ) # id 1
5844+ # Make a mutation on a parallel branch the parent
5845+ mut_cols = tables .mutations .asdict ()
5846+ mut_cols ["parent" ] = np .array ([tskit .NULL , 0 ], dtype = np .int32 )
5847+ tables .mutations .set_columns (** mut_cols )
5848+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_BAD_MUTATION_PARENT" ):
5849+ tables .tree_sequence ()
5850+
5851+ def test_tree_sequence_mutation_parent_after_child (self ):
5852+ tables , mid , leaf = self ._chain_tree ()
5853+ s = tables .sites .add_row (0.0 , "A" )
5854+ tables .mutations .add_row (site = s , node = leaf , derived_state = "C" ) # id 0 (child)
5855+ tables .mutations .add_row (site = s , node = mid , derived_state = "G" ) # id 1 (parent)
5856+ tables .sort ()
5857+ mut_cols = tables .mutations .asdict ()
5858+ mut_cols ["parent" ] = np .array ([1 , tskit .NULL ], dtype = np .int32 )
5859+ tables .mutations .set_columns (** mut_cols )
5860+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_MUTATION_PARENT_AFTER_CHILD" ):
5861+ tables .tree_sequence ()
5862+
5863+ def test_tree_sequence_mutation_parent_different_site (self ):
5864+ tables , a , _ = self ._two_leaf_tree ()
5865+ s0 = tables .sites .add_row (0.0 , "A" )
5866+ s1 = tables .sites .add_row (0.5 , "A" )
5867+ tables .mutations .add_row (site = s0 , node = a , derived_state = "C" ) # id 0
5868+ tables .mutations .add_row (site = s1 , node = a , derived_state = "G" ) # id 1
5869+ mut_cols = tables .mutations .asdict ()
5870+ mut_cols ["parent" ] = np .array ([tskit .NULL , 0 ], dtype = np .int32 )
5871+ tables .mutations .set_columns (** mut_cols )
5872+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_MUTATION_PARENT_DIFFERENT_SITE" ):
5873+ tables .tree_sequence ()
5874+
5875+ def test_tree_sequence_mutation_parent_equal (self ):
5876+ tables , a , _ = self ._two_leaf_tree ()
5877+ s = tables .sites .add_row (0.0 , "A" )
5878+ tables .mutations .add_row (site = s , node = a , derived_state = "C" ) # id 0
5879+ mut_cols = tables .mutations .asdict ()
5880+ mut_cols ["parent" ] = np .array ([0 ], dtype = np .int32 )
5881+ tables .mutations .set_columns (** mut_cols )
5882+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_MUTATION_PARENT_EQUAL" ):
5883+ tables .tree_sequence ()
5884+
5885+ def test_tree_sequence_mutation_parent_out_of_bounds (self ):
5886+ tables , a , _ = self ._two_leaf_tree ()
5887+ s = tables .sites .add_row (0.0 , "A" )
5888+ tables .mutations .add_row (site = s , node = a , derived_state = "C" ) # id 0
5889+ # >= num_rows
5890+ mut_cols = tables .mutations .asdict ()
5891+ mut_cols ["parent" ] = np .array ([1 ], dtype = np .int32 )
5892+ tables .mutations .set_columns (** mut_cols )
5893+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_MUTATION_OUT_OF_BOUNDS" ):
5894+ tables .tree_sequence ()
5895+ # < NULL
5896+ mut_cols = tables .mutations .asdict ()
5897+ mut_cols ["parent" ] = np .array ([- 2 ], dtype = np .int32 )
5898+ tables .mutations .set_columns (** mut_cols )
5899+ with pytest .raises (tskit .LibraryError , match = "TSK_ERR_MUTATION_OUT_OF_BOUNDS" ):
5900+ tables .tree_sequence ()
0 commit comments