-
Notifications
You must be signed in to change notification settings - Fork 11
Vision revision and track #100
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
Changes from all commits
01d73c2
ef9f5ae
a6b9529
7ce69c5
87bddd9
0f0ae12
b3d74dc
750c267
487d2b4
97d0ee7
a7ef277
dbe0bc9
acf2dd6
3ffc6bf
d7e750f
db183c3
5af3e12
210d1c6
4f9617e
9267646
8e9dba2
2282d95
5582f33
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 |
|---|---|---|
| @@ -1,16 +1,17 @@ | ||
| version: 2 | ||
|
|
||
| build: | ||
| os: ubuntu-22.04 | ||
| tools: | ||
| python: "3.10" | ||
| os: ubuntu-22.04 | ||
| tools: | ||
| python: "3.10" | ||
|
|
||
| sphinx: | ||
| configuration: docs/source/conf.py | ||
| configuration: docs/source/conf.py | ||
|
|
||
| python: | ||
| install: | ||
| - method: pip | ||
| path: . | ||
| extra_requirements: | ||
| - dev | ||
| install: | ||
| - method: pip | ||
| path: . | ||
| extra_requirements: | ||
| - dev | ||
| - track |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,14 @@ | ||
| track | ||
| ===== | ||
|
|
||
| This module tracks particle trajectories in `.xyz` files that lack explicit | ||
| particle IDs, assigning consistent identities across frames. | ||
| It is especially useful for outputs generated by the :doc:`vision` module. | ||
|
|
||
| Functions | ||
| --------- | ||
|
|
||
| .. toctree:: | ||
| :maxdepth: 1 | ||
|
|
||
| track_xyz <_autosummary/dynsight.track.track_xyz> |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,138 @@ | ||
| from __future__ import annotations | ||
|
|
||
| import logging | ||
| from pathlib import Path | ||
|
|
||
| import pandas as pd | ||
| import trackpy as tp | ||
|
|
||
| from dynsight.trajectory import Trj | ||
|
|
||
| logging.basicConfig( | ||
| level=logging.INFO, | ||
| format="%(asctime)s | %(levelname)s | %(message)s", | ||
| ) | ||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def track_xyz( | ||
|
Collaborator
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. There are some interesting parameters here, which probably need a bit more of an explanation of their ranges and their meaning. I see your doc string, but I think it needs more. For all these:
Collaborator
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. How big can each be? What are appropriate ranges?
Collaborator
Author
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. I took the doc from the original trackpy documentation.
Collaborator
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. So how does the user select what values to use?
Collaborator
Author
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. trial and error, based on the system they have. (concentration of particles, velocity of them, vibrations). |
||
| input_xyz: Path, | ||
| output_xyz: Path, | ||
| search_range: float = 10, | ||
| memory: int = 1, | ||
| adaptive_stop: None | float = 0.95, | ||
| adaptive_step: None | float = 0.5, | ||
| ) -> Trj: | ||
| """Track particles from an `.xyz` file and write a new file with IDs. | ||
|
|
||
| The input `.xyz` is assumed to contain only raw 3D coordinates | ||
| (without atom labels/identity), and each frame begins with a line | ||
| indicating the number of atoms, followed by a comment line, then a list of | ||
| positions. Each frame in the input file must follow this structure:: | ||
|
|
||
| <number of atoms> | ||
| comment line | ||
| <x> <y> <z> | ||
| <x> <y> <z> | ||
| ... | ||
| <x> <y> <z> | ||
|
|
||
| The output file will have the same structure, but each line will start | ||
| with the tracked particle ID. | ||
|
|
||
| Parameters: | ||
| input_xyz: | ||
| Path to the input .xyz file containing positions only. | ||
|
|
||
| output_xyz: | ||
| Path where the output .xyz file with particle IDs will be saved. | ||
|
|
||
| search_range: | ||
| The maximum allowable displacement of objects between frames for | ||
| them to be considered the same particle. | ||
|
|
||
| memory: | ||
| The maximum number of frames during which an object can vanish, | ||
| then reppear nearby, and be considered the same particle. | ||
|
|
||
| adaptive_stop: | ||
|
andrewtarzia marked this conversation as resolved.
|
||
| If not `None`, when encountering a region with too many candidate | ||
| links (subnet), retry by progressively reducing `search_range` | ||
| until the subnet is solvable. If `search_range` becomes less or | ||
| equal than the `adaptive_stop`, give up and raise a | ||
| `SubnetOversizeException`. | ||
|
|
||
| adaptive_step: | ||
| Factor by which the `search_range` is multiplied to reduce it | ||
| during adaptive search. Effective only if `adaptive_stop` is not | ||
| `None`. | ||
|
andrewtarzia marked this conversation as resolved.
|
||
| """ | ||
| if adaptive_stop is None and adaptive_step is not None: | ||
| msg = "adaptive_step is set but adaptive_stop is None." | ||
| raise ValueError(msg) | ||
| if adaptive_stop is not None and adaptive_step is None: | ||
| msg = "adaptive_stop is set but adaptive_step is None." | ||
| raise ValueError(msg) | ||
|
|
||
| input_xyz = Path(input_xyz) | ||
| output_xyz = Path(output_xyz) | ||
|
|
||
| if not input_xyz.exists(): | ||
| msg = f"Input file not found: {input_xyz}" | ||
| raise FileNotFoundError(msg) | ||
|
|
||
| positions = _collect_positions(input_xyz=input_xyz) | ||
|
|
||
| if not {"frame", "x", "y", "z"}.issubset(positions.columns): | ||
| msg = "Error in the .xyz format. Each line must be <x> <y> <z>." | ||
| raise ValueError(msg) | ||
|
|
||
| linked = tp.link_df( | ||
| positions, | ||
| search_range=search_range, | ||
| memory=memory, | ||
| adaptive_step=adaptive_step, | ||
| adaptive_stop=adaptive_stop, | ||
| ) | ||
|
|
||
| with output_xyz.open("w") as f: | ||
| for frame_num in sorted(linked["frame"].unique()): | ||
| frame_data = linked[linked["frame"] == frame_num] | ||
| f.write(f"{len(frame_data)}\n") | ||
| f.write(f"Frame {frame_num}\n") | ||
| for _, row in frame_data.iterrows(): | ||
| pid = int(row["particle"]) | ||
| x, y, z = row["x"], row["y"], row["z"] | ||
| f.write(f"{pid} {x:.6f} {y:.6f} {z:.6f}\n") | ||
|
|
||
| logger.info(f"Linked .xyz file written to: {output_xyz}") | ||
| return Trj.init_from_xyz(traj_file=output_xyz, dt=1) | ||
|
|
||
|
|
||
| def _collect_positions(input_xyz: Path) -> pd.DataFrame: | ||
| """Read the xyz file and return the positions dataset at each frame.""" | ||
| lines = input_xyz.read_text().splitlines() | ||
|
andrewtarzia marked this conversation as resolved.
|
||
|
|
||
| data = [] | ||
| frame = -1 | ||
| row = 0 | ||
| dimensions = 3 | ||
| for _ in range(len(lines)): | ||
|
andrewtarzia marked this conversation as resolved.
|
||
| if row >= len(lines): | ||
| break | ||
| if lines[row].strip().isdigit(): | ||
| num_atoms = int(lines[row]) | ||
| frame += 1 | ||
| row += 2 # skip comment line. | ||
| for a in range(num_atoms): | ||
| if row + a >= len(lines): | ||
| break | ||
| parts = lines[row + a].strip().split() | ||
| if len(parts) >= dimensions: | ||
| x, y, z = map(float, parts[0:3]) | ||
| data.append({"frame": frame, "x": x, "y": y, "z": z}) | ||
| row += num_atoms | ||
| else: | ||
| row += 1 | ||
|
|
||
| return pd.DataFrame(data) | ||
Uh oh!
There was an error while loading. Please reload this page.