Files
SciMesh/tests/test_sdk_search_parallel.py
T
Emil 5b2d5b5f6e
coordinator / test (push) Waiting to run
python / test (push) Waiting to run
release / binaries (amd64, darwin) (push) Waiting to run
release / binaries (amd64, linux) (push) Waiting to run
release / binaries (amd64, windows) (push) Waiting to run
release / binaries (arm64, darwin) (push) Waiting to run
release / binaries (arm64, linux) (push) Waiting to run
release / binaries (arm64, windows) (push) Waiting to run
release / wheel (push) Waiting to run
release / release (push) Blocked by required conditions
release / image (push) Waiting to run
users / test (push) Waiting to run
Add similarity-search-parallel: thread-pool scoring, byte-identical to similarity-search
2026-08-03 15:12:18 +03:00

186 lines
6.3 KiB
Python

"""Tests for the SDK-built similarity-search-parallel workload."""
from __future__ import annotations
import csv
from pathlib import Path
import pytest
from scimesh.sdk import (
ArtifactCollection,
DeterminismProfile,
JobRequest,
LocalArtifactStore,
LocalCoreBatchExecutor,
LocalPlanningContext,
StageKind,
)
from scimesh.workloads.library import default_sdk_registry, default_sdk_runtime
from scimesh.workloads.search.core import run_search_shard, write_search_shards
from scimesh.workloads.search_parallel import (
run_search_shard_parallel,
search_similar_parallel,
)
from scimesh.workloads.similarity_search import (
find_molecule_by_id,
search_similar,
write_search_results,
)
def _write_dataset(path: Path, molecules: list[tuple[str, str]]) -> None:
path.write_text(
"chembl_id\tcanonical_smiles\n"
+ "".join(f"{mid}\t{smiles}\n" for mid, smiles in molecules),
encoding="utf-8",
)
def _tie_dataset(path: Path) -> None:
# Deliberate similarity ties: propanol isomers and duplicated rows, so the
# parallel merge must reproduce the sequential row-order preference.
_write_dataset(
path,
[
("QUERY", "CCO"),
("A1", "CCCO"),
("A2", "C(CC)O"),
("B", "CCN"),
("C1", "CCC"),
("C2", "CCC"),
("D", "CCCC"),
],
)
def test_parallel_matches_sequential_byte_exactly(tmp_path: Path) -> None:
dataset = tmp_path / "molecules.tsv"
_tie_dataset(dataset)
query = find_molecule_by_id(dataset, "QUERY")
reference = search_similar(dataset, query, top_k=5, progress_every=0)
reference_path = tmp_path / "reference.csv"
write_search_results(reference_path, reference.matches)
for threads in (1, 2, 4):
parallel = search_similar_parallel(dataset, query, top_k=5, threads=threads)
parallel_path = tmp_path / f"parallel-{threads}.csv"
write_search_results(parallel_path, parallel.matches)
assert parallel_path.read_bytes() == reference_path.read_bytes(), (
f"threads={threads} diverged from the reference"
)
def test_parallel_shard_matches_sequential_shard(tmp_path: Path) -> None:
dataset = tmp_path / "molecules.tsv"
_tie_dataset(dataset)
shard_dir = tmp_path / "shards"
shard_dir.mkdir()
shards = write_search_shards(dataset, shard_dir, shard_rows=2)
parameters = {"query_smiles": "CCO", "top_k": 3, "threads": 4}
sequential_out = tmp_path / "seq.tsv"
parallel_out = tmp_path / "par.tsv"
sequential_parameters = dict(parameters)
sequential_parameters.pop("threads")
run_search_shard(shards[0], sequential_parameters, sequential_out)
run_search_shard_parallel(shards[0], parameters, parallel_out)
assert parallel_out.read_bytes() == sequential_out.read_bytes()
with parallel_out.open(encoding="utf-8") as handle:
rows = list(csv.DictReader(handle))
assert rows[0]["rank"] == "1"
assert rows[0]["similarity"].startswith("0.5") # CCO vs CCCO
assert len(rows) <= 3
def test_parallel_rejects_bad_parameters(tmp_path: Path) -> None:
dataset = tmp_path / "molecules.tsv"
_tie_dataset(dataset)
output = tmp_path / "out.tsv"
with pytest.raises(ValueError, match="threads must be a non-negative integer"):
run_search_shard_parallel(
dataset, {"query_smiles": "CCO", "threads": -1}, output
)
with pytest.raises(ValueError, match="unsupported"):
run_search_shard_parallel(dataset, {"query_smiles": "CCO", "nope": 1}, output)
with pytest.raises(ValueError, match="query_smiles is invalid"):
run_search_shard_parallel(dataset, {"query_smiles": "СС"}, output)
def _registered_parallel_search(shard_rows: int = 2):
registry = default_sdk_registry(shard_rows=shard_rows)
runtime = default_sdk_runtime()
description = next(
item
for item in registry.descriptions()
if item.workload.name == "similarity-search-parallel"
)
definition, negotiated = registry.require(
description.workload.name,
description.workload.version,
description.package_digest,
runtime=runtime,
)
return registry, runtime, description, definition, negotiated
def test_parallel_manifest_is_registered_and_negotiable() -> None:
_, runtime, description, definition, negotiated = _registered_parallel_search()
manifest = definition.manifest
assert description.enabled is True
assert manifest.workload.name == "similarity-search-parallel"
assert manifest.workload.version == "1.0.0"
assert manifest.determinism is DeterminismProfile.BYTE_EXACT
assert manifest.verifier.verifier.canonical == "exact-artifact@1"
assert set(mode.value for mode in manifest.trust_modes) == {
"trusted",
"untrusted_quorum",
}
assert [stage.kind for stage in manifest.workflow.stages] == [
StageKind.MAP,
StageKind.REDUCE,
]
assert "threads" in manifest.parameters_schema["properties"]
assert negotiated is not None
assert runtime is not None
def test_parallel_executor_matches_reference(tmp_path: Path) -> None:
dataset = tmp_path / "molecules.tsv"
_tie_dataset(dataset)
registry, runtime, description, definition, _ = _registered_parallel_search()
artifact_store = LocalArtifactStore(tmp_path / "artifacts")
input_port = definition.manifest.inputs["input"]
dataset_artifact = artifact_store.import_file(
dataset,
declaration=input_port.schema,
)
request = JobRequest(
workload=definition.manifest.workload,
parameters={"query_id": "QUERY", "top_k": 3, "threads": 2},
inputs={"input": ArtifactCollection.single(dataset_artifact)},
)
result = LocalCoreBatchExecutor(
registry,
runtime,
artifact_store,
tmp_path / "sdk-work",
).execute(request, description.package_digest)
result_artifact = result.outputs["result"].items[0].artifact
reference_path = tmp_path / "reference.csv"
query = find_molecule_by_id(dataset, "QUERY")
reference = search_similar(dataset, query, top_k=3, progress_every=0)
write_search_results(reference_path, reference.matches)
assert (
artifact_store.materialize(result_artifact).read_bytes()
== reference_path.read_bytes()
)
assert result.task_key == "reduce/final"