Repository navigation
Add trajectory frames: --frames, per-frame runs and CSV time series (proteins/trajectories step 5) #48
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add trajectory frames: --frames, per-frame runs and CSV time series (proteins/trajectories step 5) #48
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,7 +7,7 @@ | |
| import numpy as np | ||
| from optparse import OptionParser | ||
|
|
||
| from dbstep import sterics, parse_data, calculator, writer, selection | ||
| from dbstep import sterics, parse_data, calculator, writer, selection, trajectory | ||
| from dbstep.constants import periodic_table, bondi, charry_tkatchenko, metals | ||
|
|
||
| class dbstep: | ||
|
|
@@ -20,8 +20,8 @@ class dbstep: | |
| atom1, atom2 (the reference atom indices as given in the input file), | ||
| cutoff, n_atoms_total, n_atoms_kept (radial crop, see --cutoff), | ||
| residue_label (PDB residue mode, e.g. "A:45 LEU"), | ||
| results (list of dicts, one per radius: file, structure, residue, atom1, atom2, radius, | ||
| mol_vol, percent_vbur, percent_sbur, bmin, bmax, L; also what --csv writes), | ||
| results (list of dicts, one per radius: file, frame, structure, residue, atom1, atom2, | ||
| radius, mol_vol, percent_vbur, percent_sbur, bmin, bmax, L; also what --csv writes), | ||
| atoms, coords, spec_atoms (the structure actually measured, after selection and crop, | ||
| in input orientation; spec_atoms are 1-indexed into it) | ||
|
|
||
|
|
@@ -407,6 +407,7 @@ def _compute(self, mol, file, options, origin, occ_grid, occ_vol, point_tree, gr | |
| # Record the result row (also written by --csv) | ||
| self.results.append({ | ||
| "file": plain_name, | ||
| "frame": structure_idx if structure_idx is not None else "", | ||
| "structure": structure_label, | ||
| "residue": self.residue_label or "", | ||
| "atom1": self.atom1, | ||
|
|
@@ -558,6 +559,7 @@ def set_options(kwargs): | |
| "self_only": ["self_only", False], | ||
| "chain": ["chain", False], | ||
| "csv": ["csv", False], | ||
| "frames": ["frames", False], | ||
| "measure": ["measure", "classic"], | ||
| "pos": ["pos", False], | ||
| "dp": ["dp", 2], | ||
|
|
@@ -613,6 +615,41 @@ def all_residues(file, **kwargs): | |
| return runs | ||
|
|
||
|
|
||
| def run_file(file, options): | ||
| """All runs for one input file: every selected frame (see --frames) times every residue for --residue all. | ||
|
|
||
| Returns: | ||
| list of dbstep objects in frame, then residue, order | ||
| """ | ||
| runs = [] | ||
| previous = options.structure | ||
| try: | ||
| for frame in trajectory.frame_indices(file, options): | ||
| options.structure = frame | ||
| if options.residue and str(options.residue).lower() == "all": | ||
| runs.extend(all_residues(file, options=options)) | ||
| else: | ||
| runs.append(dbstep(file, options=options)) | ||
| finally: | ||
| options.structure = previous | ||
| return runs | ||
|
|
||
|
|
||
| def all_frames(file, frames=None, **kwargs): | ||
| """Run every selected frame of a multi-structure file (multi-frame xyz, multi-record sdf, multi-MODEL pdb). | ||
|
|
||
| `frames` follows --frames ('start:stop:stride', 0-based Python slice rules, or a list of indices; | ||
| default all). Accepts the same keyword arguments as dbstep(), including residue="all". | ||
|
|
||
| Returns: | ||
| list of dbstep objects, one per frame (times residues for residue="all") | ||
| """ | ||
| options = kwargs["options"] if "options" in kwargs else set_options(kwargs) | ||
| if frames is not None: | ||
| options.frames = frames | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Restore If a caller supplies an options object, 🤖 Prompt for AI Agents |
||
| return run_file(file, options) | ||
|
|
||
|
|
||
| def main(): | ||
| files = [] | ||
| # get command line inputs. Use -h to list all possible arguments and default values | ||
|
|
@@ -635,6 +672,7 @@ def main(): | |
| parser.add_option("--chain", dest="chain", action="store", help="[PDB] Keep only this chain (plus the selected residue)", default=False, metavar="chain") | ||
| parser.add_option("--exclude-self", dest="exclude_self", action="store_true", help="[PDB] The selected residue occupies no volume: measure only its environment", default=False) | ||
| parser.add_option("--self-only", dest="self_only", action="store_true", help="[PDB] Keep only the selected residue (same as measuring it extracted to its own file)", default=False) | ||
| parser.add_option("--frames", dest="frames", action="store", help="Frames of a multi-structure file to run, 0-based with Python slice rules: start:stop:stride, e.g. 0:1000:10, ::5, or a single index (default: all)", default=False, metavar="frames") | ||
| parser.add_option("--csv", dest="csv", action="store", help="Write all result rows (one per file/structure/residue/radius) to this CSV file", default=False, metavar="file") | ||
| parser.add_option("--cutoff", dest="cutoff", action="store", help="Ignore atoms farther than this distance (Angstrom) from atom1; 'auto' keeps exactly the atoms that can occupy the buried-volume sphere. Keeps the grid small for large systems (default: off)", default=False, metavar="cutoff") | ||
| parser.add_option("--isoval", dest="isoval", action="store", help="Density isovalue cutoff (default: 0.0016)", type="float", default=0.0016, metavar="isoval") | ||
|
|
@@ -728,6 +766,8 @@ def main(): | |
| print(" PDB residue mode: measuring residue {}{}".format(options.residue, ", excluding " + " and ".join(filters) if filters else "")) | ||
| if options.self_only: | ||
| print(" Only the selected residue is kept (--self-only)") | ||
| if options.frames: | ||
| print(" Frames {} of each multi-structure file will be run".format(options.frames)) | ||
| if options.cutoff or options.residue: | ||
| cutoff = options.cutoff if options.cutoff else "auto" | ||
| print(" Atoms farther than {} from atom1 are ignored (--cutoff)".format("the auto cutoff" if str(cutoff).lower() == "auto" else "{} Angstrom".format(cutoff))) | ||
|
|
@@ -750,18 +790,8 @@ def main(): | |
| vec_df[numeric_cols] = vec_df[numeric_cols].round(2) | ||
| vec_df.to_csv(os.path.splitext(file)[0] + "_2d_output.csv", index=False) | ||
| else: | ||
| # Multi-structure files (multi-xyz, multi-sdf, multi-MODEL pdb): one run per structure | ||
| _, ext = os.path.splitext(file) | ||
| counters = {".xyz": parse_data.get_xyz_structures, ".sdf": parse_data.get_sdf_structures, ".mol": parse_data.get_sdf_structures, | ||
| ".pdb": parse_data.get_pdb_models, ".ent": parse_data.get_pdb_models} | ||
| n_structures = len(counters[ext](file)) if ext in counters else 1 | ||
| for idx in (range(n_structures) if n_structures > 1 else [None]): | ||
| options.structure = idx | ||
| if options.residue and str(options.residue).lower() == "all": | ||
| runs.extend(all_residues(file, options=options)) | ||
| else: | ||
| runs.append(dbstep(file, options=options)) | ||
| options.structure = None | ||
| # Multi-structure files (multi-xyz, multi-sdf, multi-MODEL pdb): one run per selected frame | ||
| runs.extend(run_file(file, options)) | ||
|
|
||
| if dbstep._column_width and not options.quiet: | ||
| print(" " + "-" * (dbstep._column_width - 3)) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| # -*- coding: UTF-8 -*- | ||
| import os | ||
| import sys | ||
|
|
||
| from dbstep import parse_data | ||
|
|
||
| """ | ||
| trajectory | ||
|
|
||
| Frames of multi-structure inputs: multi-frame XYZ, multi-record SDF and multi-MODEL PDB files. | ||
| Each frame is measured by its own dbstep run (with the crop recomputed, since neighbours move), | ||
| and --frames selects which frames to run. | ||
| """ | ||
|
|
||
| # functions returning the structure boundaries of a multi-structure file, by extension | ||
| FRAME_COUNTERS = { | ||
| ".xyz": parse_data.get_xyz_structures, | ||
| ".sdf": parse_data.get_sdf_structures, | ||
| ".mol": parse_data.get_sdf_structures, | ||
| ".pdb": parse_data.get_pdb_models, | ||
| ".ent": parse_data.get_pdb_models, | ||
| } | ||
|
|
||
|
|
||
| def count_frames(file): | ||
| """Number of structures (frames) in a file; 1 for single-structure formats.""" | ||
| if not isinstance(file, str): | ||
| return 1 | ||
| _, ext = os.path.splitext(file) | ||
| if ext not in FRAME_COUNTERS: | ||
| return 1 | ||
| return max(1, len(FRAME_COUNTERS[ext](file))) | ||
|
|
||
|
|
||
| def parse_frames(spec, n_frames): | ||
| """Frame indices selected by a --frames value. | ||
|
|
||
| The value uses Python slice semantics on the 0-based frame index: 'start:stop:stride' with any | ||
| part omitted ('::10' every tenth frame, '100:' from frame 100 on, ':50' the first fifty), a | ||
| single index, negative indices from the end, or a comma-separated list of these. A list or | ||
| tuple of integers is accepted from Python. Unset means every frame. | ||
|
|
||
| Returns: | ||
| list of 0-based frame indices in the order given | ||
| """ | ||
| if spec in (False, None, ""): | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Treat integer frame 0 as a selection. If a Python caller passes 🤖 Prompt for AI Agents |
||
| return list(range(n_frames)) | ||
| tokens = [str(t).strip() for t in spec] if isinstance(spec, (list, tuple)) else [t.strip() for t in str(spec).split(",")] | ||
| indices = [] | ||
| for token in tokens: | ||
| if not token: | ||
| continue | ||
| try: | ||
| if ":" in token: | ||
| parts = token.split(":") | ||
| if len(parts) > 3: | ||
| raise ValueError | ||
| indices.extend(range(n_frames)[slice(*[int(p) if p.strip() else None for p in parts])]) | ||
| else: | ||
| indices.append(int(token)) | ||
| except ValueError: | ||
| sys.exit(f" Can't read frames '{spec}'. Use start:stop:stride (0-based, Python slice rules), e.g. --frames 0:1000:10") | ||
| selected = [] | ||
| for index in indices: | ||
| frame = index + n_frames if index < 0 else index | ||
| if not 0 <= frame < n_frames: | ||
| sys.exit(f" Frame {index} is out of range (file has {n_frames} frame{'s' if n_frames != 1 else ''})") | ||
| selected.append(frame) | ||
| if not selected: | ||
| sys.exit(f" --frames {spec} selects no frames") | ||
| return selected | ||
|
|
||
|
|
||
| def frame_indices(file, options): | ||
| """Frames to run for a file: a list of 0-based indices, or [None] for a single-structure file without --frames.""" | ||
| n_frames = count_frames(file) | ||
| frames = getattr(options, "frames", False) | ||
| if n_frames == 1 and not frames: | ||
| return [None] | ||
| return parse_frames(frames, n_frames) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Give each saved tensor a frame-specific filename.
If a multi-frame run uses
--tensor --save, this loop starts one calculation per frame. The tensor save indbstep/Dbstep.pyLine 159 writes every calculation to the same input-derived_tensor.npypath. Later frames overwrite earlier tensors. Include the selected frame in that output path so the requested frames remain available.🤖 Prompt for AI Agents