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 +5 -0
- sqq/__main__.py +8 -0
- sqq/cli.py +62 -0
- sqq/config.py +150 -0
- sqq/core/__init__.py +1 -0
- sqq/core/cage.py +664 -0
- sqq/core/cup.py +168 -0
- sqq/core/f3f4.py +137 -0
- sqq/core/graph.py +173 -0
- sqq/core/ice.py +80 -0
- sqq/core/pbc.py +31 -0
- sqq/core/ring.py +81 -0
- sqq/core/selection.py +54 -0
- sqq/example/gro/test1.gro +11395 -0
- sqq/example/gro/test2.gro +45447 -0
- sqq/io/__init__.py +1 -0
- sqq/io/gro_writer.py +204 -0
- sqq/io/summary.py +1002 -0
- sqq/io/trajectory.py +152 -0
- sqq/models.py +141 -0
- sqq/pipeline.py +309 -0
- sqq-0.1.0.dist-info/METADATA +212 -0
- sqq-0.1.0.dist-info/RECORD +26 -0
- sqq-0.1.0.dist-info/WHEEL +5 -0
- sqq-0.1.0.dist-info/entry_points.txt +2 -0
- sqq-0.1.0.dist-info/top_level.txt +1 -0
sqq/__init__.py
ADDED
sqq/__main__.py
ADDED
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."""
|