From d8dce2d3949443dec8ad589a91bf89f3240bf78a Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Wed, 1 Nov 2023 11:58:27 +0000 Subject: [PATCH 01/12] Added to changes to conform to vedo 2023.5.0+dev26a --- brainrender/_colors.py | 2 +- brainrender/actor.py | 12 ++++++------ brainrender/actors/cylinder.py | 2 +- brainrender/actors/points.py | 2 +- brainrender/actors/ruler.py | 2 +- examples/add_mesh_from_file.py | 2 +- examples/cell_density.py | 2 +- examples/neurons.py | 2 +- examples/screenshot.py | 2 +- examples/user_volumetric_data.py | 2 +- examples/volumetric_data.py | 4 ++-- 11 files changed, 17 insertions(+), 17 deletions(-) diff --git a/brainrender/_colors.py b/brainrender/_colors.py index 61f09b6e..e69b6020 100644 --- a/brainrender/_colors.py +++ b/brainrender/_colors.py @@ -3,7 +3,7 @@ import matplotlib.cm as cm_mpl import numpy as np from vedo.colors import colors as vcolors -from vedo.colors import getColor +from vedo.colors import get_color as getColor def map_color(value, name="jet", vmin=None, vmax=None): diff --git a/brainrender/actor.py b/brainrender/actor.py index b3f9cf81..ebbc9feb 100644 --- a/brainrender/actor.py +++ b/brainrender/actor.py @@ -24,7 +24,7 @@ def make_actor_label( zoffset=0, ): """ - Adds a 2D text ancored to a point on the actor's mesh + Adds a 2D text anchored to a point on the actor's mesh to label what the actor is :param kwargs: keyword arguments can be passed to determine @@ -45,7 +45,7 @@ def make_actor_label( color = [0.2, 0.2, 0.2] # Get mesh's highest point - points = actor.mesh.points().copy() + points = actor.mesh.vertices.copy() point = points[np.argmin(points[:, 1]), :] point += np.array(offset) + default_offset point[2] = -point[2] @@ -64,12 +64,12 @@ def make_actor_label( # Mark a point on Mesh that corresponds to the label location if radius is not None: - pt = actor.closestPoint(point) + pt = actor.closest_point(point) pt[2] = -pt[2] sphere = Sphere(pt, r=radius, c=color, res=8) sphere.ancor = pt new_actors.append(sphere) - sphere.computeNormals() + sphere.compute_normals() return new_actors @@ -159,7 +159,7 @@ def center(self): """ Returns the coordinates of the mesh's center """ - return self.mesh.points().mean(axis=0) + return self.mesh.center_of_mass() @classmethod def make_actor(cls, mesh, name, br_class): @@ -218,7 +218,7 @@ def __rich_console__(self, *args): f"[{orange}]center of mass:[/{orange}][{amber}] {self.mesh.center_of_mass().astype(np.int32)}" ) rep.add( - f"[{orange}]number of vertices:[/{orange}][{amber}] {len(self.mesh.points())}" + f"[{orange}]number of vertices:[/{orange}][{amber}] {self.mesh.npoints}" ) rep.add( f"[{orange}]dimensions:[/{orange}][{amber}] {np.array(self.mesh.bounds()).astype(np.int32)}" diff --git a/brainrender/actors/cylinder.py b/brainrender/actors/cylinder.py index 715b2689..0c1d2223 100644 --- a/brainrender/actors/cylinder.py +++ b/brainrender/actors/cylinder.py @@ -20,7 +20,7 @@ def __init__(self, pos, root, color="powderblue", alpha=1, radius=350): # Get pos if isinstance(pos, Mesh): - pos = pos.points().mean(axis=0) + pos = pos.center_of_mass() elif isinstance(pos, Actor): pos = pos.center logger.debug(f"Creating Cylinder actor at: {pos}") diff --git a/brainrender/actors/points.py b/brainrender/actors/points.py index 5568c095..22cbd089 100644 --- a/brainrender/actors/points.py +++ b/brainrender/actors/points.py @@ -140,7 +140,7 @@ def __init__( volume = ( vPoints(data) .density(dims=dims, radius=radius, **kwargs) - .c("Dark2") + .cmap("Dark2") .alpha([0, 0.9]) .mode(1) ) # returns a vedo Volume diff --git a/brainrender/actors/ruler.py b/brainrender/actors/ruler.py index f4a98f9d..735cec5d 100644 --- a/brainrender/actors/ruler.py +++ b/brainrender/actors/ruler.py @@ -37,7 +37,7 @@ def ruler(p1, p2, unit_scale=1, units=None, s=50): dist = mag(p2 - p1) * unit_scale label = precision(dist, 3) + " " + units lbl = Text3D(label, pos=midpoint, s=s + 100, justify="center") - lbl.SetOrientation([0, 0, 180]) + # lbl.SetOrientation([0, 0, 180]) actors.append(lbl) # Add spheres add end diff --git a/examples/add_mesh_from_file.py b/examples/add_mesh_from_file.py index 375e6a72..d6a0bc4a 100644 --- a/examples/add_mesh_from_file.py +++ b/examples/add_mesh_from_file.py @@ -14,7 +14,7 @@ scene.add_brain_region("SCm", alpha=0.2) # Add from file -scene.add("examples/data/CC_134_1_ch1inj.obj", color="tomato") +scene.add("data/CC_134_1_ch1inj.obj", color="tomato") # Render! scene.render() diff --git a/examples/cell_density.py b/examples/cell_density.py index fcfa2bfb..2a97fa7c 100644 --- a/examples/cell_density.py +++ b/examples/cell_density.py @@ -27,7 +27,7 @@ def get_n_random_points_in_region(region, N): Z = np.random.randint(region_bounds[4], region_bounds[5], size=10000) pts = [[x, y, z] for x, y, z in zip(X, Y, Z)] - ipts = region.mesh.inside_points(pts).points() + ipts = region.mesh.inside_points(pts).vertices return np.vstack(random.choices(ipts, k=N)) diff --git a/examples/neurons.py b/examples/neurons.py index 46d930bb..f1c17a9d 100644 --- a/examples/neurons.py +++ b/examples/neurons.py @@ -13,7 +13,7 @@ scene = Scene(title="neurons") # Add a neuron from file -scene.add(Neuron("examples/data/neuron1.swc")) +# scene.add(Neuron("data/neuron1.swc")) # Download neurons data with morphapi mlapi = MouseLightAPI() diff --git a/examples/screenshot.py b/examples/screenshot.py index 6fd7e462..efe6b50a 100644 --- a/examples/screenshot.py +++ b/examples/screenshot.py @@ -32,7 +32,7 @@ "focalPoint": (7718, 4290, -3507), "distance": 40610, } -zoom = 1.5 +zoom = 2.5 # If you only want a screenshot and don't want to move the camera # around the scene, set interactive to False. diff --git a/examples/user_volumetric_data.py b/examples/user_volumetric_data.py index 3dd1c32c..ddfd7f8a 100644 --- a/examples/user_volumetric_data.py +++ b/examples/user_volumetric_data.py @@ -74,7 +74,7 @@ # 3. create a Volume vedo actor and smooth print("Creating volume") -vol = Volume(transformed_stack, origin=scene.root.origin()).smooth_median() +vol = Volume(transformed_stack).smooth_median() # 4. Extract a surface mesh from the volume actor diff --git a/examples/volumetric_data.py b/examples/volumetric_data.py index f67a5df6..d3c1fb60 100644 --- a/examples/volumetric_data.py +++ b/examples/volumetric_data.py @@ -21,12 +21,12 @@ scene = Scene(inset=False) -data = np.load("examples/data/volume.npy") +data = np.load("data/volume.npy") print(data.shape) # make a volume actor and add actor = Volume( - "examples/data/volume.npy", + "data/volume.npy", voxel_size=200, # size of a voxel's edge in microns as_surface=False, # if true a surface mesh is rendered instead of a volume c="Reds", # use matplotlib colormaps to color the volume From 68bd066d3585fbcc688bbe48ef339004c14956c5 Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 14 Nov 2023 15:53:18 +0000 Subject: [PATCH 02/12] Further compatibility updates --- benchmark/bm_cells.py | 2 +- benchmark/bm_napari.py | 2 +- benchmark/timer.py | 4 ++-- examples/add_cells.py | 2 +- examples/user_volumetric_data.py | 3 ++- 5 files changed, 7 insertions(+), 6 deletions(-) diff --git a/benchmark/bm_cells.py b/benchmark/bm_cells.py index 6170d8e5..c5df472a 100644 --- a/benchmark/bm_cells.py +++ b/benchmark/bm_cells.py @@ -19,7 +19,7 @@ def get_n_random_points_in_region(region, N): Z = np.random.randint(region_bounds[4], region_bounds[5], size=10000) pts = [[x, y, z] for x, y, z in zip(X, Y, Z)] - ipts = region.mesh.insidePoints(pts).points() + ipts = region.mesh.insidePoints(pts).coordinates return np.vstack(random.choices(ipts, k=N)) diff --git a/benchmark/bm_napari.py b/benchmark/bm_napari.py index d802d458..7968ed8f 100644 --- a/benchmark/bm_napari.py +++ b/benchmark/bm_napari.py @@ -21,7 +21,7 @@ surfaces = [] for act in scene.clean_actors: surfaces.append( - (act.points(), act.faces(), np.ones(len(act.points())) * 0.5) + (act.vertices, act.cells, np.ones(len(act.vertices)) * 0.5) ) # render stuff in napar diff --git a/benchmark/timer.py b/benchmark/timer.py index e6eb560a..2422a9d6 100644 --- a/benchmark/timer.py +++ b/benchmark/timer.py @@ -37,7 +37,7 @@ def __rich_console__(self, console, dimension): ) for act in self.scene.clean_actors: try: - points = len(act.points()) + points = act.npoints except AttributeError: points = 0 actors.add( @@ -71,7 +71,7 @@ def __rich_console__(self, console, dimension): points = [] for act in self.scene.clean_actors: try: - points.append(len(act.points())) + points.append(act.npoints) except AttributeError: pass tot_points = sum(points) diff --git a/examples/add_cells.py b/examples/add_cells.py index 42dcfb3b..a52b0c59 100644 --- a/examples/add_cells.py +++ b/examples/add_cells.py @@ -22,7 +22,7 @@ def get_n_random_points_in_region(region, N): Z = np.random.randint(region_bounds[4], region_bounds[5], size=10000) pts = [[x, y, z] for x, y, z in zip(X, Y, Z)] - ipts = region.mesh.inside_points(pts).points() + ipts = region.mesh.inside_points(pts).coordinates return np.vstack(random.choices(ipts, k=N)) diff --git a/examples/user_volumetric_data.py b/examples/user_volumetric_data.py index ddfd7f8a..19b18e1e 100644 --- a/examples/user_volumetric_data.py +++ b/examples/user_volumetric_data.py @@ -74,7 +74,8 @@ # 3. create a Volume vedo actor and smooth print("Creating volume") -vol = Volume(transformed_stack).smooth_median() +vol = Volume(transformed_stack).permute_axes(2, 1, 0) +vol.smooth_median() # 4. Extract a surface mesh from the volume actor From 22e9a767287e4ec7fee99a02f7e4e0623ffe7b2b Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Tue, 14 Nov 2023 16:00:49 +0000 Subject: [PATCH 03/12] Changed function name in bm_cells from camel case to snake case --- benchmark/bm_cells.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/bm_cells.py b/benchmark/bm_cells.py index c5df472a..4af2f5f1 100644 --- a/benchmark/bm_cells.py +++ b/benchmark/bm_cells.py @@ -19,7 +19,7 @@ def get_n_random_points_in_region(region, N): Z = np.random.randint(region_bounds[4], region_bounds[5], size=10000) pts = [[x, y, z] for x, y, z in zip(X, Y, Z)] - ipts = region.mesh.insidePoints(pts).coordinates + ipts = region.mesh.inside_points(pts).coordinates return np.vstack(random.choices(ipts, k=N)) From 6ee0daf5cc150a8325ab498d948e531716c5ce17 Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Thu, 16 Nov 2023 16:48:42 +0000 Subject: [PATCH 04/12] All examples render correctly --- brainrender/actor.py | 2 +- brainrender/actors/neurons.py | 4 ++- brainrender/actors/points.py | 2 +- brainrender/actors/ruler.py | 5 ++-- brainrender/actors/volume.py | 6 +++-- brainrender/render.py | 46 ++++++++++++++++++++------------ examples/neurons.py | 2 +- examples/user_volumetric_data.py | 7 ++--- 8 files changed, 46 insertions(+), 28 deletions(-) diff --git a/brainrender/actor.py b/brainrender/actor.py index ebbc9feb..3e12defa 100644 --- a/brainrender/actor.py +++ b/brainrender/actor.py @@ -58,7 +58,7 @@ def make_actor_label( # Create label txt = Text3D( - label, point * np.array([1, 1, -1]), s=size, c=color, depth=0.1 + label, point * np.array([-1, -1, -1]), s=size, c=color, depth=0.1 ) new_actors.append(txt.rotate_x(180).rotate_y(180)) diff --git a/brainrender/actors/neurons.py b/brainrender/actors/neurons.py index 22091a0d..de87509e 100644 --- a/brainrender/actors/neurons.py +++ b/brainrender/actors/neurons.py @@ -95,4 +95,6 @@ def _from_file(self, neuron: (str, Path)): self.name = self.name or path.name - return self._from_morphapi_neuron(MorphoNeuron(data_file=neuron)) + return self._from_morphapi_neuron( + MorphoNeuron(data_file=neuron, invert_dims=True) + ) diff --git a/brainrender/actors/points.py b/brainrender/actors/points.py index 22cbd089..e71f73f4 100644 --- a/brainrender/actors/points.py +++ b/brainrender/actors/points.py @@ -134,7 +134,7 @@ def __init__( logger.debug("Creating a PointsDensity actor") # flip coordinates on XY axis to match brainrender coordinates system - # data[:, 2] = -data[:, 2] + data[:, 2] = -data[:, 2] # create volume and then actor volume = ( diff --git a/brainrender/actors/ruler.py b/brainrender/actors/ruler.py index 735cec5d..59fb800f 100644 --- a/brainrender/actors/ruler.py +++ b/brainrender/actors/ruler.py @@ -37,7 +37,7 @@ def ruler(p1, p2, unit_scale=1, units=None, s=50): dist = mag(p2 - p1) * unit_scale label = precision(dist, 3) + " " + units lbl = Text3D(label, pos=midpoint, s=s + 100, justify="center") - # lbl.SetOrientation([0, 0, 180]) + lbl.rotate_z(180, around=midpoint) actors.append(lbl) # Add spheres add end @@ -46,6 +46,7 @@ def ruler(p1, p2, unit_scale=1, units=None, s=50): act = Actor(merge(*actors), name="Ruler", br_class="Ruler") act.c((0.3, 0.3, 0.3)).alpha(1).lw(2) + return act @@ -66,7 +67,7 @@ def ruler_from_surface( p2 = p1.copy() p2[axis] = 0 # zero the chosen coordinate - pts = root.mesh.intersect_with_line(p1, p2) + pts = root._mesh.intersect_with_line(p1, p2) surface_point = pts[0] return ruler(p1, surface_point, unit_scale=unit_scale, units=units, s=s) diff --git a/brainrender/actors/volume.py b/brainrender/actors/volume.py index 4f49ce75..6257b856 100644 --- a/brainrender/actors/volume.py +++ b/brainrender/actors/volume.py @@ -78,12 +78,14 @@ def _from_numpy(self, griddata, voxel_size, color, **volume_kwargs): Creates a vedo.Volume actor from a 3D numpy array with volume data """ - return VedoVolume( + # c no longer valid parameter for Vedo Volume + volume = VedoVolume( griddata, spacing=[voxel_size, voxel_size, voxel_size], - c=color, **volume_kwargs, ) + volume.cmap(color) + return volume def _from_file(self, filepath, voxel_size, color, **volume_kwargs): """ diff --git a/brainrender/render.py b/brainrender/render.py index b2dcc0d1..60f7c547 100644 --- a/brainrender/render.py +++ b/brainrender/render.py @@ -8,6 +8,7 @@ from rich import print from rich.syntax import Syntax from vedo import Plotter +from vedo import Volume as VedoVolume from vedo import settings as vsettings from brainrender import settings @@ -21,6 +22,7 @@ # mtx used to transform meshes to sort axes orientation mtx = [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, -1, 0], [0, 0, 0, 1]] +mtx_swap_x_z = [[0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0], [0, 0, 0, 1]] class Render: @@ -88,24 +90,24 @@ def _make_axes(self): # make custom axes dict axes = dict( - axesLineWidth=3, - tipSize=0, + axes_linewidth=3, + tip_size=0, xtitle="AP (μm)", ytitle="DV (μm)", ztitle="LR (μm)", - textScale=0.8, - xTitleRotation=180, + text_scale=0.8, + xtitle_rotation=180, zrange=z_range, - zValuesAndLabels=z_ticks, - xyGrid=False, - yzGrid=False, - zxGrid=False, - xUseBounds=True, - yUseBounds=True, - zUseBounds=True, - xLabelRotation=180, - yLabelRotation=180, - zLabelRotation=90, + z_values_and_labels=z_ticks, + xygrid=False, + yzgrid=False, + zxgrid=False, + x_use_bounds=True, + y_use_bounds=True, + z_use_bounds=True, + xlabel_rotation=180, + ylabel_rotation=180, + zlabel_rotation=90, ) return axes @@ -118,8 +120,8 @@ def _prepare_actor(self, actor): Once an actor is 'corrected' it spawns labels and silhouettes as needed """ - # don't apply transforms to points density actors - if isinstance(actor, PointsDensity): + # don't apply transforms to points density actors or rulers + if isinstance(actor, PointsDensity) or actor.br_class == "Ruler": logger.debug( f'Not transforming actor "{actor.name} (type: {actor.br_class})"' ) @@ -129,7 +131,16 @@ def _prepare_actor(self, actor): if not actor._is_transformed: try: actor._mesh = actor.mesh.clone() - # actor._mesh.apply_transform(mtx) + + if isinstance(actor._mesh, VedoVolume): + actor._mesh.permute_axes(2, 1, 0) + actor._mesh.apply_transform(mtx, True) + elif actor.br_class in ["None", "Gene Data"]: + actor._mesh.apply_transform(mtx_swap_x_z) + actor._mesh.apply_transform(mtx) + else: + actor._mesh.apply_transform(mtx) + except AttributeError: # some types of actors don't transform logger.debug( f'Failed to transform actor: "{actor.name} (type: {actor.br_class})"' @@ -256,6 +267,7 @@ def render( bg=settings.BACKGROUND_COLOR, camera=camera.copy() if update_camera else None, rate=40, + axes=self.plotter.axes, ) elif self.backend == "k3d": # pragma: no cover # Remove silhouettes diff --git a/examples/neurons.py b/examples/neurons.py index f1c17a9d..09f80a14 100644 --- a/examples/neurons.py +++ b/examples/neurons.py @@ -13,7 +13,7 @@ scene = Scene(title="neurons") # Add a neuron from file -# scene.add(Neuron("data/neuron1.swc")) +scene.add(Neuron("data/neuron1.swc")) # Download neurons data with morphapi mlapi = MouseLightAPI() diff --git a/examples/user_volumetric_data.py b/examples/user_volumetric_data.py index 19b18e1e..07c81f9e 100644 --- a/examples/user_volumetric_data.py +++ b/examples/user_volumetric_data.py @@ -28,9 +28,10 @@ from bg_space import AnatomicalSpace from myterial import blue_grey, orange from rich import print -from vedo import Volume +from vedo import Volume as VedoVolume from brainrender import Scene +from brainrender.actors import Volume print(f"[{orange}]Running example: {Path(__file__).name}") @@ -74,14 +75,14 @@ # 3. create a Volume vedo actor and smooth print("Creating volume") -vol = Volume(transformed_stack).permute_axes(2, 1, 0) +vol = VedoVolume(transformed_stack) vol.smooth_median() # 4. Extract a surface mesh from the volume actor print("Extracting surface") mesh = vol.isosurface(value=20).c(blue_grey).decimate().clean() -SHIFT = [-20, 15, 30] # fine tune mesh position +SHIFT = [30, 15, -20] # fine tune mesh position current_position = mesh.pos() new_position = [SHIFT[i] + current_position[i] for i in range(3)] mesh.pos(*new_position) From 8bacced4c522e7e9a9c1e30c9379b7b3ab8a95ac Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Fri, 17 Nov 2023 13:29:47 +0000 Subject: [PATCH 05/12] WIP writing tests based on examples --- tests/test_integration.py | 100 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 tests/test_integration.py diff --git a/tests/test_integration.py b/tests/test_integration.py new file mode 100644 index 00000000..994a52aa --- /dev/null +++ b/tests/test_integration.py @@ -0,0 +1,100 @@ +import numpy as np +import pytest + +from brainrender import Scene +from brainrender.actors import Points + + +def get_n_points_in_region(region, N): + """ + Gets N points inside (or on the surface) of a mes + """ + + region_bounds = region.mesh.bounds() + X = np.linspace(region_bounds[0], region_bounds[1], num=N) + Y = np.linspace(region_bounds[2], region_bounds[3], num=N) + Z = np.linspace(region_bounds[4], region_bounds[5], num=N) + pts = [[x, y, z] for x, y, z in zip(X, Y, Z)] + + ipts = region.mesh.inside_points(pts).points() + return np.vstack(ipts) + + +def check_bounds(bounds, parent_bounds): + """ + Checks that the bounds of an actor are within the bounds of the root + """ + for i, bound in enumerate(bounds): + if i % 2 == 0: + assert bound >= parent_bounds[i] + else: + assert bound <= parent_bounds[i] + + +@pytest.fixture +def scene(): + scene = Scene(inset=False) + yield scene + del scene + + +def test_scene_with_brain_region(scene): + brain_region = scene.add_brain_region( + "grey", + alpha=0.4, + ) + + bounds = brain_region.bounds() + root_bounds = scene.root.bounds() + + assert scene.actors[1] == brain_region + + check_bounds(bounds, root_bounds) + + +def test_add_cells(scene): + mos = scene.add_brain_region("MOs", alpha=0.15) + coordinates = get_n_points_in_region(mos, 1000) + points = Points(coordinates, name="CELLS", colors="steelblue") + + scene.add(points) + + assert scene.actors[0] == scene.root + assert scene.actors[1] == mos + assert scene.actors[2] == points + + scene.render(interactive=False) + + root_bounds = scene.root.bounds() + region_bounds = mos.bounds() + points_bounds = points.bounds() + + check_bounds(points_bounds, root_bounds) + check_bounds(region_bounds, root_bounds) + + +def test_add_labels(scene): + th, mos = scene.add_brain_region("TH", "MOs") + scene.add_label(th, "TH") + + scene.render(interactive=False) + + assert scene.actors[1] == th + assert scene.actors[2] == mos + assert len(th.labels) == 2 + assert len(mos.labels) == 0 + + th_label_text_bounds = th.labels[0].bounds() + th_label_bounds = th.labels[1].bounds() + root_bounds = scene.root.bounds() + + check_bounds(th_label_text_bounds, root_bounds) + check_bounds(th_label_bounds, root_bounds) + + +def test_add_mesh_from_file(scene, pytestconfig): + root_path = pytestconfig.rootpath + scene.add_brain_region("SCm", alpha=0.2) + scene.add( + root_path / "tests" / "files" / "CC_134_1_ch1inj.obj", color="tomato" + ) From 06747b8f9aafbcde17a97fefe236651c440d697c Mon Sep 17 00:00:00 2001 From: IgorTatarnikov Date: Mon, 27 Nov 2023 15:52:24 +0000 Subject: [PATCH 06/12] Added basic tests for each example script --- brainrender/actors/neurons.py | 3 + pyproject.toml | 5 +- tests/files/volume.npy | Bin 0 -> 637432 bytes tests/test_integration.py | 309 +++++++++++++++++++++++++++++++++- 4 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 tests/files/volume.npy diff --git a/brainrender/actors/neurons.py b/brainrender/actors/neurons.py index de87509e..66e97f29 100644 --- a/brainrender/actors/neurons.py +++ b/brainrender/actors/neurons.py @@ -77,9 +77,12 @@ def __init__( self.mesh.c(color).alpha(alpha) def _from_morphapi_neuron(self, neuron: (MorphoNeuron)): + # Temporarily set cache to false as meshes were being corrupted + # on second load mesh = neuron.create_mesh( neurite_radius=self.neurite_radius, soma_radius=self.soma_radius, + use_cache=False, )[1] return mesh diff --git a/pyproject.toml b/pyproject.toml index 48c6d3b4..0b1fb10f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,7 +10,7 @@ dependencies = [ "numpy", "pandas", "h5py<=3.9", # vedo requires hdf5 <=1.12.x but hdf5 is 1.14+ from h5py 3.10 onwards - "vedo", + "vedo>=2023.5.0", "k3d", "imio", "msgpack", @@ -58,7 +58,8 @@ dev = [ "pre-commit", "ruff", "setuptools_scm", - "pyside2" + "pyside2", + "imio", ] nb = ["jupyter", "k3d"] pyside2= ["PySide2"] diff --git a/tests/files/volume.npy b/tests/files/volume.npy new file mode 100644 index 0000000000000000000000000000000000000000..8a5477faaf2f1587a095218725f21070ae3ee3c9 GIT binary patch literal 637432 zcmeEP30#g#+b@+CqK!n9ltgKHmV24)p`UG}td%y2}J38mg@8?|CH8c0j_5aWE%*;LAC){T2@DURwB|Icn z=+AU?o8hX@YU{&Ue0@W0{aG%qi(RKX+q<~VbgYlJoIc;pu`b?i&h&+jb#@;CZK%yR zHPjYT28P-zwf_%6K|;bi_vid4Q5Swz-&)$Qzq$C{7wUNs4=vpeKU00;Jk;wylW#5U z%imlyw!b(RE!_@3V|^SSKcjCg?NdD$EnVl&THoJXH1>tRx%iulAC1$$`vuo8&OE&? zYLDIr}#k-nqBbkFzs2Z2Fsvx_)8gJ^}3AXbk@D7v8z4 z`-4G`_8WfXV*C>)=2>>9l7aOE}Z1YGqz{<{AK4+nalQI{GEBfa$#P) znK_b9{<2(XP9Fr+eRY53;_y*E^C>5V`m2tSnXhMp_-l@lA2IQxvGmu7t7oHm|M)BSi@&*O z><@o)(Uc30kH6~p_#1!I-vn;%{44i|zp*#%6WkaP?;GO!pOvp4VXTk)QTVfR@Oy3l zBM!vl{;Yg(7&&~L{m&$qm*Z00Zp}J}nZv-@|4edu>t%fqw_CIR9Ze%1&i-do9B+L- zal19^9A*v!Xa6(F<*k?XLELW5`gb&qcsTo?NpZaO`NZwktaF$-44nPXB$u~d)(3IB zHS6EeaCj_t^LY*rSW_;xULLV1Gz~a9foC!|$!za&o4PY`alG|8#O>Cs*JG&<-!YW0 zh!zYljN=^wTD%AWEDe0GOey8Ky* zu*Zs>6m!IxIUQ8MD6Tw2D=)TVR&=~hC#Ui1Ya)m^}SDuDIs5YmK;6H@+7`blf%CHZV9 zW0%$T*ZU^*IF!};uN}nO=a1v^XuWw<*Mk(Vrk^RcoDcTzdhJ@{yf;Z@cUhP zuKmHJv_B0O?-C4~V5yO-WHzc(%$?XTfY*kPj;&mcc5HeIW?YR2t8EiNpv-_K{X%x` zpQm5QrFu76AIz@_VwQS!V%+Cm0y?H2V9Ziul;ac!5+>&Z9Y+B;k`dLUAH|ow`qQkn zxW3Lw*NgLMjq8A!wfZ@z7OaDPU;xZ2y2=jpyT#7Vg6#Fem?r%wsDnzwUWO~9waJ2W zZ5l9)SahhNZBMsP6ds%EPV4OzfHOO90j;^dV8-~ZOpj+;Ov<%gbmXq4e>btUJgU|2 z&7O`8YrwF5+_DBeCwX*0+Dm zUV~<5J5BdHt)q`Lo;y_WLhJ4=e#g+G*w>h`lweK8Lczo;I`H!@I=AMEu*Svq2X{QbKh^Zv)`GpcpBxsB z3yu#7iBfM>ewgr(&^Kg_a8CdDW__YDA6G)_?GGF$9Fl+JqQgCP)4Quq@P?2;-i+Mj zCTHT`pAvtDHSi)Z3iS;o79G*<-T9{bfrpLo9tU# zCm$R(am<{~wJ(>4lU)3tO^V|a*Tr%EUc7jozlvVZ#a{*M-#8oFTD;xGb&jL@xcVS& z|48TJTC4nyvgv=tu{2F-w75L=F`V9-Wh1htT0N%!P2lp0%ht%pKf}sl`)7IT)0*dN zjXX5R&>Zu>O6T%7&-XVM95bzV-e^SdpVR(~{o?=1&Yy>iBThjkR|ecME0(b1d3}{J zjmXV3-Bl3G?_2&#a4jWK5D|WmXJMntbC_4fOItdaNAJST&bnGB7eNO`)}85p4}-ph zn>uAa;RU`k;V-UQD^O!(g^PV$1sN)`f*r72`22)6?X&$Jlr zO1{Jge?i~)1i}8GLP{-6j`r7iN#9nw#LOG9hIza#n7(3rggFy+gH>B_mwor{H3OHs zu~B;OnSJN=8S^|u-*jv%{Ae_S+WStv$-C*+^qV5}bS(8^*+Su=6Dz1W1qH$m6Asb# z_op%^MtL)x^pEwN?x5-0(>L+%7-w+^u+YErD6gKGPdB#*V zhjMlzOrP~1o4jjoOUvIMuWENS<3Vpp$2fAK5J7t6v#(zWFM`X z#OkjY1y-7$hE;L0Xoz$XxLv9bBfGYRGo^ijr6*>ex?Q9%?@wk+okzFYb1idEUux6# zz0}tS`Sck_9wX~z4SaU2fweJX!O1(HKvJ?1*r@9X=&gS6%GRss>U9+y;cx&RS$7Ub zm-xUj8ad#SK?1vbU=-6Oz!~VsE5p{*7sP!P7HBieOeE;%<1+waN8TeUpU`e(h-&WURg;O~KY1^2ga@y-Lr4nKr9FZ8)p46O6LZ^P|iZV+lBj|Mr zrk!dF&vd^5KJQu$20vN{O+6f29T$g=B(g5%lYvIpE39tLY2fgB3p`jM1y@b^0u}Ak zppk!Hd}zU7lxZCV^@2X2%Ueru&7e1!RS83LzIq@7)7gk`*8wpnJ3-OZHqZrxfwlA0 z;U@NYs~RI2A?N6E2fMQK(zdf5R$pgJ23o?i<$UC6J`g_ac^xiQdRSqx148uxb=seWM);+BF{Tbsh<9t&cak+xIii zm-W6~eppMZ*lV&zUJn`JSbz3jW_NgSgf4RP@rFw4BVp};>u{>tAZ#Xc8rF>O0HsI2 zM5aR`a6#W)c$h&7GMS=`RF$rwNvsbln4Sc6XN^RA1_Z*TqB-E2U6)qXFY5W&6grXx z;S1R8kwW&V;asq;w+EC}v4Gv~c7s=RykW4LJT4j>3@4POL%i1qhsj;RiarV?f5lOB zE!-E4e7+SG9s%fbyf&QQeh0L6xC{EN?*lAe-)&X>qMi#k56J52__K%DH;nn)9^lkG zC3w|-22e=X1h(ruVD8PgXjvNvSUj~YEWG;`Y5B%r!korK6sMpMCpI9*eSFl;J{5X) zT?dSui-4N(LssCooZTGWqt$VtlRlTJy|RWeS}w&Dj9SAQ)wqL&Wri$NnZO!<7Qrs} zgOKCpN^rGyI_Om%g6@nWc;7KIjGl(WsLop;+s+sEU)CRveSM3)G5ZGlwdyM4nmUG| z9<^;%dlHRDZ-k4Af1$#{u2S#M?WUiPp2B9WJ0ETh+}?{QDH}J9!u}3xkh`uDqx2vUIOqMk2PO45y+jN zi1L%?Bl$sju*mlp_JP_-=0)jqR@p-rjLx{sRDRql9O#()fvY1Z8OYn z$q(UOO>#6df9)>Zv%!S!D{+)IJ*Yr^8}y1gc|eJ+Nqxh5YkM$-!)~&D)~ce~=h|rM zcxmWa+lI}lsHM}&E;1IoG+F<7cj=cCuh7fX=-Qp{e-J5O6g2GF>d zJN+&`mfq86GnH>Ui&kHfPnpPc`|X^pHE_WB*vLiDj2KgEOMhO;Vq3lyh!o~&88R;h z`LTCq&S5$%HKh6v9Z6fyUe9*P3TFc)Mz9|xm6>xM>nN{*dBWlju|nP5szSe8L#goD z2ZceelzzMa$<@;uWFr^i+Dz?AL0aef4xR|X| z>%!l*cCTWL#_Dfb;P`iTg)oarLwY`6K`T?{Og=Q>bhg zK8s?vw7Pv+Q`Gfs_kRhEZC+_c@-!&f($g=JNM=;-34n$a>u3Sj26P z6E3~E&BgyMn{xPPro!czz_s@itEvPvgpJjj}ne z|F7LzdHMh1{#VbpmcH??aQr9I|K_5R#lN}uvvINb@*tl4um}8@)BAauxtq;&yedsE z6>sK6R9%$dIt2{yvNiLs4IL_Hd1JNMTY9);~-RCHJ-m)WNjwV zRaxvzqjhlcZ9xF9>cC;a%Gv_q)yioCW$(*oT{|Z7PaoaO{}Sge{B)}iy|}PF6Y4jQ zF1nsgbr`dj>hEI0_>CIRhQ8>^I$TJhXBb_e?^MY$b3De-$_jqMjHwcW;jdkJQ6t@d zy%ZnS;M_AQl%)2fSs)#y+*IO!xi?f6!f?e&&1?&?dQm@i9Fjuk>zyQNgG^vYKA zuBcHt!Pj2t4CnTUdvWJv4tl5KwAaTb)@Un6T@V$@)L7(=}Ii(P;9Bl`eT+@Ze3YE~!MPsnXz4f^F z`{~H1PZ~lUccW|FOi>q^yTEVwHTFYJ3bZaVNAi!;TFqUQ!Pj=ueJ2ujx9tM5Du`5YRMV@&H%=%Gs`kXC&8zsJ>ckfvw_=E6DU!Y1oQ9K z!lOt5w~M=lUfUvUyyXZ8)Vqbk?UBEiUi={ej7v_cn;doFQJF`M6@p50(I)#0pIyn zh4xsbW9N{b_?N}u=+mp+SZ_-a`Ji#1>@r=APd^xqKid@Ijr%&|Uq(Pwu(bp_Msz@@ zpL{{D>{g=2bCI8Wosq%o%2oyJV~_SZ2A0q248J~?g;Nj9qo9%l=*lvGl;>!PXRfS4 z$*Wb!_E$Z~qv~#0Q5cImD-;ps!M926+ckKFwmlw~S%e2gTi{hu^Uw_|AGH4$8>AK8 z1G`M!+-kTGddz1Xwn~B%0u88M_7x5RN8sezVC2<~!g{jl$l#(kX0=rD$cT}|E;Sq9 zJ97q|h}weZ7#t-P?XHnsmp0%b9aAxN?J1tLI2iBt@_lHD5*?(BzjReGbhlf7FF7nNRt%$ar{*jcYMV2SHQ zsFBhaGP^`byVD{ZIa&^{EFObu+C4xEs|opKtQtO~9*meuRWj~=5}CB(C>ecdE56@# zANIHGL#n19$FV2%kl&(kG~szqRJch48T#6{8ZPK>9T=@1#q73ODlkLj2`ysYgW0n* z&`w$tdr1bMp^h8j*4Izbl#zV0*zOIweXJYGk3Wdz=LeCk8)C`T>z3F^bu$hPK7^%u z+2D6+x1f+VKs}D`gIlKThngE6w3>D2lKGKLkse~V4O#%=7DofyDV@M{$r!l$+iv8X z&;eBoyTc8S&!JIMU*dSTV6>XVr|zOvs-rEWJz%x9J_lfvhUXyDca|O zz27MG#BV4z8uSJB`IZQ;yDr6FL-ok}#jo($3*FJcs03)#ITd|4upd1h(jHz+jR#2^ z1^}r&dO)Q@tJUnyCS6-V8)}-+%dVv`=IiD%NwZYhEBF;yWhRMc%~=a|&Dwx6HW3!z zi$v4+(C`#}4-Pw!fGu|3!n&%#*wQ{9-g{R z(xy6lPn&Jk4>BvfxxuvS6?tmai5s*+n?jmA4rGlw76H-mST?NqGo3a?feHG2k=4nH z1B#;z;byDzY*=L;5X*DS!HNON2 zLIh`op4PrJHzXL!lp^b5(g!u{5!{JgKBJk2F*c;zn+ z{ouV>eYvf(Uo>T;sl|ps0kRFG21M_oYL5>VrVo|+hkhVD$^#kvT@d$qE*yvVt zKfWo}e`Osiwc~ZXk|-D)nkiWMa96`OWyGJuIFJ{jpe{@;T_sG@$`#}4XYU*0DE_rB zWqHEn@XU5RyO%wAV{o@0jt7nTK$sDbL+3x^F?{XZy1zH84?jB(_5L*vKZm6-l=gYOTFtpdBM+Sa^|(2`UUK$#tuI zhr=r_|5>_t8i!fDTrTcsm*RN;6Z(&M`B^#O+Uq}o`(KsuvvR=s^{@Q@Po{Hq{;cxf z$C^wPoAfd=;*rBqJfDH5f4JuSaj8Gzpg!l1;{S-aAA8+Ac46RZv!utm{10nmc@=)i zydk&O3uI2e7C5L%3la-N0yD=oyuo=#O(Qyv<_RkfnH|#^DR!5UEA2=4>eGG{|3}1e zem0eTe^D@*m->QtJ*Ge~LFS0yh2W&%c&IrwZP+?tF|UWvS8E6rDt(YLF<3#dj{7LN z04d>>{2~G0ra~zDakTJcaxC9QIlyevxckk%;n^H}bNi3rfUEO62UW|XOmc58;a%VS zoUgSdgzu8Eo3C!UM0nuN3u>6zRZ6W?m;Ug6F0(%=9@5DxY z1381%v!7P&Wz){8fr=bo)}xIiQ@(Qtb@^tx@FQFxkZN!7+yDQqIrirEAHe}v=XWkT zwQoxmbq{Cm?M-DDTKF>V6-vx4>-$VVM|HN>_UUZ5BB5o{B8vt)bAni>XXRI?_0t=DOO=3bvrQqMALuUvp2`y+~)lH zBcy~x?XhiUhD&$SiKmP~%yR@26=lJuN*{JmKu?ggLKCF=9t7vw4n@{x;YhoCe{?tX zJiKoa52vk2K(|M%!+2`lx24Jgka=tx(q8FS!O93)e(MYGopylZ&$*xzy6NbW zofOWH-Hv(^Ep)bbd;D2migexDg-rL+#1a+u_+7W*So4tro~6+TjhKD_%nr?hhy5Ip zwc*LS|F6BNP^|+uRyaO>9xfJaE)*194iNV8UdGmDT0`Z@{!nM-Ca`uaguAJMD2``{ zZf)$2*6jF-^t)_F?_OHrwB&>6L1$l7n>q=*cugZoV}r@-1xvAW$2fd2`zCgKw-k#E z<|CJwbx_As84u#+VcBQ8XyXWP+Dp^D*}d?ehYMq)3lx9Y0cxX>E^9gXE!b0tp})g< zFd=;fj2VVen4dk$y%CEP%8%hr+rJ|FgiSc=f+Uv7s73G74&cSJ^NFX^Q{p~25PK|K zjiWDr!_aUIPQw|f@R&XtvG@|U$Lq*(t149XwJ$qytV*+QOZ_}t$U078geL%VW4{7h zb}Ja1ioOSD?YRVVR?mhf+nJ(AA?fH!Mmd^yyb8ahhT-lTWQg^8h`(V&teI&{OxJc5 zElB7gQu9y6v+u>?=b2r|r!D~)+<%7VwB3fPGbPB&jp4*SLI$fU$$+YJc|R=|m)le^ z(UnVCLmN#{{zeW?y+0piD(^#9&*ac6_d@jkj3!>!MIDdNe2vv7tisPHLy|du1NQhl z0QY&gh};b}6^)4MFM6ILNko176NlNJq;Oj?K7N+L!$!;EH;b}yl!Fx6@h}SM9qa}q z+UhsEdm!E~#C2{yX*n6vK@Hq>H3elaPlJ_Kc5th+CDL2tjh6bkqgyd^@WKT#*yYfC zy!>P*vT}SZ78uVaNnr=@E2S|wwLpvby7UxXe$5w^WoVHA0VI2F29ur-rAh7l5bVF+ z8Xt7rh|jM-ft|;-!R?n{1j97j{X9Jv+QL(~aTBIUCNWaw0y%v1^^ zXViKS^IIAC+36T8w>lZeKi4GD2~)7c8dn(Q68+P1aadP_ecg35X!lV87KWXGh1Tm) z=M7qTd0=0hxiSZ5C_TpG?*-xc;k^k7KY}fyUy$s3T}a;?OZ@4*7D<0?Ao7Yb7YU9V zll|7C$@zjx^tUG8C|nt&QMb?iJ`Ns6 zt!>b`xR#Uw-`=qwW_p2kOE&?!?It*P(m=HQbtFpESHP*pv3Q?rKT@W31Rs@`C68A= z!BY}DiNbQ;<699u@aGvOM1CJ58ml)(q?QQD#EaudMQ#Eq7~YR4=TE^M)@{b+(plI| z@gTN)r=RrP>gDGvfvh*Xtx2I!B7236@wX+n7vOqePE-Akh&6 zQi_D0+NHpKuk`71P;BnhF;ki{;@fyQTIEBO^cDx<%nxfa_9l?{ePwU<-e(x7* z_qR4%d$ioVS?@!nSr&6@sT-?zr$3mkya{SWXQ8FXD^S>rG<2tG3?BNFAr`Mr;9-_( z_}l&>?0vGc$g!t1$=u|NC3>wOIl}&;$)1LyD$1_tDtR72 zh2gR|_xc(%V*LudHK{*Qw_Atr)*M3HHL7sn-rl0#nIG`7d$w3|#w4=CvY+UbVjt0G z^Yb{YsuV9s+fEj&bR-9-7ojw3TfCgE##t}?@Y~Ug(GE)`@afI^Ci4Yatl}1+j|htd}pF$lZATrd5^r#gyYH- zb?o=$GmJHlWhdO|(u9jUQEeLLkNsd$leCuPp}wx;N?tTPUBLF(JPGjKbwU5T2p-8+ zhQ*_N;mB=W@u5Sv@%%$0@E(05WVv_)4w-qF1Sxr7LC`uR`_hO+9oH85OjZ#|E8F5} zr*ESv{q#xxYatnaIT#)Mya;{Va1+g@t&xkL3Ou&|3S)Qwag(-hFC}c4Ke`W|`-gI2 z^mZyG<#2#GQDV-fM$clOs6GHkJvV_3^ck?*XD#Ac%)q9@AE2HZ{&1_D9+o^~MyfkK zM3LK*&{$`CQmG>+x)$+)OloU}>CP+AZTG$S%9#^*kSh;m#>t^6`Y)jQxNgwpYYGc$ z18J91sU}>csL zlC9qG*^m@;F+_ll$lM1(U>|xgI{_zUFF>HH8rmrJ90v{zBNugN6a993kaW9IXyOiO z9HH43Yt?-hd~BN(7(BK&>?=6PD&2WXzhZX^^BJ3lxw5`~oeYHrE}oi~{-Inj1^T@1 zLF)x6M=+HLYFR&XbFwjB=RR^LWE(Dd_yAG5b5nRzJ4h&FpW0szKEc6XG;D0Vv zX!s4S=c2<`vxYI!e}ncP%0>Nds6I^k+Ji66lc1Jwn@FEhsHUFMUxc6MZ)a|#L4*>F})MW0smy!OL8N}nXc8|H|Zn8i)TPs@*!?_Av9(%0;DZ&T`o+-LgKl@$73 z@CxCnc5+O&y(ie}+6?xLgD!j$>xzQ?v!IvOI?yF}B6z?5EWDH$i{6w<;~Sar@Y8l0 z^567??mAjfv=zE!_c25*~+*$O)JDx%t*M>DkcMnmM@$*M<@xV3G_>q~7 zFo;&C4^rnTP!lS6LNip)!&1yePZ=iTNC6wfbc2CmX<+!&(QL7YfZbnp0T{TNpfk&s zpie1f!0YZzuxtAVcKe*m>{P*6M)TffYJlfv;kiNW`Qr>6o4qIbzK&uCw-*~T_3hHK z>#L^qeeW0b|2^n@h#ynoCsZmF2v={D;*Tg>C0KoeM|o`4qM}#dqx4=RDt}bzW z;`qgN@jNZ1bN)$4JX&mIeCL9k>7=R0P5QcS?2p24n2d3YHCumZh*`m;&1M4&H}cs& zs|Aymyy2Y=PM~a;KW3`ODzHc6Bk7^z;)UCd@`XFnQUoe)%XrH^s`7d~{ls%|8YLY3 zyi%xtzRz#p2yc#8+!ohc3cq+gja>Y1wMQTPj6=*W3#_N8(Yf3E&?nT4g@zO7^5jY& zUrMzPFVH>a*Z0aVY@~&cPPo!RKGKZ&GxNGRk_*l2=j{4)PH$<+`S$P2?z}}j#=vp(|&bAp6Kr*j7=$ygIsd&wSFR!&g&+sOrSob_>>Ev|EMEhRbMekALC`2FD5 z?ym8hSamb6PyNhF7xognU(o3LjjkD#d#{e{y(j0y)=>3jHs<+W17|l+_{Ng@wwm2_bIv2;;|0o~r4{lgjHZAub^Zwb> zrKPm~;C1zXl5_F47uUHun#a}K^=Vw-^0fw8@3Xj{;x>opM^YSb^K|hvu52z3Xa8*S zNB(hf|1J5malrZW-@^N^%H`@8m)tYraV@EH_5XXa?V>O<5NO7addKj_J>S=`wm3Xx zqv_YLRy;iQkeRv6sD}7|EL|L9eQLeU1@W|&)VccqcQQIc$p2=1OE6e~1^#Y+!U>mq z3XiHjOWq zZ)XxMvz+(YH<_P2xj#QlH`vU}`2qh7t4yit(X`Zzc~r1q9DVkM1%1V33)Mxok}BU| zA$(N(O<;E2hI-_dFSNR{pFio-X|oa?^B?pNacoWN{%(nFQ>$IboO;=r ztr_G&cblX`%j9%ma^@z`)iID-ynE4~?35dQ0hzwHuYI@GULJ z3$<>npn|P=)QJ>1dSF}!rgyR?8*cKJ@hJPiv^gsR5s!ue^^L*o%@1Mh$MDV|O*Sx;uoPRjv<{780p?Re%<(O2rGMVin&!C9cUv^TG-l&kTw4&HTRM;u?H z4~?35dQ0hzwI7E5LXrP-I(}wbR;{ZOGfv?Wqfon@?L2cYnAUp(P;>kUj%QthQZwhm zq1LCsmH{Kd**)Xnp>t`1#B;k z1%7do@W}@QcqCLB>KTQhhoxUoAQg%R?k$DdCDG7!%yBfs{4gG|W-p%pZV?)rn~2O_ z+&~)k(@|;3Xqdwr2nI#Ovg!H#*|W71Xv^7OcxBVFnw{_d99+!1IF$b}|ASzbZ#$uk z!+vI*tq>|1*dSdaN4Rae4|G+054#(FhO&$zlIVCFhtet}z{($Qx^@paJa~Y-EB4@B zrXD0vXC=AXa~W1S8;Z}H#$W?CBWzYX6B(Rd3Lp2TVA4rrm|Nw^W?j1|kRBz|Y=81| zaN#)Ug<$Z9jg(J94)sQ3DSLR;9_Z2g7@D5g4q44^1NJUXFVt^u|s#3g}??WR&=+ z6irxd2B*K(qrvLt@6vw`E@owSpmb(zpx0Dirl(1*1{2SjAkXa?2w-g#KPeEc+7gV7 z`ex!e3G?y5$kRBXV;P>j#E3-Jagli08qfc%NQ7G+k>FAV(RwL);^?VCM%;EGdTlz9 z=(y>4_VGB>ua7&;%_|eCA*jaxad2>ROqz}x(TQBgj#)+P!$GiI^u6hg}`6eAGR@gN=+xa=TsLc_w zqdY^v*{}V;n;9p;rrVd`TU?72o)w|I>3L|~Xn#D@{4Rbkw+e6Bs7B`I-Nv(IHju@$ z)A4>^Z+!HgJrVSF5PdTlBwF079a-?Y16f*>MqDn6$cjNdFt)8EJTmnl+&#|{=~xfN+tsIGVAKv%C$3_(H`=67p#ZlX zZ%N*3Ku zeLuQ%U+k%ElnBj!tu6WOBS^RkE6CAdC3<;g5L=G1WAg#(K{bd3Yoe*W-CaKFC-k@;OiTXAB_+`wk~PUd<$X+60o7RvYlm zgK==n`kW?QlpiQGd9rW|Pw{{azxrmB*@wwr>fXEl_uBvW%J!TU$|`M@gwAU>L3Q33 zl#^ZeP2aONSmy2$Y*Drrw^5WN(&kpgXORZ6kQz$fYjq>ia#$3(XdHRL$`U*GO+=9D zAc{WfD|%ZMOJq&6NT)fhXo=AiqAfd~-0eD+thh3aWPA)E_8Z1y{VE?=vT;|#-IIF1 zLK4z=Hbpaqe4V$#^(JkFD;Fd*TbH&BFEPS!kkT&>j#qgLpHs3(cWN1m5%$5o_DI*= zOPYs2=oVsIXD3qAu|ILUT-O&aiHMV%wP@k40HS2Tkif!2q_mB%NMCk?=$1?pNn4Ra zmKB?d3I>*wG8aYi$wQtzxv5F)H#*j>$37#~?)QM#(~u@y%w5$(nBYB~9^+q5k23E; zJKZ?bY+c$iT*S$?gI}bhVVqz&jBtGd-FF;CcGBapg~|i8_MRzrdY_B0Sq6}qFZ+@? zChzg2efA{Kb&hEC$E(DP+DM`|ULqahqD1}LMT#mTQb@v@ZNz-1x@dBrc=GYdd~Cle z9w%t&lN&qMkaGdL_}zI8XmIRo6D}&MKT{RG?lL{($FW+64>O;27d2a#whR}QmTBPp zo?fsZ{1ceukO2!MW6&(G0DOM?P&}z;e@yIh@B!g^VrZp8s&7}~+J_s7je3yiY3FAo zYRZ1{XwY+VjoK`to^BMeXA{Vx?3rXoawUl$=|YO_%8{~!2R;C4aDSa8q-@m=V*7Lk{AptO>H(^trz^-<1uv9L+I9o+p=I2w8` z6PLj-qRi_@j;iM1$^Ej(-jpQKV#&>9)ObY_?i5@1xzmNB*>a8|iIrLSP%Xx;UsFi$ za}ngiw}JxHzL;ur0dKF+T+p@R3|Kh7I~*Ce0{9jC zHd~9eOusO>rcZ~e@z{R86b#<>2HyCb3RkNfMibu7MQiK!;34c$d?;)!scff8hF+eE zbxY=wS6Pci)6-0e;$j*P$+II4c5_AB?u-@1KTO0I9*MBwq$Kh|GLgIh61Z&kdvtku z6rN>&8kePpqx=;s!0DaoEO>vRVNO_WTn*)?*`WT8c`$79Xt3x?$sfXn+BaRw>b8LS z(1i!m*F6WboqEEOi;?Is5}`h$Z@}jZL|E0DPwwnGifejBpd%M*v0}%bqMixG_~O7< zXrZAW8S&0VG^Avr$mByhw%Rro%RY@DUv~!*LBKntDojKJEq0**-VwCNb1^j5_Gb^y zDr7X}^BeA~jhYmRo}cll+xxZ0*ZIf5GdJ5mgbU}ru7d8owbY`8KFpO%LUx==5J-pe zNO^85)c2N#9o9vlV@ap*p%=rkAVD659^H?3x}7DjE=6OdH{po!wIJ{j5yd<)7NuJy z;=%Vf;3HrAljm$}#QH#0sbbD^ZS0akx|205Rc z0Di*n7PDy6CR`SDB%{K!sgw~1Xcg~tYM{RC!*g`G9-U14FAGh!y}A8qHkGK(bmNCxZ9cK#KbR{+<(~@Lw74Yc=kFBM+IZ3Q3)?i zc7t*W*}rE_fiN9IfcwS|op*uc;0`@^r>Ps1%MJ7Fu!*?9Hv=}79b1}e6<#om|o z$d}6Dq4sci`BE9%*H)F4D&I_h^-L04_cs&!hRXNf2g{t* zjB3vaHhWMPcKetdieGH~hpfTHxiA@YR&b;0I<0hXEc<3EEOw8tzUJS4E0YW-N!?vS9 zb^kG{qk#-#c-p7#j-p7I-bRhqKT^bu8XCjAyMx)6`dJ`t`V821q7OLt-V;=x8HThi z+u)Z`S82X=@q05=a90@JS_m~mNAwCoy7;g;m>g4+1E&F;5-$Cx(Gmp^vv z3_-TlUjFj&__z0tErnmao<=q*&u!)n(d;GkU)r0R6t_S)R8^W^Y4KR-x-Xude0vN% zV`~u2>(_zJb(a929wxGz)1I;;jqXC9pstANHw`L0xzEa2@IXe%bXI$sG4mxvo_?^u zSg4lg&R4wS&MS1z78@U8{i4wYF-_dZmeL!m&psDmI^b+u{=(Q|{%g}5UPtF@6V>6% z1mjj1QpE%B3d3TS3yB!@6f%o_xv0C3$7ln{y$pQ`y=iHXLEjXc6}Z$i05yr zzr0`bhwLw!Vrc5qAC(q6Ip7akQ~x*ci?_YF&ehdCuHLRs;{unjHOP9O#r+hwIXpj- z;&_{GmAb9p%XXOlnjkBj?n%AbvgdVl`tAkK-nFXHhnrHj}9V||~VcmG|}PMPG4 zhi0`p=Xt|++~-v&UFR1qoh6)je6is9yqaIv6}_Uc>8X>5uUD`~P(DdckTiPpZ|jX8 zm-{`IYY$Fxd0PrUR}WY3clmMm8RO0#E@sMk5xl8ZdS({YXN{eA9yFV7q`~hoN|!(5 z^i%%1q;taTGYa&@w<{@iCYJVEszNV!)}$70Hl}iqW(fMq1Pb3Tt)^1Sh19ue=lRO^ zGNw7fiT#__ByMpWaFR>od~7Mn)%#uUj%jBWH+lzuRMI)YG!IvPUg>_pp|?`h(HbRc z$kJfpgxO;$wGbZTPETe{?rvn}E||p1qpoa_!gi*7=40l}TpK#z#%kKGU^jENR|n>W z%Qs;zG81%oBjjoBar(i1*ZO*!bHMr3QXFvgaP>+^C~n9zdV1<6uh&*zp{&a;>V{X9 z@M0fz+U&Y16BO}?sXiUc>>Rn8x&GDw6kXPV+b5U;rx{B?<>XXw`^YwsJozn9{yLJS z%Q~|rXI``S)BISABLZex;7e*+L9(#@B1b`$#Md8;nV*M?12a9$-apsjPca_L-xdyp z$q61b^U{|w7;VU0Gw96r;wb|4vYS8`M*<_IF(C6rCUlMvpqm~uVRv=_jNiN!p3xAZ zHMW81&aHPqvr8fP;xQjyIHwIOeDr{I{9ER>?Gsw9_Y3NA%n?D?JE5i$ujliV+np9b znF+t0OUEYi<#*T$HKIL*H&=|LW#@XaFS1sGv{gO9InTXdq@fcW z6*>uB%lQ6ZoA8|Y%gY`KBLn)<<|apfTaPwnpwVgx4!AlbB+hlT z6Y6zVq(eV+rAI`3W`tRt;51!Dv^P8n_FANd9KXe)TO|eP<(0u`sOmgyyP+JP)KAC0 z%8EE(K^WRMeKwYs8%<;*mXI!Ec=*cfWw^Xs9`3EX01L{tqY%#nuv@YweC@v%Bp