|
| 1 | +import msprime |
| 2 | +import pytest |
| 3 | + |
| 4 | +import tskit_arg_visualizer as argviz |
| 5 | + |
| 6 | + |
| 7 | +def _example_d3arg(): |
| 8 | + ts = msprime.sim_ancestry( |
| 9 | + samples=4, |
| 10 | + sequence_length=100, |
| 11 | + recombination_rate=1e-2, |
| 12 | + record_full_arg=True, |
| 13 | + ploidy=1, |
| 14 | + random_seed=123, |
| 15 | + ) |
| 16 | + return ts, argviz.D3ARG.from_ts(ts) |
| 17 | + |
| 18 | + |
| 19 | +class TestRenderingSmoke: |
| 20 | + def test_draw_returns_drawinfo_without_opening_browser(self, monkeypatch): |
| 21 | + _, d3arg = _example_d3arg() |
| 22 | + |
| 23 | + # Keep test non-interactive while still exercising HTML generation path. |
| 24 | + monkeypatch.setattr(argviz, "display", lambda *_args, **_kwargs: None) |
| 25 | + |
| 26 | + info = d3arg.draw( |
| 27 | + width=320, |
| 28 | + height=240, |
| 29 | + tree_highlighting=False, |
| 30 | + show_mutations=False, |
| 31 | + is_notebook=True, |
| 32 | + ) |
| 33 | + |
| 34 | + assert isinstance(info, argviz.DrawInfo) |
| 35 | + # Width/height may be adjusted internally (e.g., axis/title spacing), |
| 36 | + # so only assert broad sanity constraints here. |
| 37 | + assert float(info.width) >= 320 |
| 38 | + assert float(info.height) >= 240 |
| 39 | + assert info.uid.startswith("arg_") |
| 40 | + |
| 41 | + included_nodes = info.included.nodes |
| 42 | + assert isinstance(included_nodes, list) |
| 43 | + assert len(included_nodes) > 0 |
| 44 | + assert set(included_nodes).issubset(set(d3arg.nodes["id"])) |
| 45 | + |
| 46 | + def test_draw_nodes_returns_drawinfo_without_opening_browser(self, monkeypatch): |
| 47 | + _, d3arg = _example_d3arg() |
| 48 | + |
| 49 | + monkeypatch.setattr(argviz, "display", lambda *_args, **_kwargs: None) |
| 50 | + |
| 51 | + info = d3arg.draw_nodes( |
| 52 | + seed_nodes=[d3arg.sample_order[0]], |
| 53 | + depth=1, |
| 54 | + width=320, |
| 55 | + height=240, |
| 56 | + tree_highlighting=False, |
| 57 | + show_mutations=False, |
| 58 | + is_notebook=True, |
| 59 | + ) |
| 60 | + |
| 61 | + assert isinstance(info, argviz.DrawInfo) |
| 62 | + assert float(info.width) >= 320 |
| 63 | + assert float(info.height) >= 240 |
| 64 | + assert info.uid.startswith("arg_") |
| 65 | + |
| 66 | + included_nodes = info.included.nodes |
| 67 | + assert isinstance(included_nodes, list) |
| 68 | + assert len(included_nodes) > 0 |
| 69 | + assert set(included_nodes).issubset(set(d3arg.nodes["id"])) |
| 70 | + |
| 71 | + def test_draw_genome_bar_smoke_notebook_mode(self, monkeypatch): |
| 72 | + _, d3arg = _example_d3arg() |
| 73 | + |
| 74 | + monkeypatch.setattr(argviz, "display", lambda *_args, **_kwargs: None) |
| 75 | + |
| 76 | + result = d3arg.draw_genome_bar( |
| 77 | + width=320, |
| 78 | + show_mutations=True, |
| 79 | + is_notebook=True, |
| 80 | + ) |
| 81 | + assert result is None |
| 82 | + |
| 83 | + |
| 84 | +class TestGraphSubsetSmoke: |
| 85 | + def test_from_ts_and_subset_graph_smoke(self): |
| 86 | + ts, d3arg = _example_d3arg() |
| 87 | + |
| 88 | + assert d3arg.num_samples == ts.num_samples |
| 89 | + assert not d3arg.nodes.empty |
| 90 | + assert not d3arg.edges.empty |
| 91 | + |
| 92 | + subset = d3arg.subset_graph(seed_nodes=[d3arg.sample_order[0]], depth=1) |
| 93 | + assert not subset.nodes.empty |
| 94 | + assert not subset.edges.empty |
| 95 | + |
| 96 | + node_ids = set(subset.nodes["id"].tolist()) |
| 97 | + assert set(subset.edges["source"]).issubset(node_ids) |
| 98 | + assert set(subset.edges["target"]).issubset(node_ids) |
| 99 | + assert set(subset.mutations["edge"]).issubset(set(subset.edges["id"])) |
| 100 | + |
| 101 | + |
| 102 | +class TestSecondaryPositionUtilities: |
| 103 | + def test_extract_x_positions_from_json_smoke(self): |
| 104 | + arg_no_labels = { |
| 105 | + "width": 500, |
| 106 | + "y_axis": {"include_labels": False}, |
| 107 | + "data": { |
| 108 | + "nodes": [ |
| 109 | + {"id": 1, "x": 50}, |
| 110 | + {"id": 2, "x": 450}, |
| 111 | + ] |
| 112 | + }, |
| 113 | + } |
| 114 | + pos_no_labels = argviz.extract_x_positions_from_json(arg_no_labels) |
| 115 | + assert pos_no_labels[1] == pytest.approx(0.0) |
| 116 | + assert pos_no_labels[2] == pytest.approx(1.0) |
| 117 | + |
| 118 | + arg_with_labels = { |
| 119 | + "width": 500, |
| 120 | + "y_axis": {"include_labels": True}, |
| 121 | + "data": { |
| 122 | + "nodes": [ |
| 123 | + {"id": 1, "x": 150}, |
| 124 | + {"id": 2, "x": 450}, |
| 125 | + ] |
| 126 | + }, |
| 127 | + } |
| 128 | + pos_with_labels = argviz.extract_x_positions_from_json(arg_with_labels) |
| 129 | + assert pos_with_labels[1] == pytest.approx(0.0) |
| 130 | + assert pos_with_labels[2] == pytest.approx(1.0) |
| 131 | + |
| 132 | + def test_calculate_evenly_distributed_positions_smoke(self): |
| 133 | + positions = argviz.calculate_evenly_distributed_positions( |
| 134 | + num_elements=5, start=0, end=1, round_to=3 |
| 135 | + ) |
| 136 | + assert len(positions) == 5 |
| 137 | + assert positions[0] == 0 |
| 138 | + assert positions[-1] == 1 |
| 139 | + assert positions == sorted(positions) |
| 140 | + |
| 141 | + midpoint = argviz.calculate_evenly_distributed_positions( |
| 142 | + num_elements=1, start=10, end=20, round_to=3 |
| 143 | + ) |
| 144 | + assert midpoint == [15.0] |
| 145 | + |
| 146 | + def test_convert_time_to_position_monotonic_in_time_scale(self): |
| 147 | + y_shift = 7 |
| 148 | + height = 200 |
| 149 | + y_for_recent = argviz.convert_time_to_position( |
| 150 | + t=0, |
| 151 | + min_time=0, |
| 152 | + max_time=10, |
| 153 | + scale="time", |
| 154 | + unique_times=[0, 5, 10], |
| 155 | + h_spacing=0.5, |
| 156 | + height=height, |
| 157 | + y_shift=y_shift, |
| 158 | + ) |
| 159 | + y_for_ancient = argviz.convert_time_to_position( |
| 160 | + t=10, |
| 161 | + min_time=0, |
| 162 | + max_time=10, |
| 163 | + scale="time", |
| 164 | + unique_times=[0, 5, 10], |
| 165 | + h_spacing=0.5, |
| 166 | + height=height, |
| 167 | + y_shift=y_shift, |
| 168 | + ) |
| 169 | + assert y_for_ancient < y_for_recent |
| 170 | + |
| 171 | + def test_convert_time_to_position_rank_requires_known_time(self): |
| 172 | + with pytest.raises(RuntimeError): |
| 173 | + argviz.convert_time_to_position( |
| 174 | + t=3, |
| 175 | + min_time=0, |
| 176 | + max_time=10, |
| 177 | + scale="rank", |
| 178 | + unique_times=[0, 1, 2], |
| 179 | + h_spacing=0.5, |
| 180 | + height=100, |
| 181 | + y_shift=0, |
| 182 | + ) |
0 commit comments