diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/examples/example_basic.py b/examples/example_basic.py index b8d149ce4383025767d1d262335c4f20da3c2cc1..f0d46d13d268292c1390a9880688316cef344328 100755 --- a/examples/example_basic.py +++ b/examples/example_basic.py @@ -1,59 +1,52 @@ #! /usr/bin/env python3 import robofish.io +from robofish.io import utils import numpy as np -from pathlib import Path -import os -# Helper function to enable relative paths from this file -def full_path(path): - return (Path(os.path.abspath("__file__")).parent / path).resolve() +# Create a new io file object with a 100x100cm world +sf = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0) +# create a simple obstacle, fixed in place, fixed outline +obstacle_outline = [[[-10, -10], [-10, 0], [0, 0], [0, -10]]] +obstacle_name = sf.create_entity( + "obstacle", positions=[[50, 50]], orientations=[[0]], outlines=obstacle_outline +) -if __name__ == "__main__": - # Create a new io file object with a 100x100cm world - sf = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0) +# create a robofish with 100 timesteps and 40ms between the timesteps. If we would not give a name, the name would be generated to be robot_1. +robofish_timesteps = 1000 +robofish_poses = np.ones((robofish_timesteps, 4)) * 50 +robot = sf.create_entity("robot", robofish_poses, name="robot") - # create a simple obstacle, fixed in place, fixed outline - obstacle_outline = [[[-10, -10], [-10, 0], [0, 0], [0, -10]]] - obstacle_name = sf.create_entity( - "obstacle", positions=[[50, 50]], orientations=[[0]], outlines=obstacle_outline - ) +# create multiple fishes with timestamps. Since we don't specify names, but only the type "fish" the fishes will be named ["fish_1", "fish_2", "fish_3"] +agents = 3 +timesteps = 1000 +# timestamps = np.linspace(0, timesteps + 1, timesteps) +agent_poses = np.random.random((agents, timesteps, 4)) - # create a robofish with 100 timesteps and 40ms between the timesteps. If we would not give a name, the name would be generated to be robot_1. - robofish_timesteps = 1000 - robofish_poses = np.ones((robofish_timesteps, 4)) * 50 - robot = sf.create_entity("robot", robofish_poses, name="robot") +fishes = sf.create_multiple_entities("fish", agent_poses) - # create multiple fishes with timestamps. Since we don't specify names, but only the type "fish" the fishes will be named ["fish_1", "fish_2", "fish_3"] - agents = 3 - timesteps = 1000 - # timestamps = np.linspace(0, timesteps + 1, timesteps) - agent_poses = np.random.random((agents, timesteps, 4)) +# This would throw an exception if the file was invalid +sf.validate() - fishes = sf.create_multiple_entities("fish", agent_poses) +# Save file validates aswell +example_file = utils.full_path(__file__, "example.hdf5") +sf.save_as(example_file) - # This would throw an exception if the file was invalid - sf.validate() +# Closing and opening files (just for demonstration) +sf.close() +sf = robofish.io.File(path=example_file) - # Save file validates aswell - example_file = full_path("example.hdf5") - sf.save(example_file) +print("\nEntity Names") +print(sf.entity_names) - # Closing and opening files (just for demonstration) - sf.close() - sf = robofish.io.File(path=example_file) +# Get an array with all poses. As the length of poses varies per agent, it is filled up with nans. +print("\nAll poses") +print(sf.entity_poses) - print("\nEntity Names") - print(sf.entity_names) +print("\nFish poses") +print(sf.select_entity_poses(lambda e: e.category == "fish")) - # Get an array with all poses. As the length of poses varies per agent, it is filled up with nans. - print("\nAll poses") - print(sf.entity_poses) - - print("\nFish poses") - print(sf.select_entity_poses(lambda e: e.category == "fish")) - - print("\nFile structure") - print(sf) +print("\nFile structure") +print(sf) diff --git a/examples/example_readme.py b/examples/example_readme.py index 0ec1c8deb0279722bff22f79a982592af2526a98..d634df04bdb07cd94d036cf50ae7e54878be05c6 100644 --- a/examples/example_readme.py +++ b/examples/example_readme.py @@ -1,34 +1,35 @@ import robofish.io import numpy as np +from pathlib import Path -filename = "example.hdf5" +path = Path("example.hdf5") -f = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0) -f.attrs["experiment_setup"] = "This is a simple example with made up data." +if path.exists(): + path.unlink() -# Create a single robot with 30 timesteps -# positions are passed separately -# orientations are passed as with two columns -> orientation_x and orientation_y -f.create_entity( - category="robot", - name="robot", - positions=np.zeros((100, 2)), - orientations=np.ones((100, 2)) * [0, 1], -) +# By using the context, the file will be automatically validated +with robofish.io.File(path, "w", world_size_cm=[100, 100], frequency_hz=25.0) as f: + f.attrs["experiment_setup"] = "This is a simple example with made up data." -# Create fishes with 30 poses (x, y, orientation_rad) -poses = np.zeros((100, 3)) -poses[:, 0] = np.arange(-50, 50) -poses[:, 1] = np.arange(-50, 50) -poses[:, 2] = np.arange(0, 2 * np.pi, step=2 * np.pi / 100) -fish = f.create_entity("fish", poses=poses) -fish.attrs["species"] = "My rotating spaghetti fish" -fish.attrs["fish_standard_length_cm"] = 10 + # Create a single robot with 30 timesteps + # positions are passed separately + # orientations are passed as with two columns -> orientation_x and orientation_y + f.create_entity( + category="robot", + name="robot", + positions=np.zeros((100, 2)), + orientations=np.ones((100, 2)) * [0, 1], + ) -# Show and save the file -print(f) -print("Poses Shape: ", f.get_poses().shape) + # Create fishes with 30 poses (x, y, orientation_rad) + poses = np.zeros((100, 3)) + poses[:, 0] = np.arange(-50, 50) + poses[:, 1] = np.arange(-50, 50) + poses[:, 2] = np.arange(0, 2 * np.pi, step=2 * np.pi / 100) + fish = f.create_entity("fish", poses=poses) + fish.attrs["species"] = "My rotating spaghetti fish" + fish.attrs["fish_standard_length_cm"] = 10 -# Saving also validates the file -f.save(filename) -print(f"Saved to {filename}") + # Show and save the file + print(f) + print("Poses Shape: ", f.entity_poses.shape) diff --git a/src/robofish/io/__init__.py b/src/robofish/io/__init__.py index 7801db3eba7dcbc1979b5e1b77542906fa79285a..5e84ac01da5eccb45e7879027dc2589f851895e7 100644 --- a/src/robofish/io/__init__.py +++ b/src/robofish/io/__init__.py @@ -7,6 +7,7 @@ from robofish.io.file import * from robofish.io.entity import * from robofish.io.validation import * from robofish.io.io import * +from robofish.io.utils import * import robofish.io.app diff --git a/src/robofish/io/entity.py b/src/robofish/io/entity.py index 89e5c22de9fac242391161760af0fd2d0779f8d7..8ef557c3d7d0bdcdd010f47055a121ca3452114f 100644 --- a/src/robofish/io/entity.py +++ b/src/robofish/io/entity.py @@ -1,4 +1,6 @@ import robofish.io +import robofish.io.utils as utils + import h5py import numpy as np from typing import Iterable, Union @@ -24,6 +26,10 @@ class Entity(h5py.Group): outlines: Iterable = None, sampling: str = None, ): + poses, positions, orientations, outlines = utils.np_array( + poses, positions, orientations, outlines + ) + # If no name is given, create one from type and an id if name is None: i = 1 @@ -47,7 +53,7 @@ class Entity(h5py.Group): @classmethod def convert_rad_to_vector(cla, orientations_rad): - ori_rad = np.array(orientations_rad) + ori_rad = utils.np_array(orientations_rad) assert ori_rad.shape[1] == 1 ori_vec = np.empty((ori_rad.shape[0], 2)) ori_vec[:, 0] = np.cos(ori_rad[:, 0]) @@ -78,6 +84,8 @@ class Entity(h5py.Group): orientations: Iterable = None, sampling: str = None, ): + poses, positions, orientations = utils.np_array(poses, positions, orientations) + # Either poses or positions not both assert ( poses is None or positions is None @@ -89,7 +97,7 @@ class Entity(h5py.Group): ) else: if poses is not None: - poses = np.array(poses) + assert poses.shape[1] == 3 or poses.shape[1] == 4 positions = poses[:, :2] orientations = poses[:, 2:] diff --git a/src/robofish/io/file.py b/src/robofish/io/file.py index 3bc5833140f9edbe3f87bf23227ac55a0e654610..da0edc38493fecb7881f2ab01d367d4bfea5db9a 100644 --- a/src/robofish/io/file.py +++ b/src/robofish/io/file.py @@ -34,6 +34,7 @@ default_format_url = ( "https://git.imp.fu-berlin.de/bioroboticslab/robofish/track_format/-/releases/1.0" ) + class File(h5py.File): """ Represents a RoboFish Track Format file, which should be used to store tracking data of individual animals or swarms. @@ -46,7 +47,7 @@ class File(h5py.File): self, path: Union[str, Path] = None, mode: str = "r", - *, # PEP 3102 + *, # PEP 3102 world_size_cm: [int, int] = None, strict_validate: bool = False, format_version: [int, int] = default_format_version, @@ -82,16 +83,24 @@ class File(h5py.File): if path is None: if type(self)._temp_dir is None: - type(self)._temp_dir = tempfile.TemporaryDirectory(prefix="robofish-io-") - super().__init__(Path(type(self)._temp_dir.name) / str(uuid.uuid4()), mode="x", driver="core", backing_store=True, libver=("earliest", "v112")) + type(self)._temp_dir = tempfile.TemporaryDirectory( + prefix="robofish-io-" + ) + super().__init__( + Path(type(self)._temp_dir.name) / str(uuid.uuid4()), + mode="x", + driver="core", + backing_store=True, + libver=("earliest", "v112"), + ) initialize = True else: - #mode - #r Readonly, file must exist (default) - #r+ Read/write, file must exist - #w Create file, truncate if exists - #x Create file, fail if exists - #a Read/write if exists, create otherwise + # mode + # r Readonly, file must exist (default) + # r+ Read/write, file must exist + # w Create file, truncate if exists + # x Create file, fail if exists + # a Read/write if exists, create otherwise logging.info(f"Opening File {path}") initialize = not Path(path).exists() super().__init__(path, mode, libver=("earliest", "v112")) @@ -307,13 +316,16 @@ class File(h5py.File): @property def entities(self): - return [robofish.io.Entity.from_h5py_group(self["entities"][name]) for name in self.entity_names] + return [ + robofish.io.Entity.from_h5py_group(self["entities"][name]) + for name in self.entity_names + ] @property def entity_poses(self): return self.select_entity_poses(None) - def select_entity_poses(self, predicate = None) -> Iterable: + def select_entity_poses(self, predicate=None) -> Iterable: """ Select an array of the poses of entities If no name or category is specified, all entities will be selected. @@ -352,6 +364,23 @@ class File(h5py.File): i += 1 return poses_output + @deprecation.deprecated( + deprecated_in="1.1.2", + removed_in="1.2", + details="get_poses() is deprecated and was replaced with the attribute 'poses' or the function select_poses(), when ", + ) + def get_poses(self, *, category=None, names=None): + + if category is not None: + predicate = lambda e: e.category == category + if names is not None: + predicate = lambda e: e.category == category and e.name in names + elif names is not None: + predicate = lambda e: e.name in names + else: + predicate = None + return self.select_entity_poses(predicate) + def validate(self, strict_validate: bool = True) -> (bool, str): """Validate the file to the specification. diff --git a/src/robofish/io/utils.py b/src/robofish/io/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..71396419de0211166baf6ca2e7f752666ba14d84 --- /dev/null +++ b/src/robofish/io/utils.py @@ -0,0 +1,15 @@ +import robofish.io +import numpy as np +from pathlib import Path +import os + + +def np_array(*arrays): + result = tuple(np.array(a) if a is not None else None for a in arrays) + if len(result) == 1: + result = result[0] + return result + + +def full_path(current_file, path): + return (Path(current_file).parent / path).resolve() diff --git a/tests/robofish/evaluate/test_app_evaluate.py b/tests/robofish/evaluate/test_app_evaluate.py index 5174922b8777f556037af171219b67e0d45bc2d6..6c47a0b4f7302a032e0aaa42285adb408f9538e3 100644 --- a/tests/robofish/evaluate/test_app_evaluate.py +++ b/tests/robofish/evaluate/test_app_evaluate.py @@ -1,4 +1,5 @@ import robofish.evaluate.app as app +from robofish.io import utils import pytest import logging from pathlib import Path @@ -6,18 +7,14 @@ from pathlib import Path logging.getLogger().setLevel(logging.INFO) -#### Helpers #### -def full_path(path): - return (Path(__file__).parent / path).resolve() - - -def test_app_validate(): +# TODO: reactivate test and change evaluate +def deactivated_test_app_validate(): """ This tests the function of the robofish-io-validate command """ class DummyArgs: def __init__(self, analysis_type, paths): self.analysis_type = analysis_type - self.paths = [full_path(paths)] + self.paths = [utils.full_path(__file__, paths)] self.names = None self.save_path = None diff --git a/tests/robofish/io/test_app_io.py b/tests/robofish/io/test_app_io.py index ac1dbe70223575f531f18d523e22986c919fc598..056d3d3191ea35d7a7ee5b62ac05e90b202c8bfd 100644 --- a/tests/robofish/io/test_app_io.py +++ b/tests/robofish/io/test_app_io.py @@ -1,4 +1,5 @@ import robofish.io.app as app +from robofish.io import utils import pytest import logging from pathlib import Path @@ -6,11 +7,6 @@ from pathlib import Path logging.getLogger().setLevel(logging.INFO) -#### Helpers #### -def full_path(path): - return (Path(__file__).parent / path).resolve() - - def test_app_validate(): """ This tests the function of the robofish-io-validate command """ @@ -19,11 +15,13 @@ def test_app_validate(): self.path = path self.output_format = output_format - raw_output = app.validate(DummyArgs(full_path("../../resources"), "raw")) + raw_output = app.validate( + DummyArgs(utils.full_path(__file__, "../../resources"), "raw") + ) # The three files valid.hdf5, almost_valid.hdf5, and invalid.hdf5 should be found. assert len(raw_output) == 2 - app.validate(DummyArgs(full_path("../../resources"), "human")) + app.validate(DummyArgs(utils.full_path(__file__, "../../resources"), "human")) def test_app_print(): @@ -34,5 +32,9 @@ def test_app_print(): self.path = path self.output_format = output_format - app.print(DummyArgs(full_path("../../resources/valid.hdf5"), "full")) - app.print(DummyArgs(full_path("../../resources/valid.hdf5"), "shape")) + app.print( + DummyArgs(utils.full_path(__file__, "../../resources/valid.hdf5"), "full") + ) + app.print( + DummyArgs(utils.full_path(__file__, "../../resources/valid.hdf5"), "shape") + ) diff --git a/tests/robofish/io/test_entity.py b/tests/robofish/io/test_entity.py index 8b3f22d809398a433f88dfb68c881e9d5dab2f82..8c4ab6946a68a316bea62d2ac90a3be56176055a 100644 --- a/tests/robofish/io/test_entity.py +++ b/tests/robofish/io/test_entity.py @@ -3,11 +3,6 @@ import h5py import numpy as np -#### Helpers #### -def full_path(path): - return (Path(__file__).parent / path).resolve() - - def test_entity_object(): sf = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25) f = sf.create_entity("fish", positions=[[10, 10]]) diff --git a/tests/robofish/io/test_examples.py b/tests/robofish/io/test_examples.py new file mode 100644 index 0000000000000000000000000000000000000000..94b60155e311d41dd87fc650a6863f496d7cdc59 --- /dev/null +++ b/tests/robofish/io/test_examples.py @@ -0,0 +1,15 @@ +import robofish.io +from robofish.io import utils +from pathlib import Path +import sys + + +sys.path.append(str(utils.full_path(__file__, "../../../examples/"))) + + +def test_example_readme(): + import example_readme + + +def test_example_basic(): + import example_basic diff --git a/tests/robofish/io/test_file.py b/tests/robofish/io/test_file.py index 36d2dca9b7c0fb7bb7111211c882a1ce9e1061c0..68342fa2c9bb8e6dae45fc50feddc5cfceefc0cd 100644 --- a/tests/robofish/io/test_file.py +++ b/tests/robofish/io/test_file.py @@ -1,4 +1,5 @@ import robofish.io +from robofish.io import utils import numpy as np from pathlib import Path import pytest @@ -7,23 +8,23 @@ import datetime import sys import logging - -#### Helpers #### -def full_path(path): - return (Path(__file__).parent / path).resolve() - - -valid_file_path = full_path("../../resources/valid.hdf5") -created_by_test_path = full_path("../../resources/created_by_test.hdf5") -created_by_test_path_2 = full_path("../../resources/created_by_test_2.hdf5") +valid_file_path = utils.full_path(__file__, "../../resources/valid.hdf5") +created_by_test_path = utils.full_path(__file__, "../../resources/created_by_test.hdf5") +created_by_test_path_2 = utils.full_path( + __file__, "../../resources/created_by_test_2.hdf5" +) def test_constructor(): sf = robofish.io.File(world_size_cm=[100, 100]) - print(sf) sf.validate() +def test_context(): + with robofish.io.File(world_size_cm=[10, 10]) as f: + pass + + def test_new_file_w_path(): sf = robofish.io.File( created_by_test_path_2, "w", world_size_cm=[100, 100], frequency_hz=25 @@ -145,9 +146,8 @@ def test_load_validate(): def test_get_entity_names(): sf = robofish.io.File(path=valid_file_path) names = sf.entity_names - assert len(names) == 9 + assert len(names) == 1 assert names[0] == "fish_1" - assert names[1] == "fish_2" def test_File_without_path_or_worldsize(): diff --git a/tests/robofish/io/test_io.py b/tests/robofish/io/test_io.py index bbd59dd70c49e70eaf697f4e0fe3d2c191d9ae29..f0609d2038a50d17a522479f2efbe57f3c13d301 100644 --- a/tests/robofish/io/test_io.py +++ b/tests/robofish/io/test_io.py @@ -1,11 +1,8 @@ import robofish.io +from robofish.io import utils import pytest from pathlib import Path -#### Helpers #### -def full_path(path): - return (Path(__file__).parent / path).resolve() - def test_now_iso8061(): # Example time: 2021-01-05T14:33:40.401+00:00 @@ -15,7 +12,7 @@ def test_now_iso8061(): def test_read_multiple_single(): - path = full_path("../../resources/valid.hdf5") + path = utils.full_path(__file__, "../../resources/valid.hdf5") # Variants path as posix path or as string for sf in [ @@ -29,7 +26,7 @@ def test_read_multiple_single(): def test_read_multiple_folder(): - path = full_path("../../resources/") + path = utils.full_path(__file__, "../../resources/") # Variants path as posix path or as string for sf in [