diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 72987a493862d328184a1e6b0a7416f883cfec31..5aac0a8d8d63491843bde749576154a3f6746c48 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -85,7 +85,7 @@ deploy centos[cpy37]: script: - ./ci/deploy.py -deploy centos[cpy38]: +.deploy centos[cpy38]: extends: .centos stage: deploy only: @@ -95,7 +95,7 @@ deploy centos[cpy38]: script: - ./ci/deploy.py -deploy windows[cpy37]: +.deploy windows[cpy37]: extends: .centos stage: deploy only: diff --git a/README.md b/README.md index 824733e47983e1ae070dbc5b12f841304739e96d..45a4a5e1bc6d653c2d9160863391bddacc6a393a 100644 --- a/README.md +++ b/README.md @@ -14,33 +14,54 @@ This repository implements an easy to use interface, to create, save, load, and ## Installation -Clone and install the repository: - +Quick variant: ``` -git clone https://git.imp.fu-berlin.de/bioroboticslab/robofish/io.git -pip install ./io +pip install robofish-trackviewer robofish-io --extra-index-url https://git.imp.fu-berlin.de/api/v4/projects/6392/packages/pypi/simple ``` +Better variant: +- Follow instructions at [Artifacts repository](https://git.imp.fu-berlin.de/bioroboticslab/robofish/artifacts) +- ```pip install robofish-io``` + + ## Usage We show a simple example below. More examples can be found in ```examples/``` ```python import robofish.io -import numpy - -f = robofish.io.File(world_size_cm=[100, 100], frequency_hz=25.0) +import numpy as np -# Create a single robot with 40 poses -# (x,y, x_orientation, y_orientation) and specified time points -f.create_entity(category="robot", name="robot", poses=numpy.zeros((40, 4))) +filename = "example.hdf5" -# Create 2 fishes with 100 poses -# (x,y, x_orientation, y_orientation) and 40ms timesteps -f.create_multiple_entities("fish", poses=numpy.zeros((2, 100, 4))) +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." + +# 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], +) + +# 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 # Show and save the file print(f) -f.save("example.hdf5") + +# Saving also validates the file +f.save(filename) +print(f"Saved to {filename}") + ``` diff --git a/examples/example_readme.py b/examples/example_readme.py index a568f8a26514e185f2d7fa32d0239d82d2c0a236..4f861169441d509cba0e8b8565a474ab8a463b40 100644 --- a/examples/example_readme.py +++ b/examples/example_readme.py @@ -4,24 +4,30 @@ import numpy as np filename = "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." -# Create a single robot with 20 timesteps -# (x,y, x_orientation, y_orientation) and specified time points - +# 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.ones((20, 2)) * 10, - orientations=np.zeros((20, 1)), + positions=np.zeros((100, 2)), + orientations=np.ones((100, 2)) * [0, 1], ) -# Create 2 fishes with 20 poses -poses = np.zeros((2, 20, 4)) -poses[:, :, 3] = 1 # all fishes pointing upwards -poses[1, :, :2] = -20 # move first fish to x,y -20,-20 -f.create_multiple_entities("fish", poses=poses) +# 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 # Show and save the file print(f) + +# Saving also validates the file f.save(filename) print(f"Saved to {filename}") diff --git a/src/conversion_scripts/convert_moritz.py b/src/conversion_scripts/convert_moritz.py index 994286e80553ec0ec024cd84b6a2cf6df177dff1..f6a82af6e400ae4f154f5011c0308d001b5aee02 100644 --- a/src/conversion_scripts/convert_moritz.py +++ b/src/conversion_scripts/convert_moritz.py @@ -63,7 +63,6 @@ def format_mm_data_folder(input, output): # The original coordinate system had the origin in the top left and the # y axis pointing "down". We transform it - print(poses.shape) # Move x axis, to be centered poses[:, 0] -= world[0] / 2 # Invert and move y axis (world_y - pose_y) - world_y / 2 diff --git a/src/robofish/evaluate/app.py b/src/robofish/evaluate/app.py index b000a3b2d5d2c433bf050f63723ebdc6dc05115f..cd829d77593b7512701fb34beb617b5856a694c5 100644 --- a/src/robofish/evaluate/app.py +++ b/src/robofish/evaluate/app.py @@ -19,11 +19,20 @@ def evaluate(args=None): Returns: A human readable print of a given hdf5 file. """ + + function_dict = { + "speed": robofish.evaluate.evaluate.evaluate_speed, + "turn": robofish.evaluate.evaluate.evaluate_turn, + "tank_positions": robofish.evaluate.evaluate.evaluate_tankpositions, + "trajectories": robofish.evaluate.evaluate.evaluate_trajectories, + "follow_iid": robofish.evaluate.evaluate.evaluate_follow_iid, + } + parser = argparse.ArgumentParser( description="This tool can be used to evaluate files from different sources. When different sources are passed, they will be plotted in different colors. With the first argument 'analysis_type', the type of analysis can be chosen." ) - parser.add_argument("analysis_type", type=str, choices=["speed"]) + parser.add_argument("analysis_type", type=str, choices=function_dict.keys()) parser.add_argument( "paths", type=str, @@ -38,14 +47,16 @@ def evaluate(args=None): default=None, ) parser.add_argument( - "--save_folder", + "--save_path", type=str, - help="Output folder for saving resulting graphics", + help="Filename for saving resulting graphics", default=None, ) if args is None: args = parser.parse_args() - if args.analysis_type == "speed": - robofish.evaluate.evaluate.evaluate_speed(args.paths) + if args.analysis_type in function_dict: + function_dict[args.analysis_type](args.paths, args.names, args.save_path) + else: + print(f"Evaluation function not found {args.analysis_type}") diff --git a/src/robofish/evaluate/evaluate.py b/src/robofish/evaluate/evaluate.py index da606d6516cbbe62f8861d660219974a1ed15ff0..c635a7345fd0d55e2b438b6df0f0b1e02b7c15e0 100644 --- a/src/robofish/evaluate/evaluate.py +++ b/src/robofish/evaluate/evaluate.py @@ -15,19 +15,20 @@ def evaluate_speed(paths, names=None, save_path=None, ignore_fish=None): for k in range(len(files_per_path)): path_speeds = [] for p, file in files_per_path[k].items(): - poses = file.get_poses_array() + poses = file.get_poses() for i in range(len(poses)): if not i in ignore_fish[k]: e_speeds = np.linalg.norm(np.diff(poses[i, :, :2], axis=0), axis=1) + e_speeds *= file.get_frequency() path_speeds.extend(e_speeds) speeds.append(path_speeds) if names is None: names = paths - plt.hist(speeds, bins=20, label=names, density=True, range=[0, 2]) + plt.hist(speeds, bins=20, label=names, density=True) plt.title("Agent speeds") - plt.xlabel("Speed (cm/timestep)") + plt.xlabel("Speed [cm/s]") plt.ylabel("Frequency") plt.ticklabel_format(useOffset=False) plt.legend() @@ -48,21 +49,28 @@ def evaluate_turn(paths, names=None, save_path=None, ignore_fish=None): for k in range(len(files_per_path)): path_turns = [] for p, file in files_per_path[k].items(): - poses = file.get_poses_array() + poses = file.get_poses() + + # Todo check if all frequencies are the same + frequency = file.get_frequency() + for i in range(len(poses)): if not i in ignore_fish[k]: # convert ori_x, ori_y to radians ori_rad = np.arctan2(poses[i, :, 2], poses[i, :, 3]) e_turns = ori_rad[1:] - ori_rad[:-1] + # e_turns *= file.get_frequency() + e_turns *= 180 / np.pi path_turns.extend(e_turns) turns.append(path_turns) if names is None: names = paths - plt.hist(turns, bins=40, label=names, density=True, range=[-np.pi, np.pi]) + # TODO: Quantil range + plt.hist(turns, bins=40, label=names, density=True, range=[-30, 30]) plt.title("Agent turns") - plt.xlabel("Change in orientation (radians)") + plt.xlabel("Change in orientation [Degree / timestep at %dhz]" % frequency) plt.ylabel("Frequency") plt.ticklabel_format(useOffset=False) plt.legend() @@ -82,12 +90,12 @@ def evaluate_orientation(paths, names=None, save_path=None, ignore_fish=None): ignore_fish = [[] for i in range(len(paths))] for k in range(len(files_per_path)): for p, file in files_per_path[k].items(): - poses = file.get_poses_array() + poses = file.get_poses() to_keep = list( set([i for i in range(len(paths))]).difference(set(ignore_fish[k])) ) poses = poses[to_keep].reshape((len(to_keep) * len(poses[0]), 4)) - world_size = file.attrs["world size"] + world_size = file.attrs["world_size_cm"] world_bounds = [ -world_size[0] / 2, -world_size[1] / 2, @@ -108,6 +116,9 @@ def evaluate_orientation(paths, names=None, save_path=None, ignore_fish=None): if len(orientations) == 1: ax = [ax] + if names is None: + names = paths + for i in range(len(orientations)): orientation = orientations[i] s_1, x_edges, y_edges, bnr = orientation[0] @@ -143,7 +154,7 @@ def evaluate_relativeOrientation(paths, names=None, save_path=None, ignore_fish= for k in range(len(files_per_path)): path_orientations = [] for p, file in files_per_path[k].items(): - poses = file.get_poses_array() + poses = file.get_poses() for i in range(len(poses)): if not i in ignore_fish[k]: for j in range(len(poses)): @@ -187,10 +198,10 @@ def evaluate_distanceToWall(paths, names=None, save_path=None, ignore_fish=None) for k in range(len(files_per_path)): path_distances = [] for p, file in files_per_path[k].items(): - worldBoundsX.append(file.attrs["world size"][0]) - worldBoundsY.append(file.attrs["world size"][1]) - poses = file.get_poses_array() - world_size = file.attrs["world size"] + worldBoundsX.append(file.attrs["world_size_cm"][0]) + worldBoundsY.append(file.attrs["world_size_cm"][1]) + poses = file.get_poses() + world_size = file.attrs["world_size_cm"] world_bounds = [ -world_size[0] / 2, -world_size[1] / 2, @@ -257,8 +268,8 @@ def evaluate_tankpositions(paths, names=None, save_path=None, ignore_fish=None): for k in range(len(files_per_path)): path_x_pos, path_y_pos = [], [] for p, file in files_per_path[k].items(): - poses = file.get_poses_array() - world_bounds.append(file.attrs["world size"]) + poses = file.get_poses() + world_bounds.append(file.attrs["world_size_cm"]) for i in range(len(poses)): if not i in ignore_fish[k]: path_x_pos.extend(poses[i, :, 0]) @@ -269,6 +280,8 @@ def evaluate_tankpositions(paths, names=None, save_path=None, ignore_fish=None): fig, ax = plt.subplots(1, len(x_pos), figsize=(8 * len(x_pos), 8)) if len(x_pos) == 1: ax = [ax] + if names is None: + names = paths for i in range(len(x_pos)): ax[i].set_title("Tankpositions (" + names[i] + ")") @@ -296,8 +309,8 @@ def evaluate_trajectories(paths, names=None, save_path=None, ignore_fish=None): ignore_fish = [[] for i in range(len(paths))] for k in range(len(files_per_path)): for p, file in files_per_path[k].items(): - poses = file.get_poses_array() - world_bounds.append(file.attrs["world size"]) + poses = file.get_poses() + world_bounds.append(file.attrs["world_size_cm"]) to_keep = list( set([i for i in range(len(paths))]).difference(set(ignore_fish[k])) ) @@ -316,6 +329,8 @@ def evaluate_trajectories(paths, names=None, save_path=None, ignore_fish=None): fig, ax = plt.subplots(1, len(pos), figsize=(len(pos) * 8, 8)) if len(pos) == 1: ax = [ax] + if names is None: + names = paths for i in range(len(pos)): sns.set_style("white", {"axes.linewidth": 2, "axes.edgecolor": "black"}) @@ -364,9 +379,9 @@ def evaluate_positionVec(paths, names=None, save_path=None, ignore_fish=None): for k in range(len(files_per_path)): path_posVec = [] for p, file in files_per_path[k].items(): - worldBoundsX.append(file.attrs["world size"][0]) - worldBoundsY.append(file.attrs["world size"][1]) - poses = file.get_poses_array() + worldBoundsX.append(file.attrs["world_size_cm"][0]) + worldBoundsY.append(file.attrs["world_size_cm"][1]) + poses = file.get_poses() # calculate posVec for every fish combination for i in range(len(poses)): if not i in ignore_fish[k]: @@ -419,9 +434,9 @@ def evaluate_follow_iid(paths, names=None, save_path=None, ignore_fish=None): for k in range(len(files_per_path)): path_follow, path_iid = [], [] for p, file in files_per_path[k].items(): - worldBoundsX.append(file.attrs["world size"][0]) - worldBoundsY.append(file.attrs["world size"][1]) - poses = file.get_poses_array() + worldBoundsX.append(file.attrs["world_size_cm"][0]) + worldBoundsY.append(file.attrs["world_size_cm"][1]) + poses = file.get_poses() for i in range(len(poses)): if not i in ignore_fish[k]: for j in range(len(poses)): diff --git a/src/robofish/io/file.py b/src/robofish/io/file.py index 326776393247ae135763c07741940cd159a7f482..deefcc9a771d6e54966224be4be1dc1abbb69c16 100644 --- a/src/robofish/io/file.py +++ b/src/robofish/io/file.py @@ -206,6 +206,10 @@ class File(h5py.File): self["samplings"].attrs["default"] = self.default_sampling return name + def get_frequency(self): + default_sampling = self["samplings"].attrs["default"] + return self["samplings"][default_sampling].attrs["frequency_hz"] + def create_entity( self, category: str,