sqq 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
sqq/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ """SQQ: Shell Quant Qualifier."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
sqq/__main__.py ADDED
@@ -0,0 +1,8 @@
1
+ """Run SQQ with `python -m sqq`."""
2
+
3
+ from .cli import main
4
+
5
+
6
+ if __name__ == "__main__":
7
+ # Delegate to the same entry point used by the installed console script.
8
+ raise SystemExit(main())
sqq/cli.py ADDED
@@ -0,0 +1,62 @@
1
+ from __future__ import annotations
2
+
3
+ """Command-line interface for SQQ."""
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from .config import write_default_config
9
+ from .pipeline import analyze
10
+
11
+
12
+ HELP_BANNER = """
13
+ +----------------------------+
14
+ | Shell Quant Qualifier |
15
+ +----------------------------+
16
+
17
+ SQQ for MD water-shell topology analysis.
18
+ """.strip()
19
+
20
+
21
+ def build_parser() -> argparse.ArgumentParser:
22
+ """Create the two-command CLI: init and analyze."""
23
+ parser = argparse.ArgumentParser(
24
+ prog="sqq",
25
+ description=HELP_BANNER,
26
+ formatter_class=argparse.RawDescriptionHelpFormatter,
27
+ )
28
+ subparsers = parser.add_subparsers(dest="command", required=True)
29
+
30
+ init_parser = subparsers.add_parser("init", help="Write a default config.yaml file.")
31
+ init_parser.add_argument("-o", "--output", default="config.yaml", help="Output config path.")
32
+
33
+ analyze_parser = subparsers.add_parser("analyze", help="Analyze MD frames.")
34
+ analyze_parser.add_argument("-i", "--input", required=True, help="Input file or directory.")
35
+ analyze_parser.add_argument("--pattern", help="Input pattern when --input is a directory.")
36
+ analyze_parser.add_argument("--top", "--topology", dest="topology", help="Topology file for xtc/trr.")
37
+ analyze_parser.add_argument("-c", "--config", help="YAML config file.")
38
+ analyze_parser.add_argument("-o", "--output", default="result_sqq", help="Output directory.")
39
+ analyze_parser.add_argument("--recursive", action="store_true", help="Read input directory recursively.")
40
+ analyze_parser.add_argument("--pairs", help="Pair file for bond_mode=pairs; each line contains two water ids.")
41
+ analyze_parser.add_argument("--pair-id", choices=["resid", "oxygen_index", "atomid"], help="How ids in --pairs are interpreted.")
42
+ analyze_parser.add_argument("--n-jobs", default=None, help="Frame-level worker count for independent GRO/XYZ files; default auto runs serially.")
43
+ analyze_parser.add_argument("--strict", action="store_true", help="Stop on the first failed frame.")
44
+ analyze_parser.add_argument("--no-gro", action="store_true", help="Disable GRO structure output.")
45
+ analyze_parser.add_argument("--no-xlsx", action="store_true", help="Disable summary.xlsx output.")
46
+ return parser
47
+
48
+
49
+ def main(argv: list[str] | None = None) -> int:
50
+ """Dispatch the selected SQQ subcommand."""
51
+ args = build_parser().parse_args(argv)
52
+ if args.command == "init":
53
+ out = Path(args.output)
54
+ write_default_config(out)
55
+ print(f"Wrote default SQQ config: {out}")
56
+ return 0
57
+ if args.command == "analyze":
58
+ analyze(args)
59
+ return 0
60
+ raise AssertionError(f"Unhandled command: {args.command}")
61
+
62
+
sqq/config.py ADDED
@@ -0,0 +1,150 @@
1
+ from __future__ import annotations
2
+
3
+ """Configuration defaults and YAML/JSON loading."""
4
+
5
+ from copy import deepcopy
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ try:
11
+ import yaml
12
+ except ImportError: # pragma: no cover - exercised in minimal source-tree runs.
13
+ yaml = None
14
+
15
+
16
+ # Defaults are intentionally explicit so a run can be reproduced from
17
+ # run_config.yaml without relying on hidden command-line assumptions.
18
+ DEFAULT_CONFIG: dict[str, Any] = {
19
+ "input": {
20
+ "pattern": "*.gro",
21
+ "recursive": False,
22
+ "first_file_time_ps": 0.0,
23
+ "frame_time_step_ps": 100.0,
24
+ "xtc_stride": 1,
25
+ },
26
+ "water": {
27
+ "resnames": ["SOL", "TIP", "WAT", "HOH"],
28
+ "oxygen_names": ["OW", "O", "OH2"],
29
+ "hydrogen_names": ["HW1", "HW2", "H1", "H2", "HW", "HT1", "HT2"],
30
+ },
31
+ "guest": {
32
+ "resnames": ["CH4", "CO2", "MET", "ETH"],
33
+ "center_atoms": {"CH4": ["C"], "CO2": ["C"]},
34
+ "center_mode": "center_atom",
35
+ },
36
+ "graph": {
37
+ "bond_mode": "auto",
38
+ "oo_cutoff_nm": 0.35,
39
+ "hbond_distance_nm": 0.35,
40
+ "hbond_angle_deg": 30.0,
41
+ "pair_file": None,
42
+ "pair_id": "resid",
43
+ },
44
+ "pbc": {
45
+ "box_mode": "orthorhombic",
46
+ },
47
+ "ring": {
48
+ "sizes": [5, 6],
49
+ "primitive": True,
50
+ "chordless": True,
51
+ },
52
+ "cup": {
53
+ "mode": "general",
54
+ "enabled": True,
55
+ "base_sizes": "auto",
56
+ "side_sizes": "auto",
57
+ "max_combinations_per_base": 50000,
58
+ },
59
+ "cage": {
60
+ "ring_sizes": [5, 6],
61
+ "target_types": ["512", "51262", "51263", "51264"],
62
+ "output_other": False,
63
+ "other_max_faces": 20,
64
+ "enabled": True,
65
+ "search_mode": "grow",
66
+ "seed_mode": "ring",
67
+ "max_states_per_seed": 2000,
68
+ "max_total_states": 250000,
69
+ "occupancy_mode": "polyhedron",
70
+ "occupancy_radius_nm": 0.5,
71
+ },
72
+ "order": {
73
+ "f3f4_enabled": True,
74
+ "focus_waters": [],
75
+ },
76
+ "ice": {
77
+ "enabled": True,
78
+ "method": "chill",
79
+ "min_six_rings": 2,
80
+ "require_four_coord_neighbors": True,
81
+ },
82
+ "output": {
83
+ "write_gro": True,
84
+ "write_tsv": False,
85
+ "write_vmd": False,
86
+ "write_xlsx_summary": True,
87
+ "write_empty_files": False,
88
+ "gro_atom_mode": "full_water",
89
+ "center_resname": "CNT",
90
+ },
91
+ "parallel": {
92
+ "n_jobs": "auto",
93
+ },
94
+ "debug": {
95
+ "use_networkx_checks": False,
96
+ },
97
+ }
98
+
99
+
100
+ def load_config(path: Path | None) -> dict[str, Any]:
101
+ """Load a config file and merge it over built-in defaults."""
102
+ config = deepcopy(DEFAULT_CONFIG)
103
+ if path is None:
104
+ return config
105
+ with path.open("r", encoding="utf-8-sig") as handle:
106
+ text = handle.read()
107
+ if yaml is not None:
108
+ user_config = yaml.safe_load(text) or {}
109
+ else:
110
+ # Source-tree smoke tests can run without PyYAML; installed SQQ uses YAML.
111
+ try:
112
+ user_config = json.loads(text) if text.strip() else {}
113
+ except json.JSONDecodeError as exc:
114
+ raise RuntimeError("Reading YAML config files requires PyYAML. Install with `pip install -e .`.") from exc
115
+ if not isinstance(user_config, dict):
116
+ raise ValueError(f"Config file must contain a YAML mapping: {path}")
117
+ return merge_config(config, user_config)
118
+
119
+
120
+ def merge_config(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
121
+ """Recursively merge user configuration into defaults."""
122
+ for key, value in override.items():
123
+ if isinstance(value, dict) and isinstance(base.get(key), dict):
124
+ merge_config(base[key], value)
125
+ else:
126
+ base[key] = value
127
+ return base
128
+
129
+
130
+ def write_default_config(path: Path) -> None:
131
+ """Write the default configuration template."""
132
+ path.parent.mkdir(parents=True, exist_ok=True)
133
+ with path.open("w", encoding="utf-8", newline="\n") as handle:
134
+ dump_config(DEFAULT_CONFIG, handle)
135
+
136
+
137
+ def dump_config(config: dict[str, Any], handle) -> None:
138
+ """Write YAML when available, otherwise a JSON-compatible fallback."""
139
+ if yaml is not None:
140
+ yaml.safe_dump(config, handle, allow_unicode=True, sort_keys=False)
141
+ else:
142
+ json.dump(config, handle, ensure_ascii=False, indent=2)
143
+ handle.write("\n")
144
+
145
+
146
+
147
+
148
+
149
+
150
+
sqq/core/__init__.py ADDED
@@ -0,0 +1 @@
1
+ """Core SQQ topology algorithms."""