From efd081d4b965775423a252fb0790bf3afeb8577c Mon Sep 17 00:00:00 2001 From: mxwang66 <54554363+mxwang66@users.noreply.github.com> Date: Wed, 27 May 2026 15:13:38 -0500 Subject: [PATCH 1/4] Expose `--no-filter` as a hidden CLI option --- src/seqwin/cli.py | 4 ++++ src/seqwin/config.py | 6 ++++-- src/seqwin/core.py | 34 +++++++++++++++++++++++----------- src/seqwin/kmers.py | 12 +----------- 4 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/seqwin/cli.py b/src/seqwin/cli.py index 4f7a19f..59b68d6 100644 --- a/src/seqwin/cli.py +++ b/src/seqwin/cli.py @@ -140,6 +140,9 @@ def main( help='Do not evaluate signature sequences with BLAST.', rich_help_panel='Signature options' ), + no_filter: bool = typer.Option( + False, '--no-filter', show_default=False, hidden=True + ), # blast_neg_only: bool = typer.Option( # False, '--fast-blast', is_flag=True, flag_value=True, show_default=False, # help='Only evaluate (BLAST) against non-target assemblies.' @@ -227,6 +230,7 @@ def main( min_len=min_len, max_len=max_len, run_blast=not no_blast, + no_filter=no_filter, #blast_neg_only=blast_neg_only, seed=seed, n_cpu=n_cpu, diff --git a/src/seqwin/config.py b/src/seqwin/config.py index 1ea5874..dcdf19d 100644 --- a/src/seqwin/config.py +++ b/src/seqwin/config.py @@ -94,9 +94,9 @@ class Config(BaseModel): min_len (int): Minimum length of output signatures. [200] max_len (int | None): Estimated maximum length of output signatures. If None, no explicit limit is applied (capped by `max_nodes_cap`). [None] run_blast (bool): If True, evaluate signature sequences with BLAST. [True] + no_filter (bool): If True, skip filtering k-mers and save the raw k-mer graph. [False] blast_neg_only (bool, deprecated): If True, only include non-target assemblies in the BLAST database. [False] - no_filter (bool): If True, skip filtering k-mers (debug only). [False] penalty_th_cap (float): If `penalty_th` is None, penalty threshold cannot be higher than this value. [0.2] edge_w_th_mul (float): Multiplier for determining the threshold for low-weight edges. [0.3] min_nodes_floor (int): Lowest possible value for `min_nodes` (see `RunState`), regardless of `min_len`. [3] @@ -138,10 +138,10 @@ class Config(BaseModel): min_len: int = 200 max_len: int | None = None run_blast: bool = True + no_filter: bool = False blast_neg_only: bool = False # NOTE: need to fix when this is turned on # Graph filtering options (not included in CLI) - no_filter: bool = False penalty_th_cap: float = 0.2 edge_w_th_mul: float = 0.3 min_nodes_floor: int = 3 @@ -255,6 +255,7 @@ class WorkingDir: config (str): The `Config` instance saved as JSON. ['config.json'] assemblies_dir (str): Directory for downloaded assemblies. ['assemblies'] assemblies_csv (str): The `Assemblies` instance (`assemblies.py`) saved as CSV. ['assemblies.csv'] + graph (str): NumPy arrays representing the k-mer graph (`kmers`, `idx`, `nodes` and `edges`). ['graph.npz'] mash (str): Mash sketch of all assemblies (.msh will be added by Mash). ['sketches'] blast_dir (str): Directory for the BLAST database. ['blastdb'] blast_log (str): Console output of the makeblastdb command, inside `blast_dir`. ['makeblastdb.log'] @@ -266,6 +267,7 @@ class WorkingDir: config: str = 'config.json' assemblies_dir: str = 'assemblies' assemblies_csv: str = 'assemblies.csv' + graph: str = 'graph.npz' mash: str = 'sketches' blast_dir: str = 'blastdb' blast_log: str = 'makeblastdb.log' diff --git a/src/seqwin/core.py b/src/seqwin/core.py index 9a7d8be..cc8867d 100644 --- a/src/seqwin/core.py +++ b/src/seqwin/core.py @@ -6,6 +6,7 @@ Dependencies: ------------- +- numpy - pandas - .assemblies - .kmers @@ -32,6 +33,7 @@ logger = logging.getLogger(__name__) +import numpy as np import pandas as pd from .assemblies import Assemblies, get_assemblies @@ -126,25 +128,35 @@ def run(self) -> None: assemblies = self.assemblies overwrite = config.overwrite + no_filter = config.no_filter working_dir = state.working_dir kmers, jaccard = get_kmers(assemblies, config, state) - if kmers.subgraphs is None: - # if config.no_filter is True (debug only) - markers = None + if no_filter: + graph_path = working_dir / WORKINGDIR.graph + file_to_write(graph_path, overwrite) + np.savez( + graph_path, + allow_pickle=False, + kmers=kmers.kmers, + idx=kmers.idx, + nodes=kmers.nodes, + edges=kmers.edges, + ) + logger.info(f'Filtering is turned off. Raw minimizer graph is saved as {graph_path}') else: markers = get_markers(kmers, assemblies, config, state) - self.kmers = kmers - self.mash = jaccard - self.markers = markers + self.kmers = kmers + self.mash = jaccard + self.markers = markers - # save run instance - results_path = working_dir / WORKINGDIR.results - file_to_write(results_path, overwrite) - results_path.write_bytes(pickle.dumps(self)) - logger.info(f'Run instance (includes all run data) saved as {results_path}') + # save run instance + results_path = working_dir / WORKINGDIR.results + file_to_write(results_path, overwrite) + results_path.write_bytes(pickle.dumps(self)) + logger.info(f'Run instance (includes all run data) saved as {results_path}') def run(config: Config) -> Seqwin: diff --git a/src/seqwin/kmers.py b/src/seqwin/kmers.py index bf1f4aa..1721819 100644 --- a/src/seqwin/kmers.py +++ b/src/seqwin/kmers.py @@ -271,17 +271,7 @@ def get_kmers( kmers = KmerGraph(assemblies, kmerlen, windowsize, n_cpu) if no_filter: - # skip kmers.filter(), debug only - kmers_path = working_dir / 'kmers.npz' - np.savez( - kmers_path, - kmers=kmers.kmers, - idx=kmers.idx, - nodes=kmers.nodes, - edges=kmers.edges, - allow_pickle=False - ) - log_and_raise(SystemExit, f'Filtering is turned off. Raw k-mers are saved to {kmers_path}') + return kmers, None # calculate filter params # 1. calculate penalty threshold From f74ca5f4273a194593e5312bd3be8297e19af918 Mon Sep 17 00:00:00 2001 From: mxwang66 <54554363+mxwang66@users.noreply.github.com> Date: Wed, 27 May 2026 15:33:14 -0500 Subject: [PATCH 2/4] Add the raw graph `graph.npz` as a pytest fixture --- .gitattributes | 3 ++- tests/smoke/fixtures/expected/graph.npz | Bin 0 -> 43050 bytes 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 tests/smoke/fixtures/expected/graph.npz diff --git a/.gitattributes b/.gitattributes index ef815a9..36bfc38 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ cpp/**/btllib/** linguist-vendored -tests/**/*.py linguist-vendored \ No newline at end of file +tests/**/*.py linguist-vendored +*.npz binary \ No newline at end of file diff --git a/tests/smoke/fixtures/expected/graph.npz b/tests/smoke/fixtures/expected/graph.npz new file mode 100644 index 0000000000000000000000000000000000000000..49cfa349491d0a4e3e23f498cf9a9b820604e3a7 GIT binary patch literal 43050 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 b`QLK#G+vD=k)yaFL~{PS=Ex$eT Date: Wed, 27 May 2026 16:14:07 -0500 Subject: [PATCH 3/4] Add smoke test validation for raw graph fixture --- tests/smoke/conftest.py | 5 ++++ tests/smoke/test_outputs.py | 54 +++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/tests/smoke/conftest.py b/tests/smoke/conftest.py index d45d98d..9813ea7 100644 --- a/tests/smoke/conftest.py +++ b/tests/smoke/conftest.py @@ -38,3 +38,8 @@ def non_targets_txt(fixtures_dir: Path) -> Path: @pytest.fixture(scope='session') def expected_fasta(fixtures_dir: Path) -> str: return read_text(fixtures_dir / 'expected' / 'signatures.fasta') + + +@pytest.fixture(scope='session') +def expected_graph(fixtures_dir: Path) -> Path: + return fixtures_dir / 'expected' / 'graph.npz' diff --git a/tests/smoke/test_outputs.py b/tests/smoke/test_outputs.py index af26309..c146b61 100644 --- a/tests/smoke/test_outputs.py +++ b/tests/smoke/test_outputs.py @@ -1,5 +1,6 @@ from pathlib import Path +import numpy as np from typer.testing import CliRunner from seqwin import cli @@ -18,6 +19,45 @@ ) +def _assert_graph_matches_expected(actual_path: Path, expected_path: Path) -> None: + actual = np.load(actual_path, allow_pickle=False) + expected = np.load(expected_path, allow_pickle=False) + + actual_keys = set(actual.files) + expected_keys = set(expected.files) + required_keys = {'kmers', 'idx', 'nodes', 'edges'} + + assert actual_keys == expected_keys, f'graph arrays mismatch: actual={sorted(actual_keys)}, expected={sorted(expected_keys)}' + assert actual_keys == required_keys, f'graph arrays must be exactly {sorted(required_keys)}, got {sorted(actual_keys)}' + + for name in sorted(required_keys): + actual_array = actual[name] + expected_array = expected[name] + assert actual_array.dtype == expected_array.dtype, f'{name} dtype mismatch: actual={actual_array.dtype}, expected={expected_array.dtype}' + assert actual_array.shape == expected_array.shape, f'{name} shape mismatch: actual={actual_array.shape}, expected={expected_array.shape}' + + np.testing.assert_array_equal(actual['idx'], expected['idx'], err_msg='idx array values mismatch') + np.testing.assert_array_equal(actual['kmers'], expected['kmers'], err_msg='kmers array values mismatch') + np.testing.assert_array_equal(actual['edges'], expected['edges'], err_msg='edges array values mismatch') + + nodes_dtype = actual['nodes'].dtype + for field_name in nodes_dtype.names or (): + if field_name == 'penalty': + np.testing.assert_allclose( + actual['nodes'][field_name], + expected['nodes'][field_name], + rtol=0, + atol=1e-12, + err_msg='nodes.penalty values mismatch', + ) + else: + np.testing.assert_array_equal( + actual['nodes'][field_name], + expected['nodes'][field_name], + err_msg=f'nodes.{field_name} values mismatch', + ) + + def _run_cli(*args: str) -> Path: result = runner.invoke(cli.app, args) assert result.exit_code == 0, result.stdout @@ -66,3 +106,17 @@ def test_multithreading_matches_expected(tmp_path: Path, targets_txt: Path, non_ ) assert read_text(out_dir / WORKINGDIR.markers_fasta) == expected_fasta + + +def test_graph_matches_expected(tmp_path: Path, targets_txt: Path, non_targets_txt: Path, expected_graph: Path) -> None: + out_dir = _run_cli( + '--tar-paths', str(targets_txt), + '--neg-paths', str(non_targets_txt), + '--prefix', str(tmp_path), + '--threads', '1', + '--title', 'no-filter', + '--no-filter', + *_shared_config, + ) + + _assert_graph_matches_expected(out_dir / WORKINGDIR.graph, expected_graph) From 9ea3d50e985ef486f33e6202403c9336f5743548 Mon Sep 17 00:00:00 2001 From: mxwang66 <54554363+mxwang66@users.noreply.github.com> Date: Wed, 27 May 2026 16:15:27 -0500 Subject: [PATCH 4/4] Bump version --- src/seqwin/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/seqwin/_version.py b/src/seqwin/_version.py index 222c11c..b703f5c 100644 --- a/src/seqwin/_version.py +++ b/src/seqwin/_version.py @@ -1 +1 @@ -__version__ = '0.4.0' \ No newline at end of file +__version__ = '0.4.1' \ No newline at end of file