diff --git a/README.rst b/README.rst index 17961984..7fde8945 100644 --- a/README.rst +++ b/README.rst @@ -1,22 +1,22 @@ -:maintainers: - `andrewtarzia `_; - `matteobecchi `_; - `simonemartino `_ -:documentation: https://dynsight.readthedocs.io - Overview ======== -``dynsight`` is structured to support a wide range of tasks commonly -encountered in the analysis of many-body dynamical systems. These tasks -include handling trajectory data, computing single-particle descriptors, -performing time-series clustering and conducting various auxiliary analyses. -To achieve this, dynsight is organized into specialized modules, each -addressing a specific aspect of this workflow. - -Previously in `cpctools`_. - -.. _`cpctools`: https://github.com/GMPavanLab/cpctools +``dynsight`` is an open Python platform built to support a wide range of tasks commonly encountered +in the analysis of complex dynamical systems. The software contains and combines modules that allows +resolving and handling trajectory data, computing single-particle descriptors obtaining time-series +data that are easier to analyze, performing time-series data clustering, and extracting relevant information out of them. +``dynsight`` contains also various auxiliary tools useful to data analysis, including, e.g., modules +(`vision `_ and `track `_) +for resolving individual-object trajectories for many-body systems - e.g., experimental ones - for which these are +not readily available, denoising algorithms, and tools for assessing maximum information extraction from data. + +The documentation is available at: `dynsight.readthedocs.io `_ + +If you use ``dynsight``, please to cite the associated scientific paper: + +| S. Martino, M. Becchi, A. Tarzia, D. Rapetti & G. M. Pavan +| "dynsight: an open Python platform for simulation and experimental trajectory data analysis" +| arXiv (2025), DOI: `10.48550/arXiv.2510.23493 `_ Installation ============ @@ -76,17 +76,10 @@ Examples There are examples throughout the documentation and available in the ``examples/`` directory of this repository. -There are also examples available in the ``cpctools`` repository -`here ` - -How To Cite -=========== - -If you use ``dynsight`` please cite +Related works and packages +========================== - https://github.com/GMPavanLab/dynsight - - https://doi.org/10.48550/arXiv.2510.23493 +``dynsight`` uses many different open-source packages. Please cite them when appropriate: * Most modules also use MDAnalysis, https://www.mdanalysis.org/pages/citations/ * If you use SOAP, please cite https://doi.org/10.1103/PhysRevB.87.184115 and DScribe https://singroup.github.io/dscribe/latest/citing.html @@ -98,17 +91,8 @@ If you use ``dynsight`` please cite * If you use ``dynsight.track``, please cite Trackpy https://soft-matter.github.io/trackpy/dev/introduction.html * Entropy calculations are based on ``infomeasure`` https://doi.org/10.1038/s41598-025-14053-5 - Acknowledgements ================ -We developed this code when working in the Pavan group, -https://www.gmpavanlab.polito.it/, whose members often provide very valuable -feedback, which we gratefully acknowledge. - -Much of the original code in ``cpctools`` was written by Daniele Rapetti (Iximiel). - -The work was funded by the European Union and ERC under projects DYNAPOL and the -NextGenerationEU project, CAGEX. - -.. figure:: docs/source/_static/EU_image.png +``dynsight`` is developed and mantained by the G. M. Pavan group at Politecnico di Torino, https://www.gmpavanlab.polito.it/. +Many group members continuously provide, with their daily work, useful feedback that we gratefully acknowledge. diff --git a/docs/source/_static/info_plot.png b/docs/source/_static/info_plot.png index 5a8c2a4c..def462fd 100644 Binary files a/docs/source/_static/info_plot.png and b/docs/source/_static/info_plot.png differ diff --git a/docs/source/index.rst b/docs/source/index.rst index f3ebab48..a96c2d9f 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -16,6 +16,7 @@ analysis data processing HDF5er + logs .. toctree:: :hidden: @@ -46,43 +47,48 @@ Overview ======== -| GitHub: https://www.github.com/GMPavanLab/dynsight - -:mod:`.dynsight` is structured to support a wide range of tasks commonly -encountered in the analysis of many-body dynamical systems. These tasks -include handling trajectory data, computing single-particle descriptors, -performing time-series clustering and conducting various auxiliary analyses. -To achieve this, dynsight is organized into specialized modules, each -addressing a specific aspect of this workflow. - -Previously in `cpctools`_. - -.. _`cpctools`: https://github.com/GMPavanLab/cpctools - +``dynsight`` is an open Python platform built to support a wide range of tasks commonly encountered +in the analysis of complex dynamical systems. The software contains and combines modules that allows +resolving and handling trajectory data, computing single-particle descriptors obtaining time-series +data that are easier to analyze, performing time-series data clustering, and extracting relevant information out of them. +``dynsight`` contains also various auxiliary tools useful to data analysis, including, e.g., modules (:doc:`vision ` and :doc:`track `) +for resolving individual-object trajectories for many-body systems - e.g., experimental ones - for which these are +not readily available, denoising algorithms, and tools for assessing maximum information extraction from data. + +The software is available at: `www.github.com/GMPavanLab/dynsight `_ + +If you use ``dynsight``, please to cite the associated scientific paper: + +| S. Martino, M. Becchi, A. Tarzia, D. Rapetti & G. M. Pavan +| "dynsight: an open Python platform for simulation and experimental trajectory data analysis" +| arXiv (2025), DOI: `10.48550/arXiv.2510.23493 `_ Installation ============ -To get :mod:`.dynsight`, you can install it with pip:: - - $ pip install dynsight +To get ``dynsight``, you can install it with pip:: + $ pip install dynsight Optional Dependancies --------------------- -Old versions :mod:`dynsight` used :mod:`cpctools` for SOAP calculations, if -you are using Python 3.10 and below, you can use :mod:`cpctools` to access -:mod:`SOAPify` and :mod:`hd5er` using :: +Old versions ``dynsight`` used ``cpctools`` for SOAP calculations, if +you are using Python 3.10 and below, you can use ``cpctools`` to access +``SOAPify`` and ``hd5er`` using :: $ pip install cpctools +If you want to use the ``dynsight.vision`` and ``dynsight.track`` modules +you will need to install a series of packages. This can be done with with pip:: + + $ pip install ultralytics PyYAML How to get started ------------------ We suggest you give a read to the ``dynsight.trajectory`` module documentation, -which offers a compact and easy way of using most of the ``dynsight`` tools. +which offers a compact and easy way of using most of the ``dynsight`` tools. Also, the documentation offers some copiable Recipes and Examples for the most common analyses. @@ -103,33 +109,22 @@ Developer Setup $ just dev - #. Run code checks:: $ just check - .. _`just`: https://github.com/casey/just - Examples ======== There are examples throughout the documentation and available in the ``examples/`` directory of this repository. -There are also examples available in the ``cpctools`` repository here: -https://github.com/GMPavanLab/cpctools/tree/main/Examples - - -How To Cite -=========== +Related works and packages +========================== -If you use ``dynsight`` please cite - - https://github.com/GMPavanLab/dynsight - - https://doi.org/10.48550/arXiv.2510.23493 +``dynsight`` uses many different open-source packages. Please cite them when appropriate: * Most modules also use MDAnalysis, https://www.mdanalysis.org/pages/citations/ * If you use SOAP, please cite https://doi.org/10.1103/PhysRevB.87.184115 and DScribe https://singroup.github.io/dscribe/latest/citing.html @@ -139,19 +134,15 @@ If you use ``dynsight`` please cite * If you use tICA, please cite ``deeptime`` https://deeptime-ml.github.io/latest/index.html * If you use ``dynsight.vision``, please cite Ultralytics YOLO https://docs.ultralytics.com/it/models/yolo11/#usage-examples * If you use ``dynsight.track``, please cite Trackpy https://soft-matter.github.io/trackpy/dev/introduction.html +* Entropy calculations are based on ``infomeasure`` https://doi.org/10.1038/s41598-025-14053-5 Acknowledgements ================ -We developed this code when working in the Pavan group, -https://www.gmpavanlab.polito.it/, whose members often provide very valuable -feedback, which we gratefully acknowledge. - -Much of the original code in ``cpctools`` was written by Daniele Rapetti (Iximiel). +``dynsight`` is developed and mantained by the G. M. Pavan group at Politecnico di Torino, https://www.gmpavanlab.polito.it/. +Many group members continuously provide, with their daily work, useful feedback that we gratefully acknowledge. -The work was funded by the European Union and ERC under projects DYNAPOL and the -NextGenerationEU project, CAGEX. Indices and tables ------------------ diff --git a/docs/source/logs.rst b/docs/source/logs.rst new file mode 100644 index 00000000..57f030a6 --- /dev/null +++ b/docs/source/logs.rst @@ -0,0 +1,27 @@ +Logs +==== + +dynsight logging system. + +.. warning:: + + A default instance of :class:`Logger` is **automatically created** when importing the ``dynsight`` package. + This instance is available as ``dynsight.logs.logger``. + + You can configure it, for example to disable the automatic recording of datasets, using: + + .. code-block:: python + + import dynsight + dynsight.logs.logger.configure(auto_recording=False) + + You can also access all its attributes and methods described in the Logs page below. + +----- +Usage +----- + +.. toctree:: + :maxdepth: 1 + + Logs <_autosummary/dynsight.logs.rst> diff --git a/src/dynsight/_internal/logs.py b/src/dynsight/_internal/logs.py index 6bf9afe6..b568bddb 100644 --- a/src/dynsight/_internal/logs.py +++ b/src/dynsight/_internal/logs.py @@ -1,29 +1,273 @@ """logging package.""" +from __future__ import annotations + +import sys +from dataclasses import dataclass from datetime import datetime, timezone from pathlib import Path +from tempfile import TemporaryDirectory +from typing import TYPE_CHECKING, Any +from zipfile import ZIP_DEFLATED, ZipFile + +import numpy as np + +if TYPE_CHECKING: + from dynsight.trajectory import Insight + +import logging + +COLORS = { + "DEBUG": "\033[36m", + "INF": "\033[32m", + "WRN": "\033[33m", + "ERR": "\033[31m", + "CRT": "\033[41m", +} +RESET = "\033[0m" + +LEVEL_ALIASES = { + "DEBUG": "DBG", + "INFO": "INF", + "WARNING": "WRN", + "ERROR": "ERR", + "CRITICAL": "CRT", +} + + +class ColorFormatter(logging.Formatter): + def format(self, record: logging.LogRecord) -> str: + alias = LEVEL_ALIASES.get(record.levelname, record.levelname) + color = COLORS.get(alias, "") + record.levelname = f"{color}{alias:^3}{RESET}" + record.msg = f"{color}{record.msg}{RESET}" + return super().format(record) + + +handler = logging.StreamHandler(sys.stdout) +formatter = ColorFormatter( + fmt="%(asctime)s | %(levelname)s | %(message)s", datefmt="%Y-%m-%d %H:%M" +) +handler.setFormatter(formatter) + +console = logging.getLogger(__name__) +console.setLevel(logging.DEBUG) +console.addHandler(handler) +console.propagate = False + + +@dataclass +class RecordedDataset: + meta: Any + path: Path class Logger: - """Creates and save human-readible log.""" + """Create and save human-readible log.""" - def __init__(self) -> None: + def __init__( + self, + *, + auto_recording: bool = True, + ) -> None: self._log: list[str] = [] + self._recorded_data: list[RecordedDataset] = [] + self._temp_dir: TemporaryDirectory[str] | None = None + self._temp_path: Path | None = None + self.auto_recording = auto_recording + + def configure( + self, + *, + auto_recording: bool = True, + ) -> None: + """Adjusts the runtime configuration of the logger. + + Parameters: + auto_recording: + Enables or disables automatic dataset recording. + When set to `True`, every processed dataset will be + automatically saved into a temporary archive. + When `False`, datasets must be explicitly registered + via `register_data()`. + """ + self.auto_recording = auto_recording + state = "enabled" if auto_recording else "disabled" + console.info(f"Automatic dataset recording {state}.") def log(self, msg: str) -> None: + """Records an informational message to the log. + + Parameters: + msg: + The message to record. + """ timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S") - entry = f"[{timestamp}] {msg}" - self._log.append(entry) + history_entry = f"[{timestamp}] {msg}" + console.info(msg) + self._log.append(history_entry) - def save(self, filename: Path) -> None: + def save_history(self, filename: Path) -> None: + """Saves the current log history to a text file. + + Parameters: + filename: + The file path where the log history will be written. + """ with filename.open("w", encoding="utf-8") as f: f.write("\n".join(self._log)) - def clear(self) -> None: + def clear_history(self) -> None: + """Clears the current log history and registered datasets.""" self._log = [] + self._cleanup_temp_dir() + self._recorded_data = [] def get(self) -> str: + """Retrieves the current log history as a string.""" return "\n".join(self._log) + def record_data(self, insight: Insight) -> None: + """Record and save a dataset associated with an `Insight` instance. + + Parameters: + insight: + the `Insight` to be registered. + """ + for existing in self._recorded_data: + if existing.meta == insight.meta: + console.warning("Insight already registered, skipping.") + return + + insight_bytes = insight.dataset.nbytes + + temp_path = self._ensure_temp_dir() + + base_filename = self._build_base_filename(insight) + filename = self._make_unique_filename(temp_path, base_filename) + + np.save(filename, insight.dataset) + + dataset_entry = RecordedDataset(meta=insight.meta, path=filename) + self._recorded_data.append(dataset_entry) + + total_bytes = sum( + entry.path.stat().st_size + for entry in self._recorded_data + if entry.path.exists() + ) + + bytes_in_kb = 1024 + bytes_in_mb = bytes_in_kb**2 + bytes_in_gb = bytes_in_kb**3 + + def _format_size(bytes_val: int) -> str: + gb = bytes_val / bytes_in_gb + mb = bytes_val / bytes_in_mb + kb = bytes_val / bytes_in_kb + thr = 0.5 + if gb >= thr: + return f"{gb:.2f} GB" + if mb >= thr: + return f"{mb:.2f} MB" + return f"{kb:.2f} KB" + + added_size = _format_size(insight_bytes) + total_size = _format_size(total_bytes) + + console.warning(f"Registering dataset with size: {added_size}.") + console.warning(f"Disk used by dynsight datasets: {total_size}.") + self.log(f"Dataset prepared: {filename.name}.") + + def extract_datasets( + self, + output_dir: Path | str = Path("analysis_archive"), + ) -> None: + """Exports all recorded datasets into a ZIP archive. + + Parameters: + output_dir: + The directory where the ZIP archive will be saved. + """ + if self._recorded_data == []: + console.error("No datasets to extract.") + return + output_path = Path(output_dir) + zip_parent = output_path.parent + zip_parent.mkdir(parents=True, exist_ok=True) + + saved_paths: list[Path] = [] + dataset_files = [ + entry.path for entry in self._recorded_data if entry.path.exists() + ] + + if dataset_files: + zip_filename = self._create_zip_archive( + dataset_files, output_path, zip_parent + ) + saved_paths.append(zip_filename) + self.log(f"Output directory zipped to {zip_filename}.") + + self._cleanup_temp_dir() + self._recorded_data = [] + + def _create_zip_archive( + self, + dataset_files: list[Path], + output_path: Path, + zip_parent: Path, + ) -> Path: + zip_base = output_path.name + zip_filename = zip_parent / f"{zip_base}.zip" + + counter = 1 + while zip_filename.exists(): + zip_filename = zip_parent / f"{zip_base}_{counter}.zip" + counter += 1 + + with ZipFile(zip_filename, "w", compression=ZIP_DEFLATED) as archive: + for file_path in sorted(dataset_files): + archive.write(file_path, arcname=file_path.name) + + return zip_filename + + def _ensure_temp_dir(self) -> Path: + if self._temp_dir is None or self._temp_path is None: + self._temp_dir = TemporaryDirectory(prefix="analysis_archive_") + self._temp_path = Path(self._temp_dir.name) + return self._temp_path + + def _cleanup_temp_dir(self) -> None: + if self._temp_dir is not None: + self._temp_dir.cleanup() + self._temp_dir = None + self._temp_path = None + + def _build_base_filename(self, insight: Insight) -> str: + base_filename = "dataset" + if isinstance(insight.meta, dict) and insight.meta: + sanitized_values = [] + for value in insight.meta.values(): + value_str = str(value) + sanitized = "".join( + ch if ch.isalnum() or ch in {"-", "_"} else "_" + for ch in value_str + ).strip("_") + if sanitized: + sanitized_values.append(sanitized) + if sanitized_values: + base_filename = "_".join(sanitized_values) + return base_filename + + def _make_unique_filename( + self, temp_path: Path, base_filename: str + ) -> Path: + filename = temp_path / f"{base_filename}.npy" + counter = 1 + while filename.exists(): + filename = temp_path / f"{base_filename}_{counter}.npy" + counter += 1 + return filename + logger = Logger() diff --git a/src/dynsight/_internal/trajectory/insight.py b/src/dynsight/_internal/trajectory/insight.py index 6cfd3c86..bcc793af 100644 --- a/src/dynsight/_internal/trajectory/insight.py +++ b/src/dynsight/_internal/trajectory/insight.py @@ -32,6 +32,10 @@ class Insight: dataset: NDArray[np.float64] meta: dict[str, Any] = field(default_factory=dict) + def __post_init__(self) -> None: + if logger.auto_recording: + logger.record_data(self) + def dump_to_json(self, file_path: Path) -> None: """Save the Insight to a JSON file and .npy file.""" # Save dataset as .npy @@ -142,6 +146,10 @@ def get_angular_velocity(self, delay: int = 1) -> Insight: theta = dynsight.soap.timesoap(self.dataset, delay=delay) attr_dict = self.meta.copy() attr_dict.update({"delay": delay}) + if self.meta.get("name") == "soap": + attr_dict.update({"name": "timesoap"}) + else: + attr_dict.update({"name": "angular_velocity"}) logger.log(f"Computed angular velocity with args {attr_dict}.") return Insight(dataset=theta, meta=attr_dict) diff --git a/src/dynsight/_internal/trajectory/trajectory.py b/src/dynsight/_internal/trajectory/trajectory.py index 99a1be23..b7ff5ea7 100644 --- a/src/dynsight/_internal/trajectory/trajectory.py +++ b/src/dynsight/_internal/trajectory/trajectory.py @@ -142,7 +142,7 @@ def get_coord_number( * neighcounts: a list[list[AtomGroup]], it can be used to speed up subsequent descriptors' computations. * An Insight containing the number of neighbors. It has the - following meta: r_cut, selection. + following meta: name, r_cut, delay, selection. """ if neigcounts is None: neigcounts = dynsight.lens.list_neighbours_along_trajectory( @@ -157,7 +157,11 @@ def get_coord_number( delay=1, ) - attr_dict = {"r_cut": r_cut, "selection": selection} + attr_dict = { + "name": "coord_number", + "r_cut": r_cut, + "selection": selection, + } logger.log(f"Computed coord_number using args {attr_dict}.") return neigcounts, Insight( dataset=nn.astype(np.float64), @@ -179,7 +183,7 @@ def get_lens( * neighcounts: a list[list[AtomGroup]], it can be used to speed up subsequent descriptors' computations. * An Insight containing LENS. It has the following meta: - r_cut, selection. + name, r_cut, delay, selection. """ if neigcounts is None: neigcounts = dynsight.lens.list_neighbours_along_trajectory( @@ -194,7 +198,12 @@ def get_lens( delay=delay, ) - attr_dict = {"r_cut": r_cut, "delay": delay, "selection": selection} + attr_dict = { + "name": "lens", + "r_cut": r_cut, + "delay": delay, + "selection": selection, + } logger.log(f"Computed LENS using args {attr_dict}.") return neigcounts, Insight( @@ -214,8 +223,8 @@ def get_soap( ) -> Insight: """Compute SOAP on the trajectory. - The returned Insight contains the following meta: r_cut, n_max, l_max, - respect_pbc, centers, selection. + The returned Insight contains the following meta: name, r_cut, n_max, + l_max, respect_pbc, selection, centers. """ soap = dynsight.soap.saponify_trajectory( self.universe, @@ -229,6 +238,7 @@ def get_soap( trajslice=self.trajslice, ) attr_dict = { + "name": "soap", "r_cut": r_cut, "n_max": n_max, "l_max": l_max, @@ -239,6 +249,61 @@ def get_soap( logger.log(f"Computed SOAP with args {attr_dict}.") return Insight(dataset=soap, meta=attr_dict) + def get_timesoap( + self, + r_cut: float | None = None, + n_max: int | None = None, + l_max: int | None = None, + soap_insight: Insight | None = None, + selection: str = "all", + centers: str = "all", + respect_pbc: bool = True, + n_jobs: int = 1, + delay: int = 1, + ) -> tuple[Insight, Insight]: + """Compute SOAP and then timeSOAP on the trajectory. + + The returned Insights (soap and timesoap) contain the following meta: + name, r_cut, n_max, l_max, respect_pbc, selection, centers. + Regarding the timeSOAP Insight, the delay used is also included. + """ + if soap_insight is not None: + if getattr(soap_insight, "meta", {}).get("name") != "soap": + msg = ( + f"soap_insight.meta['name'] must be 'soap', found: " + f"{soap_insight.meta.get('name', None)}" + ) + raise ValueError(msg) + msg = ( + "Loaded existing soap_insight: parameters r_cut, n_max, l_max," + " selection, centers, and respect_pbc will be ignored." + ) + logger.log(msg) + soap = soap_insight + else: + if r_cut is None or n_max is None or l_max is None: + msg = ( + "r_cut, n_max e l_max cannot be None" + " if the soap_insight is not provided." + ) + raise ValueError(msg) + + soap = self.get_soap( + r_cut=r_cut, + n_max=n_max, + l_max=l_max, + selection=selection, + centers=centers, + respect_pbc=respect_pbc, + n_jobs=n_jobs, + ) + logger.log(f"Computed SOAP with args {soap.meta}.") + + timesoap = soap.get_angular_velocity(delay=delay) + + logger.log(f"Computed timeSOAP with args {timesoap.meta}.") + return soap, timesoap + def get_orientational_op( self, r_cut: float, @@ -254,7 +319,7 @@ def get_orientational_op( * neighcounts: a list[list[AtomGroup]], it can be used to speed up subsequent descriptors' computations. * An Insight containing the orientational order parameter. - It has the following meta: r_cut, order, selection. + It has the following meta: name, r_cut, order, selection. """ if neigcounts is None: neigcounts = dynsight.lens.list_neighbours_along_trajectory( @@ -270,7 +335,12 @@ def get_orientational_op( order=order, ) - attr_dict = {"r_cut": r_cut, "order": order, "selection": selection} + attr_dict = { + "name": "orientational_op", + "r_cut": r_cut, + "order": order, + "selection": selection, + } logger.log( f"Computed orientational order parameter using args {attr_dict}." @@ -295,7 +365,7 @@ def get_velocity_alignment( * neighcounts: a list[list[AtomGroup]], it can be used to speed up subsequent descriptors' computations. * An Insight containing the average velocities alignment. - It has the following meta: r_cut, selection. + It has the following meta: name, r_cut, selection. """ if neigcounts is None: neigcounts = dynsight.lens.list_neighbours_along_trajectory( @@ -310,7 +380,11 @@ def get_velocity_alignment( neigh_list_per_frame=neigcounts, ) - attr_dict = {"r_cut": r_cut, "selection": selection} + attr_dict = { + "name": "velocity_alignement", + "r_cut": r_cut, + "selection": selection, + } logger.log( f"Computed average velocity alignment using args {attr_dict}." @@ -353,6 +427,7 @@ def get_rdf( step=trajslice.step, ) attr_dict = { + "name": "rdf", "distances_range": distances_range, "s1": s1, "s2": s2, diff --git a/src/dynsight/_internal/vision/label_tool/script.js b/src/dynsight/_internal/vision/label_tool/script.js index 79b6f9de..8174e41d 100644 --- a/src/dynsight/_internal/vision/label_tool/script.js +++ b/src/dynsight/_internal/vision/label_tool/script.js @@ -327,6 +327,7 @@ exportAllBtn.onclick = async () => { } const numTrain = Math.floor(images.length * (trainPercent / 100)); const zip = new JSZip(); + const datasetName = "yolo_dataset"; const imgTrain = zip.folder("images/train"); const imgVal = zip.folder("images/val"); const lblTrain = zip.folder("labels/train"); @@ -372,18 +373,19 @@ exportAllBtn.onclick = async () => { } const names = Object.keys(labelMap); - const yaml = `path: . - train: images/train - val: images/val - nc: ${names.length} - names: [${names.map((n) => `'${n}'`).join(", ")}] - `; + const yaml = [ + `path: ${datasetName}`, + "train: images/train", + "val: images/val", + `nc: ${names.length}`, + `names: [${names.map((n) => `'${n}'`).join(", ")}]`, + ].join("\n"); zip.file("dataset.yaml", yaml); const content = await zip.generateAsync({ type: "blob" }); const a = document.createElement("a"); a.href = URL.createObjectURL(content); - a.download = "yolo_dataset.zip"; + a.download = `${datasetName}.zip`; a.click(); URL.revokeObjectURL(a.href); }; @@ -400,20 +402,27 @@ synthBtn.onclick = async () => { ); const width = parseInt(prompt("Image width?", "640"), 10); const height = parseInt(prompt("Image height?", "640"), 10); - + const requestedPerImage = parseInt( + prompt("Number of objects per image?", "10"), + 10, + ); if ( !numImages || Number.isNaN(numImages) || !width || Number.isNaN(width) || !height || - Number.isNaN(height) + Number.isNaN(height) || + !requestedPerImage || + Number.isNaN(requestedPerImage) || + requestedPerImage < 1 ) { alert("Invalid parameters."); return; } const crops = []; + const datasetName = "synt_dataset"; const labelMap = {}; let nextId = 0; for (const file of images) { @@ -460,8 +469,7 @@ synthBtn.onclick = async () => { ctx.fillRect(0, 0, width, height); const placed = []; - const numObj = Math.min(5, crops.length); - for (let i = 0; i < numObj; i++) { + for (let i = 0; i < requestedPerImage; i++) { const crop = crops[Math.floor(Math.random() * crops.length)]; const img = await loadImage(crop.file); const c = document.createElement("canvas"); @@ -479,9 +487,7 @@ synthBtn.onclick = async () => { crop.height, ); - const scale = - (0.15 + 0.15 * Math.random()) * - (Math.min(width, height) / Math.max(crop.width, crop.height)); + const scale = 1 const w = crop.width * scale; const h = crop.height * scale; @@ -539,18 +545,19 @@ synthBtn.onclick = async () => { } const names = Object.keys(labelMap); - const yaml = `path: . - train: images/train - val: images/val - nc: ${names.length} - names: [${names.map((n) => `'${n}'`).join(", ")}] - `; + const yaml = [ + `path: ${datasetName}`, + "train: images/train", + "val: images/val", + `nc: ${names.length}`, + `names: [${names.map((n) => `'${n}'`).join(", ")}]`, + ].join("\n"); zip.file("dataset.yaml", yaml); const content = await zip.generateAsync({ type: "blob" }); const a = document.createElement("a"); a.href = URL.createObjectURL(content); - a.download = "synt_dataset.zip"; + a.download = `${datasetName}.zip`; a.click(); URL.revokeObjectURL(a.href); }; diff --git a/src/dynsight/logs.py b/src/dynsight/logs.py index 00128318..1b5f082f 100644 --- a/src/dynsight/logs.py +++ b/src/dynsight/logs.py @@ -1,7 +1,8 @@ """logging package.""" -from dynsight._internal.logs import logger +from dynsight._internal.logs import Logger, logger __all__ = [ + "Logger", "logger", ] diff --git a/tests/logger/test_logger.py b/tests/logger/test_logger.py new file mode 100644 index 00000000..6e96ac92 --- /dev/null +++ b/tests/logger/test_logger.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +import zipfile +from pathlib import Path + +from dynsight.logs import logger +from dynsight.trajectory import Trj + + +def test_zip_arch() -> None: + """Ensure the zip archive is created and contains the expected files.""" + logger.clear_history() + + original_dir = Path(__file__).absolute().parent + lens_trajectory_file = original_dir / "../systems/2_particles.xyz" + tsoap_topology_file = original_dir / "../systems/balls_7_nvt.gro" + tsoap_trajectory_file = original_dir / "../systems/balls_7_nvt.xtc" + + lens_traj = Trj.init_from_xyz(lens_trajectory_file, dt=1) + tsoap_traj = Trj.init_from_xtc(tsoap_trajectory_file, tsoap_topology_file) + + lens_traj.get_lens(r_cut=4) + tsoap_traj.get_timesoap(r_cut=4, n_max=4, l_max=4) + + logger.extract_datasets(original_dir / "archive") + + zip_files = list(original_dir.glob("*.zip")) + assert zip_files, f"No zip archive found in {original_dir}" + + zip_path = zip_files[0] + + try: + assert zip_path.exists(), f"Archive not found: {zip_path}" + + with zipfile.ZipFile(zip_path, "r") as zf: + names = [ + Path(name).stem + for name in zf.namelist() + if not name.endswith("/") + ] + n_insight_computed = 3 + assert len(names) == n_insight_computed, ( + f"Expected {n_insight_computed}" + f" files in archive, found {len(names)}: {names}" + ) + expected = { + "lens_4_1_all", + "soap_4_4_4_True_all_all", + "timesoap_4_4_4_True_all_all_1", + } + assert set(names) == expected, f"Unexpected file names: {names}" + + finally: + if zip_path.exists(): + zip_path.unlink(missing_ok=True) + logger.clear_history() + + +def test_disabling_auto_record() -> None: + """Ensure no zip archive is created if auto_recording is disabled.""" + logger.clear_history() + logger.configure(auto_recording=False) + + original_dir = Path(__file__).absolute().parent + lens_trajectory_file = original_dir / "../systems/2_particles.xyz" + tsoap_topology_file = original_dir / "../systems/balls_7_nvt.gro" + tsoap_trajectory_file = original_dir / "../systems/balls_7_nvt.xtc" + + lens_traj = Trj.init_from_xyz(lens_trajectory_file, dt=1) + tsoap_traj = Trj.init_from_xtc(tsoap_trajectory_file, tsoap_topology_file) + + lens_traj.get_lens(r_cut=4) + tsoap_traj.get_timesoap(r_cut=4, n_max=4, l_max=4) + + logger.extract_datasets(original_dir / "archive") + + zip_files = list(original_dir.glob("*.zip")) + assert not zip_files, f"Unexpected zip archive(s) found: {zip_files}" + + logger.clear_history() diff --git a/tests/tsoap/test_tsoap.py b/tests/tsoap/test_tsoap.py index f8658ed8..9576c07a 100644 --- a/tests/tsoap/test_tsoap.py +++ b/tests/tsoap/test_tsoap.py @@ -12,10 +12,10 @@ def test_tsoap(case_data: TimeSOAPCaseData) -> None: - oritinal_dir = Path(__file__).resolve().parent - topology_file = oritinal_dir / "../systems/balls_7_nvt.gro" - trajectory_file = oritinal_dir / "../systems/balls_7_nvt.xtc" - expected_tsoap = oritinal_dir / "test_tsoap" / case_data.expected_tsoap + original_dir = Path(__file__).resolve().parent + topology_file = original_dir / "../systems/balls_7_nvt.gro" + trajectory_file = original_dir / "../systems/balls_7_nvt.xtc" + expected_tsoap = original_dir / "test_tsoap" / case_data.expected_tsoap universe = MDAnalysis.Universe(topology_file, trajectory_file) example_trj = Trj(universe) @@ -26,7 +26,9 @@ def test_tsoap(case_data: TimeSOAPCaseData) -> None: l_max=4, ) - test_tsoap = test_soap.get_angular_velocity(delay=case_data.delay) + _, test_tsoap = example_trj.get_timesoap( + soap_insight=test_soap, delay=case_data.delay + ) if not expected_tsoap.exists(): np.save(expected_tsoap, test_tsoap.dataset)