From d2d8ac27791d385eb96b33da1bd4645c10429344 Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Wed, 30 Jul 2025 16:23:28 +0100 Subject: [PATCH 1/5] Rename SiberneticReplay to make it easier to integrate into sibernetic itself --- siberneticmodel.py => SiberneticReplay.py | 0 app.py | 2 +- load.py | 2 +- test.sh | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename siberneticmodel.py => SiberneticReplay.py (100%) diff --git a/siberneticmodel.py b/SiberneticReplay.py similarity index 100% rename from siberneticmodel.py rename to SiberneticReplay.py diff --git a/app.py b/app.py index 601a704..7d4270a 100644 --- a/app.py +++ b/app.py @@ -1,5 +1,5 @@ from neuromlmodel import add_neuroml_model # noqa: F401 -from siberneticmodel import add_sibernetic_model # noqa: F401 +from SiberneticReplay import add_sibernetic_model # noqa: F401 from virtualworm import add_virtualworm_muscles # noqa: F401 from virtualworm import add_virtualworm_neurons # noqa: F401 diff --git a/load.py b/load.py index 2d3749a..37deeab 100644 --- a/load.py +++ b/load.py @@ -2,7 +2,7 @@ import sys from neuromlmodel import add_neuroml_model # noqa: F401 -from siberneticmodel import add_sibernetic_model # noqa: F401 +from SiberneticReplay import add_sibernetic_model # noqa: F401 from virtualworm import add_virtualworm_muscles # noqa: F401 from virtualworm import add_virtualworm_neurons # noqa: F401 diff --git a/test.sh b/test.sh index 8fa3bed..90aeee3 100755 --- a/test.sh +++ b/test.sh @@ -11,7 +11,7 @@ python neuromlmodel.py -nogui python neuropal.py -nogui # Test the Sibernetic loader without showing the GUI -python siberneticmodel.py Sibernetic/impermeability.txt -b -nogui +python SiberneticReplay.py Sibernetic/impermeability.txt -b -nogui # Test the VirtualWorm loader without showing the GUI python virtualworm.py -nogui From 0c5a739a18c0086be76cf9eae0df9dc917210920 Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Wed, 30 Jul 2025 18:26:14 +0100 Subject: [PATCH 2/5] Update to latest sibernetic version --- SiberneticReplay.py | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/SiberneticReplay.py b/SiberneticReplay.py index a0aa130..e8ce422 100644 --- a/SiberneticReplay.py +++ b/SiberneticReplay.py @@ -1,3 +1,10 @@ +""" +A PyVista based viewer/player for saved Sibernetic simulations + +Loads in the generated position_buffer.txt file + +""" + import pyvista as pv import sys import os @@ -176,12 +183,19 @@ def create_mesh(step): if __name__ == "__main__": plotter = pv.Plotter() - position_file = "Sibernetic/position_buffer.txt" + position_file = "buffers/position_buffer.txt" # can be overwritten by arg + + if not os.path.isfile(position_file): + position_file = ( + "Sibernetic/position_buffer.txt" # example location in Worm3DViewer repo + ) include_boundary = False if "-b" in sys.argv: include_boundary = True + else: + print("Run with -b to display boundary box") if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]): position_file = sys.argv[1] From 3f0e4271655994143294dfb4a7360c3fa73a1b1b Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Wed, 30 Jul 2025 18:37:50 +0100 Subject: [PATCH 3/5] Add initial docker file --- Dockerfile | 27 +++++++++++++++++++++++++++ generate.sh | 18 ++++++++++++++++++ requirements.txt | 5 ++++- runLocal.sh | 1 + 4 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 Dockerfile create mode 100755 generate.sh create mode 100755 runLocal.sh diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5a7323f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,27 @@ +FROM python:3.12-slim + +WORKDIR /app + +RUN apt-get update && apt-get install -y \ + build-essential \ + curl \ + software-properties-common \ + git \ + libxrender1 procps libgl1-mesa-glx xvfb \ + && rm -rf /var/lib/apt/lists/* + + +COPY requirements.txt ./ +RUN pip3 install -r requirements.txt + + +COPY *.py *.stl *.obj ./ +COPY Sibernetic/* ./Sibernetic/ +COPY NeuroML2/* ./NeuroML2/ +COPY NeuroML2/cells/* ./NeuroML2/cells/ + +EXPOSE 8501 + +HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health + +ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"] \ No newline at end of file diff --git a/generate.sh b/generate.sh new file mode 100755 index 0000000..801b989 --- /dev/null +++ b/generate.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -ex + +no_cache_flag="" +if [[ ($# -eq 1) && ($1 == '-r') ]]; then + no_cache_flag="--no-cache" +fi + +# Set the platform flag if we're on ARM +arch=$(uname -m) +if [[ "$arch" == "arm64" || "$arch" == "aarch64" ]]; then + platform_flag="--platform linux/amd64" +else + platform_flag="" +fi + + +docker build $platform_flag -t worm3dviewer $no_cache_flag . diff --git a/requirements.txt b/requirements.txt index 5b89916..f80bb82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,7 @@ -# Worm3DViewer Requirements +altair +pandas +streamlit +vtk pyvista stpyvista diff --git a/runLocal.sh b/runLocal.sh new file mode 100755 index 0000000..5de80f6 --- /dev/null +++ b/runLocal.sh @@ -0,0 +1 @@ +docker run -p 8501:8501 -i -t worm3dviewer /bin/bash From a20b1ea25c35d90dad5869ff3d3c64a552f8bc3d Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Fri, 19 Sep 2025 14:30:28 +0100 Subject: [PATCH 4/5] Latest from sibernetic repo --- SiberneticReplay.py | 269 +++++++++++++++++++++++++++++++------------- 1 file changed, 190 insertions(+), 79 deletions(-) diff --git a/SiberneticReplay.py b/SiberneticReplay.py index e8ce422..334593d 100644 --- a/SiberneticReplay.py +++ b/SiberneticReplay.py @@ -8,40 +8,67 @@ import pyvista as pv import sys import os +import time +import json +import numpy as np +import matplotlib.pyplot as plt -last_mesh = None -all_points = [] -all_point_types = [] +last_meshes = {} -boundary_points = [] -boundary_point_types = [3, 3.1] +replay_speed = 0.05 # seconds between frames +replaying = False -point_size = 5 -boundary_point_size = 3 +all_points = [] +all_point_types = [] -boundary_color = "#eeeeee" plotter = None - -offset_ = 50 - -color_range = {1.1: "blue", 2.2: "turquoise"} +offset_ = 0 +slider = None + +show_boundary = False + + +def get_color_info_for_type(type_): + """ + Get color, info string and point size for a given point type + returns: color, info, size + """ + + if type_ == 1.1: + return "#8BEBFC", "liquid 1", 5 + elif type_ == 1.2: + return "#3ACFF0", "liquid 2", 5 + elif type_ == 2.1: + return "yellow", "elastic 1", 5 + elif type_ == 2.2: + return "#FF0000", "elastic 2", 5 + elif type_ > 2 and type_ < 3: + return "#00cc00", "elastic variable", 5 + elif type_ == 3: + return "grey", "boundary 0", 3 + elif type_ == 3.1: + return "black", "boundary 1", 7 + else: + return "orange", "unknown", 5 def add_sibernetic_model( pl, position_file="Sibernetic/position_buffer.txt", + report_file=None, swap_y_z=False, - offset=50, + offset=0, include_boundary=False, ): - global all_points, all_point_types, last_mesh, plotter, offset_ + global all_points, all_point_types, last_meshes, plotter, offset_, slider, show_boundary offset_ = offset plotter = pl + show_boundary = include_boundary - points = [] + points = {} types = [] line_count = 0 @@ -49,6 +76,42 @@ def add_sibernetic_model( time_count = 0 logStep = None + report_data = None + count_point_types = {} + + if report_file is not None: + sim_dir = os.path.dirname(os.path.abspath(report_file)) + report_data = json.load(open(report_file, "r")) + print(report_data) + position_file = os.path.join(sim_dir, "position_buffer.txt") + + if "worm" in report_data["configuration"]: + muscle_activation_file = os.path.join( + sim_dir, "muscles_activity_buffer.txt" + ) + print("Loading muscle activation file from: %s" % muscle_activation_file) + musc_dat = np.loadtxt(muscle_activation_file, delimiter="\t").T + print(musc_dat) + print(musc_dat.shape) + # plt.imshow(musc_dat, interpolation="none", aspect="auto", cmap="YlOrRd") + + f, ax = plt.subplots(tight_layout=True) + ax.imshow(musc_dat, interpolation="none", aspect="auto", cmap="YlOrRd") + # ax.set_ylim([-1, 1]) + ax.set_xlabel("Time (s)") + _ = ax.set_ylabel("Muscle") + + h_chart = pv.ChartMPL(f, size=(0.35, 0.35), loc=(0.02, 0.06)) + h_chart.title = None + h_chart.border_color = "white" + h_chart.show_title = False + h_chart.background_color = (1.0, 1.0, 1.0, 0.4) + pl.add_chart( + h_chart, + ) + + first_pass_complete = False + for line in open(position_file): ws = line.split() # print(ws) @@ -65,40 +128,34 @@ def add_sibernetic_model( if len(ws) == 4: type_ = float(ws[3]) + if type_ not in points: + points[type_] = [] - if type_ not in boundary_point_types: - if swap_y_z: - points.append([float(ws[1]), 1 * float(ws[0]), float(ws[2])]) - else: - points.append([float(ws[0]), float(ws[1]), float(ws[2])]) - - types.append(type_) + if not first_pass_complete: + if type_ not in count_point_types: + count_point_types[type_] = 0 + count_point_types[type_] += 1 + if swap_y_z: + points[type_].append([float(ws[1]), 1 * float(ws[0]), float(ws[2])]) else: - if include_boundary: - if swap_y_z: - boundary_points.append( - [float(ws[1]), 1 * float(ws[0]), float(ws[2])] - ) - else: - boundary_points.append( - [float(ws[0]), float(ws[1]), float(ws[2])] - ) - - # types.append(type_) + points[type_].append([float(ws[0]), float(ws[1]), float(ws[2])]) + + types.append(type_) if logStep is not None: pcount += 1 if pcount == numOfBoundaryP + numOfElasticP + numOfLiquidP: + first_pass_complete = True print( - "End of one batch of %i added, %i total points at line %i, time: %i" - % (len(points), pcount, line_count, time_count) + "End of one batch of %i total points (%i types), at line %i, time: %i" + % (pcount, len(points), line_count, time_count) ) all_points.append(points) all_point_types.append(types) - points = [] + points = {} types = [] pcount = 0 numOfBoundaryP = 0 @@ -107,10 +164,8 @@ def add_sibernetic_model( line_count += 1 - # all_points_np = np.array(all_points) - print( - "Loaded positions with %i elastic, %i liquid and %i boundary points (%i total), %i lines" + "Loaded positions with %i elastic, %i liquid and %i boundary points (%i total), over %i lines" % ( numOfElasticP, numOfLiquidP, @@ -120,62 +175,106 @@ def add_sibernetic_model( ) ) - if include_boundary: - bound_mesh = pv.PolyData(boundary_points) - bound_mesh.translate((offset_, -50, -100), inplace=True) - - plotter.add_mesh( - bound_mesh, - render_points_as_spheres=True, - color=boundary_color, - point_size=boundary_point_size, - ) - print("Num of time points found: %i" % len(all_points)) + print("Count of point types found: %s" % dict(sorted(count_point_types.items()))) create_mesh(0) - plotter.remove_scalar_bar("types") - max_time = len(all_points) - 1 - pl.add_slider_widget(create_mesh, rng=[0, max_time], value=0, title="Time point") - pl.add_timer_event(max_steps=5, duration=2, callback=create_mesh) + slider = pl.add_slider_widget( + create_mesh, rng=[0, max_time], value=0, title="Time point", style="modern" + ) + + pl.add_checkbox_button_widget(play_animation, value=False) -def create_mesh(step): - import time +def play_animation(play): + global plotter, last_meshes, all_points, all_point_types, replaying, slider + print("Playing animation: %s" % play) + + if not play: + replaying = False + print("Animation stopped.") + return + else: + replaying = True + print("Animation started.") + + if last_meshes is None: + print("No meshes to animate. Please load a model first.") + return + + for i in range(len(all_points)): + if not replaying: + break + curr_time = slider.GetSliderRepresentation().GetValue() + + print( + " --- Animating step %i (curr_time: %s) of %i, %s" + % (i, curr_time, len(all_points), play) + ) + next_time = curr_time + 1 + slider.GetSliderRepresentation().SetValue(next_time) + + create_mesh(next_time) + plotter.update() + plotter.render() + time.sleep(replay_speed) + + +def create_mesh(step): step_count = step value = step_count - global all_points, all_point_types, last_mesh, plotter, offset_ + global all_points, last_meshes, plotter, offset_, replaying, show_boundary index = int(value) + if index >= len(all_points): + print( + "Index %i out of bounds for all_points with length %i" + % (index, len(all_points)) + ) + replaying = False + return - print("Changing to time point: %s (%s) " % (index, value)) - curr_points = all_points[index] - curr_types = all_point_types[index] + print(" -- Creating new mesh at time point: %s (%s) " % (index, value)) + curr_points_dict = all_points[index] - print("Plotting %i points with %i types" % (len(curr_points), len(curr_types))) + print(" Plotting %i point types" % (len(curr_points_dict))) - if last_mesh is None: - last_mesh = pv.PolyData(curr_points) - last_mesh["types"] = curr_types - last_mesh.translate((0, -1000, 0), inplace=True) - print(last_mesh) + for type_, curr_points in curr_points_dict.items(): + color, info, size = get_color_info_for_type(type_) + is_boundary = "boundary" in info + if show_boundary is False and is_boundary: + continue - # last_actor = - plotter.add_mesh( - last_mesh, - render_points_as_spheres=True, - cmap=[c for c in color_range.values()], - point_size=point_size, + print( + " - Plotting %i points of type '%s' (%s), color: %s, size: %i" + % (len(curr_points), type_, info, color, size) ) - else: - last_mesh.points = curr_points - last_mesh.translate((offset_, -50, -100), inplace=True) + + if len(curr_points) == 0: + continue + if type_ not in last_meshes: + last_meshes[type_] = pv.PolyData(curr_points) + last_meshes[type_].translate((0, 0, 0), inplace=True) + + # last_actor = + plotter.add_mesh( + last_meshes[type_], + render_points_as_spheres=True, + point_size=size, + color=color, + ) + else: + if not is_boundary: + last_meshes[type_].points = curr_points + last_meshes[type_].translate((offset_, 0, 0), inplace=True) + else: + print("Boundary points not translated") plotter.render() - time.sleep(0.1) + # time.sleep(0.1) return @@ -184,6 +283,7 @@ def create_mesh(step): plotter = pv.Plotter() position_file = "buffers/position_buffer.txt" # can be overwritten by arg + report_file = None if not os.path.isfile(position_file): position_file = ( @@ -198,14 +298,25 @@ def create_mesh(step): print("Run with -b to display boundary box") if len(sys.argv) > 1 and os.path.isfile(sys.argv[1]): - position_file = sys.argv[1] + if "json" in sys.argv[1]: + position_file = None + report_file = sys.argv[1] + else: + position_file = sys.argv[1] add_sibernetic_model( - plotter, position_file, swap_y_z=True, include_boundary=include_boundary + plotter, + position_file, + report_file, + swap_y_z=True, + include_boundary=include_boundary, ) plotter.set_background("white") plotter.add_axes() - # plotter.set_viewup([0, 0, 10]) + plotter.camera_position = "zx" + plotter.camera.roll = 90 + plotter.camera.elevation = 45 + print(plotter.camera_position) if "-nogui" not in sys.argv: plotter.show() From 0b8d693e4e531eb8c9cc924655b7998ef4b1d35f Mon Sep 17 00:00:00 2001 From: Padraig Gleeson Date: Fri, 19 Sep 2025 14:37:26 +0100 Subject: [PATCH 5/5] Improve offset handling --- SiberneticReplay.py | 25 +++++++++++++++++-------- load.py | 2 +- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/SiberneticReplay.py b/SiberneticReplay.py index 334593d..0c2d72a 100644 --- a/SiberneticReplay.py +++ b/SiberneticReplay.py @@ -24,7 +24,7 @@ plotter = None -offset_ = 0 +offset3d_ = (0, 0, 0) slider = None show_boundary = False @@ -59,12 +59,19 @@ def add_sibernetic_model( position_file="Sibernetic/position_buffer.txt", report_file=None, swap_y_z=False, - offset=0, + offset3d=(0, 0, 0), include_boundary=False, ): - global all_points, all_point_types, last_meshes, plotter, offset_, slider, show_boundary - - offset_ = offset + global \ + all_points, \ + all_point_types, \ + last_meshes, \ + plotter, \ + offset3d_, \ + slider, \ + show_boundary + + offset3d_ = offset3d plotter = pl show_boundary = include_boundary @@ -226,7 +233,7 @@ def play_animation(play): def create_mesh(step): step_count = step value = step_count - global all_points, last_meshes, plotter, offset_, replaying, show_boundary + global all_points, last_meshes, plotter, offset3d_, replaying, show_boundary index = int(value) if index >= len(all_points): @@ -257,7 +264,7 @@ def create_mesh(step): continue if type_ not in last_meshes: last_meshes[type_] = pv.PolyData(curr_points) - last_meshes[type_].translate((0, 0, 0), inplace=True) + last_meshes[type_].translate(offset3d_, inplace=True) # last_actor = plotter.add_mesh( @@ -269,7 +276,9 @@ def create_mesh(step): else: if not is_boundary: last_meshes[type_].points = curr_points - last_meshes[type_].translate((offset_, 0, 0), inplace=True) + last_meshes[type_].translate( + (offset3d_[0], offset3d_[1], offset3d_[2]), inplace=True + ) else: print("Boundary points not translated") diff --git a/load.py b/load.py index 37deeab..3d9a72e 100644 --- a/load.py +++ b/load.py @@ -11,7 +11,7 @@ spacing = 50 - add_sibernetic_model(plotter, swap_y_z=True, offset=spacing) + add_sibernetic_model(plotter, swap_y_z=True, offset3d=(spacing, -50, -100)) add_neuroml_model( plotter, "NeuroML2/c302_D_Full.net.nml", somas_only=False ) # at 0...