feat(numbering): publish canonical numbering API

Add a public numbering module and route fragmenting, validation,
and scaffold preparation through the canonical numbering entry.

Rewrite the repository entry docs around the fixed numbering
contract, add MkDocs landing pages, and document the mirror
mapping used for medicinal-chemistry comparisons.

Also refresh the validation analysis reports to explain the
canonical-versus-mirrored numbering relationship.
This commit is contained in:
2026-03-20 15:14:31 +08:00
parent 8071a141ee
commit 3e07402f4e
22 changed files with 529 additions and 444 deletions

View File

@@ -0,0 +1,34 @@
from __future__ import annotations
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
def test_root_readme_documents_canonical_numbering() -> None:
readme = (PROJECT_ROOT / "README.md").read_text(encoding="utf-8")
assert "1 = 内酯羰基碳" in readme
assert "2 = 相邻酯氧" in readme
assert "3..N = 从 2 位出发沿环唯一图遍历顺序继续编号" in readme
assert "6 → 13" in readme
assert "7 → 12" in readme
def test_root_agents_exists_and_documents_numbering_invariants() -> None:
agents_path = PROJECT_ROOT / "AGENTS.md"
assert agents_path.exists()
agents = agents_path.read_text(encoding="utf-8")
assert "canonical numbering" in agents
assert "不是视觉顺时针" in agents
assert "bridge / fused multi-anchor" in agents
def test_mkdocs_ring_numbering_page_documents_mirror_mapping() -> None:
ring_doc = (PROJECT_ROOT / "docs" / "user-guide" / "ring-numbering.md").read_text(encoding="utf-8")
assert "p_mirror = ring_size - p + 3" in ring_doc
assert "6 → 13" in ring_doc
assert "15 → 4" in ring_doc

View File

@@ -0,0 +1,52 @@
from __future__ import annotations
from macro_lactone_toolkit import (
MacrolactoneFragmenter,
mirror_macrolactone_position,
mirror_macrolactone_positions,
number_macrolactone,
)
from macro_lactone_toolkit.splicing.scaffold_prep import prepare_macrolactone_scaffold
from .helpers import build_macrolactone
def test_number_macrolactone_matches_fragmenter_numbering() -> None:
built = build_macrolactone(16, {5: "methyl"})
api_result = number_macrolactone(built.smiles, ring_size=16)
fragmenter_result = MacrolactoneFragmenter(ring_size=16).number_molecule(built.smiles)
assert api_result.position_to_atom == fragmenter_result.position_to_atom
assert api_result.atom_to_position == fragmenter_result.atom_to_position
def test_mirror_macrolactone_position_for_ring16() -> None:
assert mirror_macrolactone_position(6, 16) == 13
assert mirror_macrolactone_position(7, 16) == 12
assert mirror_macrolactone_position(15, 16) == 4
assert mirror_macrolactone_position(16, 16) == 3
def test_mirror_macrolactone_positions_returns_stable_mapping() -> None:
assert mirror_macrolactone_positions([6, 7, 15, 16], 16) == {
6: 13,
7: 12,
15: 4,
16: 3,
}
def test_prepare_scaffold_keeps_requested_position_label() -> None:
built = build_macrolactone(16, {5: "ethyl"})
scaffold, dummy_map = prepare_macrolactone_scaffold(
built.mol,
positions=[5],
ring_size=16,
)
numbering = number_macrolactone(built.mol, ring_size=16)
assert 5 in dummy_map
assert numbering.position_to_atom[5] == built.position_to_atom[5]
assert numbering.position_to_atom[5] == scaffold.GetAtomWithIdx(dummy_map[5]).GetNeighbors()[0].GetIdx()

View File

@@ -285,6 +285,10 @@ def test_analyze_validation_fragment_library_script_generates_reports(tmp_path):
report_zh = (output_dir / "fragment_library_analysis_report_zh.md").read_text(encoding="utf-8")
assert "桥连或双锚点侧链不会进入当前片段库" in report_zh
assert "cyclic single-anchor side chains" in report_zh
assert "6 → 13" in report_zh
assert "7 → 12" in report_zh
assert "15 → 4" in report_zh
assert "16 → 3" in report_zh
def test_active_text_assets_do_not_reference_legacy_api():