reactra 0.1.0.dev1__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.
reactra/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Public Reactra tools for exported simulation results."""
2
+
3
+ __version__ = "0.1.0.dev1"
reactra/artifacts.py ADDED
@@ -0,0 +1,41 @@
1
+ """Exact artifact identity and publication to new local paths."""
2
+
3
+ import hashlib
4
+ import os
5
+ import re
6
+ import tempfile
7
+ from contextlib import contextmanager
8
+ from pathlib import Path
9
+
10
+
11
+ def fingerprint(path: str | Path, *, expected_sha256=None) -> dict:
12
+ if expected_sha256 is not None and not re.fullmatch(r"[0-9a-fA-F]{64}", expected_sha256):
13
+ raise ValueError("Expected SHA-256 must contain exactly 64 hexadecimal characters")
14
+ digest = hashlib.sha256()
15
+ size = 0
16
+ with Path(path).open("rb") as stream:
17
+ while chunk := stream.read(1024 * 1024):
18
+ size += len(chunk)
19
+ digest.update(chunk)
20
+ actual = digest.hexdigest()
21
+ result = {"path": str(Path(path)), "byte_count": size, "sha256": actual}
22
+ if expected_sha256 is not None:
23
+ result["matches"] = actual == expected_sha256.lower()
24
+ return result
25
+
26
+
27
+ @contextmanager
28
+ def new_output(path: str | Path):
29
+ """Publish a completed temporary file without overwriting an existing path."""
30
+ destination = Path(path)
31
+ if destination.exists() or destination.is_symlink():
32
+ raise FileExistsError(f"Output already exists: {destination}")
33
+ descriptor, temporary = tempfile.mkstemp(
34
+ prefix=".reactra-", suffix=destination.suffix, dir=destination.parent
35
+ )
36
+ os.close(descriptor)
37
+ try:
38
+ yield Path(temporary)
39
+ os.link(temporary, destination)
40
+ finally:
41
+ Path(temporary).unlink(missing_ok=True)
reactra/cli.py ADDED
@@ -0,0 +1,108 @@
1
+ """Command-line tools for exported results, independent of the private engine."""
2
+
3
+ import argparse
4
+ import csv
5
+ import json
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from reactra import __version__
10
+ from reactra.artifacts import fingerprint, new_output
11
+ from reactra.colvar import iter_colvar, select_columns, summarize
12
+
13
+
14
+ def export_csv(source, output, columns=None):
15
+ with new_output(output) as temporary:
16
+ with temporary.open("w", newline="", encoding="utf-8") as stream:
17
+ writer = csv.writer(stream)
18
+ selected = None
19
+ for row in iter_colvar(source):
20
+ if selected is None:
21
+ selected = select_columns(row, columns)
22
+ writer.writerow(selected)
23
+ writer.writerow(row[name] for name in selected)
24
+ return {"output": str(output), "columns": list(selected)}
25
+
26
+
27
+ def plot_trace(source, output, x, ys, stride=1):
28
+ if stride < 1:
29
+ raise ValueError("Plot stride must be positive")
30
+ if Path(output).suffix.lower() not in (".png", ".svg", ".pdf"):
31
+ raise ValueError("Plot output must end in .png, .svg, or .pdf")
32
+ try:
33
+ import matplotlib
34
+
35
+ matplotlib.use("Agg")
36
+ import matplotlib.pyplot as plt
37
+ except ImportError as error:
38
+ raise ValueError("Plotting requires: python -m pip install 'reactra[plot]'") from error
39
+ selected = None
40
+ values = []
41
+ rows = 0
42
+ for index, row in enumerate(iter_colvar(source)):
43
+ if selected is None:
44
+ selected = select_columns(row, [x, *ys])
45
+ rows += 1
46
+ if index % stride == 0:
47
+ values.append(tuple(row[name] for name in selected))
48
+ figure, axes = plt.subplots(
49
+ len(ys), 1, sharex=True, squeeze=False, figsize=(8, 2.8 * len(ys)), layout="constrained"
50
+ )
51
+ try:
52
+ for column, axis in enumerate(axes[:, 0], 1):
53
+ axis.plot([row[0] for row in values], [row[column] for row in values], linewidth=1)
54
+ axis.set_ylabel(ys[column - 1])
55
+ axis.grid(alpha=0.2)
56
+ axes[-1, 0].set_xlabel(x + " (recorded units)")
57
+ figure.suptitle("Recorded simulation traces")
58
+ with new_output(output) as temporary:
59
+ figure.savefig(temporary)
60
+ finally:
61
+ plt.close(figure)
62
+ return {"output": str(output), "rows": rows, "plotted_rows": len(values), "stride": stride}
63
+
64
+
65
+ def build_parser():
66
+ parser = argparse.ArgumentParser(description="Reactra public simulation-result tools")
67
+ parser.add_argument("--version", action="version", version=__version__)
68
+ commands = parser.add_subparsers(dest="command", required=True)
69
+ summary = commands.add_parser("summary", help="Summarize a PLUMED/COLVAR numeric table")
70
+ summary.add_argument("source", type=Path)
71
+ summary.add_argument("--columns", nargs="+")
72
+ summary.add_argument("--time-column", default="time")
73
+ export = commands.add_parser("export-csv", help="Export selected COLVAR columns to a new CSV")
74
+ export.add_argument("source", type=Path)
75
+ export.add_argument("--columns", nargs="+")
76
+ export.add_argument("--output", type=Path, required=True)
77
+ plot = commands.add_parser("plot", help="Plot recorded columns; no free-energy reconstruction")
78
+ plot.add_argument("source", type=Path)
79
+ plot.add_argument("--x", default="time")
80
+ plot.add_argument("--y", nargs="+", required=True)
81
+ plot.add_argument("--stride", type=int, default=1)
82
+ plot.add_argument("--output", type=Path, required=True)
83
+ digest = commands.add_parser("fingerprint", help="Hash exact artifact bytes, optionally verify")
84
+ digest.add_argument("source", type=Path)
85
+ digest.add_argument("--sha256")
86
+ return parser
87
+
88
+
89
+ def main(argv=None):
90
+ args = build_parser().parse_args(argv)
91
+ try:
92
+ if args.command == "summary":
93
+ result = summarize(args.source, columns=args.columns, time_column=args.time_column)
94
+ elif args.command == "export-csv":
95
+ result = export_csv(args.source, args.output, args.columns)
96
+ elif args.command == "plot":
97
+ result = plot_trace(args.source, args.output, args.x, args.y, args.stride)
98
+ else:
99
+ result = fingerprint(args.source, expected_sha256=args.sha256)
100
+ print(json.dumps(result, indent=2, allow_nan=False))
101
+ return 0 if result.get("matches", True) else 1
102
+ except (OSError, ValueError) as error:
103
+ print(json.dumps({"error": str(error)}), file=sys.stderr)
104
+ return 2
105
+
106
+
107
+ if __name__ == "__main__":
108
+ raise SystemExit(main())
reactra/colvar.py ADDED
@@ -0,0 +1,124 @@
1
+ """Strict, streaming access to PLUMED text tables; no simulation dependencies."""
2
+
3
+ import math
4
+ from pathlib import Path
5
+
6
+
7
+ def iter_colvar(path: str | Path):
8
+ """Yield numeric rows by field name, refusing missing or changing schemas.
9
+
10
+ Identical repeated FIELDS headers are accepted (for concatenated chunks).
11
+ No sorting, deduplication, unit conversion, or periodic unwrapping occurs.
12
+ """
13
+ fields = None
14
+ count = 0
15
+ with Path(path).open(encoding="utf-8") as stream:
16
+ for line_number, line in enumerate(stream, 1):
17
+ tokens = line.split()
18
+ if not tokens:
19
+ continue
20
+ if tokens[:2] == ["#!", "FIELDS"]:
21
+ declared = tuple(tokens[2:])
22
+ if not declared or len(set(declared)) != len(declared):
23
+ raise ValueError(f"Line {line_number}: FIELDS must be nonempty and unique")
24
+ if fields is not None and fields != declared:
25
+ raise ValueError(f"Line {line_number}: FIELDS changed within the file")
26
+ fields = declared
27
+ continue
28
+ if tokens[0].startswith("#"):
29
+ continue
30
+ if fields is None:
31
+ raise ValueError(f"Line {line_number}: data precedes the FIELDS header")
32
+ if len(tokens) != len(fields):
33
+ raise ValueError(f"Line {line_number}: expected {len(fields)} numeric values")
34
+ try:
35
+ values = tuple(float(token) for token in tokens)
36
+ except ValueError as error:
37
+ raise ValueError(f"Line {line_number}: invalid numeric value") from error
38
+ if not all(math.isfinite(value) for value in values):
39
+ raise ValueError(f"Line {line_number}: nonfinite numeric value")
40
+ count += 1
41
+ yield dict(zip(fields, values, strict=True))
42
+ if not count:
43
+ raise ValueError("COLVAR contains no data rows")
44
+
45
+
46
+ def select_columns(row, columns):
47
+ selected = tuple(columns) if columns is not None else tuple(row)
48
+ if not selected or len(set(selected)) != len(selected):
49
+ raise ValueError("Selected columns must be nonempty and unique")
50
+ missing = set(selected) - row.keys()
51
+ if missing:
52
+ raise ValueError(f"Unknown columns: {', '.join(sorted(missing))}")
53
+ return selected
54
+
55
+
56
+ def summarize(path: str | Path, *, columns=None, time_column="time") -> dict:
57
+ """Compute linear population statistics in one pass and report time anomalies.
58
+
59
+ Circular CVs need circular analysis; these statistics intentionally do not
60
+ infer periodicity, units, reaction basins, convergence, or free energies.
61
+ """
62
+ statistics = {}
63
+ count = 0
64
+ previous_time = None
65
+ time = None
66
+ for row in iter_colvar(path):
67
+ if not count:
68
+ selected = select_columns(row, columns)
69
+ statistics = {
70
+ name: {
71
+ "first": row[name],
72
+ "min": row[name],
73
+ "max": row[name],
74
+ "mean": 0.0,
75
+ "m2": 0.0,
76
+ }
77
+ for name in selected
78
+ }
79
+ if time_column in row:
80
+ time = {
81
+ "column": time_column,
82
+ "first": row[time_column],
83
+ "last": row[time_column],
84
+ "repeated_steps": 0,
85
+ "backward_steps": 0,
86
+ }
87
+ elif time_column != "time":
88
+ raise ValueError(f"Unknown time column: {time_column}")
89
+ count += 1
90
+ for name, stats in statistics.items():
91
+ value = row[name]
92
+ delta = value - stats["mean"]
93
+ stats["mean"] += delta / count
94
+ stats["m2"] += delta * (value - stats["mean"])
95
+ stats["min"] = min(stats["min"], value)
96
+ stats["max"] = max(stats["max"], value)
97
+ stats["last"] = value
98
+ if time is not None:
99
+ current = row[time_column]
100
+ if previous_time is not None:
101
+ time["repeated_steps"] += current == previous_time
102
+ time["backward_steps"] += current < previous_time
103
+ time["last"] = current
104
+ previous_time = current
105
+ for stats in statistics.values():
106
+ if not all(math.isfinite(value) for value in stats.values()):
107
+ raise ValueError("Numeric range exceeds supported summary precision")
108
+ stats["std_population"] = math.sqrt(max(0.0, stats.pop("m2") / count))
109
+ if not all(math.isfinite(value) for value in stats.values()):
110
+ raise ValueError("Numeric range exceeds supported summary precision")
111
+ if time is not None:
112
+ span = time["last"] - time["first"]
113
+ if not math.isfinite(span):
114
+ raise ValueError("Time range exceeds supported summary precision")
115
+ time["last_minus_first"] = span
116
+ time["strictly_increasing"] = not (time["repeated_steps"] or time["backward_steps"])
117
+ return {
118
+ "source": str(Path(path)),
119
+ "rows": count,
120
+ "columns": statistics,
121
+ "time": time,
122
+ "units": "as recorded; not inferred",
123
+ "statistics": "linear; no periodic unwrapping or free-energy estimation",
124
+ }
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.5
2
+ Name: reactra
3
+ Version: 0.1.0.dev1
4
+ Summary: Inspect, summarize, and plot exported molecular-simulation trajectories
5
+ Author: MechanismAtlas
6
+ Classifier: Development Status :: 2 - Pre-Alpha
7
+ Classifier: Intended Audience :: Science/Research
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
10
+ Requires-Python: >=3.12
11
+ Provides-Extra: plot
12
+ Requires-Dist: matplotlib<4,>=3.8; extra == 'plot'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Reactra
16
+
17
+ **Reaction Engine for Atomistic Catalytic Trajectory Reconstruction and Automation**
18
+
19
+ Reactra is the simulation platform being developed for the MechanismAtlas
20
+ enzyme-reaction ecosystem. This **0.1.0.dev1** public preview provides useful,
21
+ standalone tools for **exported simulation results**. The simulation engine is
22
+ currently private and is **not included in this distribution**.
23
+
24
+ The public tools work with ordinary PLUMED/COLVAR numeric tables, including
25
+ exported OPES traces. They require no model weights, accelerator, private
26
+ credentials, molecular structures, or mechanism definitions.
27
+
28
+ ## Install
29
+
30
+ Python 3.12 or later:
31
+
32
+ ```sh
33
+ python -m pip install reactra==0.1.0.dev1
34
+ # Optional plotting support:
35
+ python -m pip install 'reactra[plot]==0.1.0.dev1'
36
+ ```
37
+
38
+ ## Inspect a trajectory
39
+
40
+ Given a text file named `COLVAR` with PLUMED's standard header:
41
+
42
+ ```text
43
+ #! FIELDS time cv opes.bias
44
+ 0.0 0.20 0.0
45
+ 0.1 0.23 -0.4
46
+ 0.2 0.27 -0.8
47
+ ```
48
+
49
+ ```sh
50
+ reactra summary COLVAR
51
+ reactra summary COLVAR --columns cv opes.bias
52
+ reactra export-csv COLVAR --columns time cv --output selected.csv
53
+ reactra plot COLVAR --x time --y cv opes.bias --output traces.png
54
+ reactra fingerprint COLVAR
55
+ # Verify an independently recorded SHA-256 digest:
56
+ reactra fingerprint COLVAR --sha256 YOUR_64_CHARACTER_HEXADECIMAL_DIGEST
57
+ ```
58
+
59
+ Summary reports the number of rows, per-column first/last/minimum/maximum/mean
60
+ and population standard deviation, and repeated or backward time steps. Summary
61
+ and CSV export stream the input instead of loading the trajectory into memory.
62
+ Identical repeated headers are accepted; changing headers, duplicate column names,
63
+ ragged rows, nonnumeric values, and NaN/infinity are refused with line diagnostics.
64
+
65
+ Plots use one panel per requested column and preserve row order. For a large file,
66
+ choose an explicit stride, for example `--stride 100`; plotting retains the
67
+ selected points in memory. Output supports PNG, SVG, and PDF. CSV and plot exports
68
+ refuse existing destinations and publish only after processing succeeds.
69
+
70
+ Units are preserved as recorded, not inferred. Statistics are linear, including
71
+ for angular columns: this release does not perform circular statistics or
72
+ periodic unwrapping. Time differences are not interpreted as completed continuous
73
+ sampling when a trace repeats or moves backward.
74
+
75
+ **A recorded bias or potential-energy trace is not a free-energy barrier.** These
76
+ tools do not infer reaction crossings, reconstruct free energies, estimate rates,
77
+ or qualify convergence. Artifact hashing checks byte identity against a supplied
78
+ digest; it does not establish scientific validity or authenticity.
79
+
80
+ ## Python API
81
+
82
+ ```python
83
+ from reactra.artifacts import fingerprint
84
+ from reactra.colvar import iter_colvar, summarize
85
+
86
+ report = summarize("COLVAR", columns=["cv"])
87
+ for row in iter_colvar("COLVAR"):
88
+ print(row["time"], row["cv"])
89
+ identity = fingerprint("COLVAR")
90
+ ```
91
+
92
+ CLI output is JSON. Exit codes are `0` for successful processing or a matching
93
+ digest, `1` for a digest mismatch, and `2` for invalid input or an I/O error.
94
+ The tools inspect explicitly named files and do not open or modify engine stores.
95
+
96
+ ## Development status
97
+
98
+ This is an early, intentionally limited public component of a larger research
99
+ project. Its API may change before a stable release. It does not provide the
100
+ private engine's run, restart, YAML-authoring, or chemistry interfaces. Install
101
+ this public preview in its own environment if you also use a private Reactra
102
+ deployment; both distributions use the `reactra` package and command name.
103
+
104
+ The release source includes focused software tests. Run them with:
105
+
106
+ ```sh
107
+ python -m pip install -e . pytest
108
+ python -m pytest
109
+ ```
@@ -0,0 +1,8 @@
1
+ reactra/__init__.py,sha256=CRMxutilLajo4gtuEyxcxFosK3_Bv8crYK6E-6oserk,88
2
+ reactra/artifacts.py,sha256=rbnGBWbIYYPgXLAscl47yFNArtgl2NhIjDqdvBfjkvY,1447
3
+ reactra/cli.py,sha256=jmKSQad4_aafH9twXOg0CrzKtQ3l5ZykTxzHWD6lBQ8,4511
4
+ reactra/colvar.py,sha256=amNlPyTLzqOvtvXpjrpeBQJbRkU_7tylLqhePmtDZfI,5239
5
+ reactra-0.1.0.dev1.dist-info/METADATA,sha256=sTvLe-v7wC47NzKOrPXPXvausVuWRFZAhlhhNEqXdxM,4178
6
+ reactra-0.1.0.dev1.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
7
+ reactra-0.1.0.dev1.dist-info/entry_points.txt,sha256=zjHINyb31gn8TwkzQAowE1gYEyMBlzHk-N148pR0cfo,45
8
+ reactra-0.1.0.dev1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ reactra = reactra.cli:main